Import upstream version 0.9.0
Debian Janitor
1 year, 3 months ago
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | # Read the Docs configuration file | |
9 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details | |
10 | ||
11 | --- | |
12 | version: 2 | |
13 | ||
14 | build: | |
15 | os: ubuntu-20.04 | |
16 | tools: | |
17 | # Keep version in sync with tox.ini (testenv:docs) and | |
18 | # docs.yml (GitHub Action Workflow). | |
19 | python: '3.10' | |
20 | ||
21 | python: | |
22 | install: | |
23 | - method: pip | |
24 | path: . | |
25 | extra_requirements: | |
26 | - docs |
0 | Credits | |
1 | ======= | |
2 | ||
3 | ``django-environ`` was initially created by `Daniele Faraglia <https://github.com/joke2k>`_ | |
4 | and currently maintained by `Serghei Iakovlev <https://github.com/sergeyklay/>`_. | |
5 | ||
6 | A full list of contributors can be found in `GitHub <https://github.com/joke2k/django-environ/graphs/contributors>`__. | |
7 | ||
8 | Acknowledgments | |
9 | =============== | |
10 | ||
11 | The existence of ``django-environ`` would have been impossible without these | |
12 | projects: | |
13 | ||
14 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ | |
15 | - `jazzband/dj-database-url <https://github.com/jazzband/dj-database-url>`_ | |
16 | - `migonzalvar/dj-email-url <https://github.com/migonzalvar/dj-email-url>`_ | |
17 | - `ghickman/django-cache-url <https://github.com/ghickman/django-cache-url>`_ | |
18 | - `dstufft/dj-search-url <https://github.com/dstufft/dj-search-url>`_ | |
19 | - `julianwachholz/dj-config-url <https://github.com/julianwachholz/dj-config-url>`_ | |
20 | - `nickstenning/honcho <https://github.com/nickstenning/honcho>`_ | |
21 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ |
0 | Backers and supporters | |
1 | ====================== | |
2 | ||
3 | You can join them in supporting django-environ development by visiting our page | |
4 | on `Open Collective <https://opencollective.com/django-environ>`_ and becoming | |
5 | a sponsor or a backer! | |
6 | ||
7 | Sponsors | |
8 | -------- | |
9 | ||
10 | Support this project by becoming a sponsor. Your logo will show up here with a | |
11 | link to your website. `Became sponsor <https://opencollective.com/django-environ/contribute/sponsors-3474/checkout>`_. | |
12 | ||
13 | |ocsponsor0| |ocsponsor1| | |
14 | ||
15 | Backers | |
16 | ------- | |
17 | ||
18 | Thank you to all our backers! | |
19 | ||
20 | |ocbackerimage| | |
21 | ||
22 | .. |ocsponsor0| image:: https://opencollective.com/django-environ/sponsor/0/avatar.svg | |
23 | :target: https://triplebyte.com/ | |
24 | :alt: Sponsor | |
25 | .. |ocsponsor1| image:: https://images.opencollective.com/static/images/become_sponsor.svg | |
26 | :target: https://opencollective.com/django-environ/contribute/sponsors-3474/checkout | |
27 | :alt: Become a Sponsor | |
28 | .. |ocbackerimage| image:: https://opencollective.com/django-environ/backers.svg?width=890 | |
29 | :target: https://opencollective.com/django-environ | |
30 | :alt: Backers on Open Collective |
0 | Changelog | |
1 | ========= | |
2 | ||
3 | All notable changes to this project will be documented in this file. | |
4 | The format is inspired by `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_ | |
5 | and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_. | |
6 | ||
7 | `v0.9.0`_ - 15-June-2022 | |
8 | ------------------------------ | |
9 | Added | |
10 | +++++ | |
11 | - Added support for Postgresql cluster URI | |
12 | `#355 <https://github.com/joke2k/django-environ/pull/355>`_. | |
13 | - Added support for Django 4.0 | |
14 | `#371 <https://github.com/joke2k/django-environ/issues/371>`_. | |
15 | - Added support for prefixed variables | |
16 | `#362 <https://github.com/joke2k/django-environ/issues/362>`_. | |
17 | - Amended documentation. | |
18 | ||
19 | ||
20 | Deprecated | |
21 | ++++++++++ | |
22 | - ``Env.unicode()`` is deprecated and will be removed in the next | |
23 | major release. Use ``Env.str()`` instead. | |
24 | ||
25 | ||
26 | Changed | |
27 | +++++++ | |
28 | - Attach cause to ``ImproperlyConfigured`` exception | |
29 | `#360 <https://github.com/joke2k/django-environ/issues/360>`_. | |
30 | ||
31 | ||
32 | Fixed | |
33 | +++++ | |
34 | - Fixed ``_cast_urlstr`` unquoting | |
35 | `#357 <https://github.com/joke2k/django-environ/issues/357>`_. | |
36 | - Fixed documentation regarding unsafe characters in URLs | |
37 | `#220 <https://github.com/joke2k/django-environ/issues/220>`_. | |
38 | - Fixed ``environ.Path.__eq__()`` to compare paths correctly | |
39 | `#86 <https://github.com/joke2k/django-environ/issues/86>`_, | |
40 | `#197 <https://github.com/joke2k/django-environ/issues/197>`_. | |
41 | ||
42 | ||
43 | `v0.8.1`_ - 20-October-2021 | |
44 | --------------------------- | |
45 | Fixed | |
46 | +++++ | |
47 | - Fixed "Invalid line" spam logs on blank lines in env file | |
48 | `#340 <https://github.com/joke2k/django-environ/issues/340>`_. | |
49 | - Fixed ``memcache``/``pymemcache`` URL parsing for correct identification of | |
50 | connection type `#337 <https://github.com/joke2k/django-environ/issues/337>`_. | |
51 | ||
52 | ||
53 | `v0.8.0`_ - 17-October-2021 | |
54 | --------------------------- | |
55 | Added | |
56 | +++++ | |
57 | - Log invalid lines when parse .env file | |
58 | `#283 <https://github.com/joke2k/django-environ/pull/283>`_. | |
59 | - Added docker-style file variable support | |
60 | `#189 <https://github.com/joke2k/django-environ/issues/189>`_. | |
61 | - Added option to override existing variables with ``read_env`` | |
62 | `#103 <https://github.com/joke2k/django-environ/issues/103>`_, | |
63 | `#249 <https://github.com/joke2k/django-environ/issues/249>`_. | |
64 | - Added support for empty var with None default value | |
65 | `#209 <https://github.com/joke2k/django-environ/issues/209>`_. | |
66 | - Added ``pymemcache`` cache backend for Django 3.2+ | |
67 | `#335 <https://github.com/joke2k/django-environ/pull/335>`_. | |
68 | ||
69 | ||
70 | Fixed | |
71 | +++++ | |
72 | - Keep newline/tab escapes in quoted strings | |
73 | `#296 <https://github.com/joke2k/django-environ/pull/296>`_. | |
74 | - Handle escaped dollar sign in values | |
75 | `#271 <https://github.com/joke2k/django-environ/issues/271>`_. | |
76 | - Fixed incorrect parsing of ``DATABASES_URL`` for Google Cloud MySQL | |
77 | `#294 <https://github.com/joke2k/django-environ/issues/294>`_. | |
78 | ||
79 | ||
80 | `v0.7.0`_ - 11-September-2021 | |
81 | ------------------------------ | |
82 | Added | |
83 | +++++ | |
84 | - Added support for negative float strings | |
85 | `#160 <https://github.com/joke2k/django-environ/issues/160>`_. | |
86 | - Added Elasticsearch5 to search scheme | |
87 | `#297 <https://github.com/joke2k/django-environ/pull/297>`_. | |
88 | - Added Elasticsearch7 to search scheme | |
89 | `#314 <https://github.com/joke2k/django-environ/issues/314>`_. | |
90 | - Added the ability to use ``bytes`` or ``str`` as a default value for ``Env.bytes()``. | |
91 | ||
92 | Fixed | |
93 | +++++ | |
94 | - Fixed links in the documentation. | |
95 | - Use default option in ``Env.bytes()`` | |
96 | `#206 <https://github.com/joke2k/django-environ/pull/206>`_. | |
97 | - Safely evaluate a string containing an invalid Python literal | |
98 | `#200 <https://github.com/joke2k/django-environ/issues/200>`_. | |
99 | ||
100 | Changed | |
101 | +++++++ | |
102 | - Added 'Funding' and 'Say Thanks!' project urls on pypi. | |
103 | - Stop raising ``UserWarning`` if ``.env`` file isn't found. Log a message with | |
104 | ``INFO`` log level instead `#243 <https://github.com/joke2k/django-environ/issues/243>`_. | |
105 | ||
106 | ||
107 | `v0.6.0`_ - 4-September-2021 | |
108 | ---------------------------- | |
109 | Added | |
110 | +++++ | |
111 | - Python 3.9, 3.10 and pypy 3.7 are now supported. | |
112 | - Django 3.1 and 3.2 are now supported. | |
113 | - Added missed classifiers to ``setup.py``. | |
114 | - Accept Python 3.6 path-like objects for ``read_env`` | |
115 | `#106 <https://github.com/joke2k/django-environ/issues/106>`_, | |
116 | `#286 <https://github.com/joke2k/django-environ/issues/286>`_. | |
117 | ||
118 | Fixed | |
119 | +++++ | |
120 | - Fixed various code linting errors. | |
121 | - Fixed typos in the documentation. | |
122 | - Added missed files to the package contents. | |
123 | - Fixed ``db_url_config`` to work the same for all postgres-like schemes | |
124 | `#264 <https://github.com/joke2k/django-environ/issues/264>`_, | |
125 | `#268 <https://github.com/joke2k/django-environ/issues/268>`_. | |
126 | ||
127 | Changed | |
128 | +++++++ | |
129 | - Refactor tests to use pytest and follow DRY. | |
130 | - Moved CI to GitHub Actions. | |
131 | - Restructuring of project documentation. | |
132 | - Build and test package documentation as a part of CI pipeline. | |
133 | - Build and test package distribution as a part of CI pipeline. | |
134 | - Check ``MANIFEST.in`` in a source package for completeness as a part of CI | |
135 | pipeline. | |
136 | - Added ``pytest`` and ``coverage[toml]`` to setuptools' ``extras_require``. | |
137 | ||
138 | ||
139 | `v0.5.0`_ - 30-August-2021 | |
140 | -------------------------- | |
141 | Added | |
142 | +++++ | |
143 | - Support for Django 2.1 & 2.2. | |
144 | - Added tox.ini targets. | |
145 | - Added secure redis backend URLs via ``rediss://``. | |
146 | - Added ``cast=str`` to ``str()`` method. | |
147 | ||
148 | Fixed | |
149 | +++++ | |
150 | - Fixed misspelling in the documentation. | |
151 | ||
152 | Changed | |
153 | +++++++ | |
154 | - Validate empty cache url and invalid cache schema. | |
155 | - Set ``long_description_content_type`` in setup. | |
156 | - Improved Django 1.11 database configuration support. | |
157 | ||
158 | ||
159 | `v0.4.5`_ - 25-June-2018 | |
160 | ------------------------ | |
161 | Added | |
162 | +++++ | |
163 | - Support for Django 2.0. | |
164 | - Support for smart casting. | |
165 | - Support PostgreSQL unix domain socket paths. | |
166 | - Tip: Multiple env files. | |
167 | ||
168 | Changed | |
169 | +++++++ | |
170 | - Fix parsing option values ``None``, ``True`` and ``False``. | |
171 | - Order of importance of engine configuration in ``db_url_config``. | |
172 | ||
173 | Removed | |
174 | +++++++ | |
175 | - Remove ``django`` and ``six`` dependencies. | |
176 | ||
177 | ||
178 | `v0.4.4`_ - 21-August-2017 | |
179 | -------------------------- | |
180 | ||
181 | Added | |
182 | +++++ | |
183 | - Support for ``django-redis`` multiple locations (master/slave, shards). | |
184 | - Support for Elasticsearch2. | |
185 | - Support for Mysql-connector. | |
186 | - Support for ``pyodbc``. | |
187 | - Added ``__contains__`` feature to Environ class. | |
188 | ||
189 | Fixed | |
190 | +++++ | |
191 | - Fix Path subtracting. | |
192 | ||
193 | ||
194 | `v0.4.3`_ - 21-August-2017 | |
195 | -------------------------- | |
196 | Changed | |
197 | +++++++ | |
198 | - Rollback the default Environ to ``os.environ``. | |
199 | ||
200 | `v0.4.2`_ - 13-April-2017 | |
201 | ------------------------- | |
202 | Added | |
203 | +++++ | |
204 | - Confirm support for Django 1.11. | |
205 | - Support for Redshift database URL. | |
206 | ||
207 | Changed | |
208 | +++++++ | |
209 | - Fixed uwsgi settings reload problem | |
210 | `#55 <https://github.com/joke2k/django-environ/issues/55>`_. | |
211 | - Update support for ``django-redis`` urls | |
212 | `#109 <https://github.com/joke2k/django-environ/pull/109>`_. | |
213 | ||
214 | `v0.4.1`_ - 13-November-2016 | |
215 | ---------------------------- | |
216 | Added | |
217 | +++++ | |
218 | - Add support for Django 1.10. | |
219 | ||
220 | Changed | |
221 | +++++++ | |
222 | - Fixed for unsafe characters into URLs. | |
223 | - Clarifying warning on missing or unreadable file. | |
224 | Thanks to `@nickcatal <https://github.com/nickcatal>`_. | |
225 | - Fixed support for Oracle urls. | |
226 | - Fixed support for ``django-redis``. | |
227 | ||
228 | `v0.4`_ - 23-September-2015 | |
229 | --------------------------- | |
230 | Added | |
231 | +++++ | |
232 | - New email schemes - ``smtp+ssl`` and ``smtp+tls`` (``smtps`` would be deprecated). | |
233 | - Added tuple support. Thanks to `@anonymouzz <https://github.com/anonymouzz>`_. | |
234 | - Added LDAP url support for database. Thanks to | |
235 | `django-ldapdb/django-ldapdb <https://github.com/django-ldapdb/django-ldapdb>`_. | |
236 | ||
237 | Changed | |
238 | +++++++ | |
239 | - Fixed non-ascii values (broken in Python 2.x). | |
240 | - ``redis_cache`` replaced by ``django_redis``. | |
241 | - Fixed psql/pgsql url. | |
242 | ||
243 | ||
244 | `v0.3.1`_ - 19 Sep 2015 | |
245 | ----------------------- | |
246 | Added | |
247 | +++++ | |
248 | - Added ``email`` as alias for ``email_url``. | |
249 | - Django 1.7 is now supported. | |
250 | - Added LDAP scheme support for ``db_url_config``. | |
251 | ||
252 | Fixed | |
253 | +++++ | |
254 | - Fixed typos in the documentation. | |
255 | - Fixed ``environ.Path.__add__`` to correctly handle plus operator. | |
256 | - Fixed ``environ.Path.__contains__`` to correctly work on Windows. | |
257 | ||
258 | ||
259 | `v0.3`_ - 03-June-2014 | |
260 | ---------------------- | |
261 | Added | |
262 | +++++ | |
263 | - Added cache url support. | |
264 | - Added email url support. | |
265 | - Added search url support. | |
266 | ||
267 | Changed | |
268 | +++++++ | |
269 | - Rewriting README.rst. | |
270 | ||
271 | v0.2.1 - 19-April-2013 | |
272 | ---------------------- | |
273 | Changed | |
274 | +++++++ | |
275 | - ``Env.__call__`` now uses ``Env.get_value`` instance method. | |
276 | ||
277 | v0.2 - 16-April-2013 | |
278 | -------------------- | |
279 | Added | |
280 | +++++ | |
281 | - Added advanced float parsing (comma and dot symbols to separate thousands and decimals). | |
282 | ||
283 | Fixed | |
284 | +++++ | |
285 | - Fixed typos in the documentation. | |
286 | ||
287 | v0.1 - 2-April-2013 | |
288 | ------------------- | |
289 | Added | |
290 | +++++ | |
291 | - Initial release. | |
292 | ||
293 | ||
294 | .. _v0.9.0: https://github.com/joke2k/django-environ/compare/v0.8.1...v0.9.0 | |
295 | .. _v0.8.1: https://github.com/joke2k/django-environ/compare/v0.8.0...v0.8.1 | |
296 | .. _v0.8.0: https://github.com/joke2k/django-environ/compare/v0.7.0...v0.8.0 | |
297 | .. _v0.7.0: https://github.com/joke2k/django-environ/compare/v0.6.0...v0.7.0 | |
298 | .. _v0.6.0: https://github.com/joke2k/django-environ/compare/v0.5.0...v0.6.0 | |
299 | .. _v0.5.0: https://github.com/joke2k/django-environ/compare/v0.4.5...v0.5.0 | |
300 | .. _v0.4.5: https://github.com/joke2k/django-environ/compare/v0.4.4...v0.4.5 | |
301 | .. _v0.4.4: https://github.com/joke2k/django-environ/compare/v0.4.3...v0.4.4 | |
302 | .. _v0.4.3: https://github.com/joke2k/django-environ/compare/v0.4.2...v0.4.3 | |
303 | .. _v0.4.2: https://github.com/joke2k/django-environ/compare/v0.4.1...v0.4.2 | |
304 | .. _v0.4.1: https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1 | |
305 | .. _v0.4: https://github.com/joke2k/django-environ/compare/v0.3.1...v0.4 | |
306 | .. _v0.3.1: https://github.com/joke2k/django-environ/compare/v0.3...v0.3.1 | |
307 | .. _v0.3: https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3 |
0 | Contributing | |
1 | ============ | |
2 | ||
3 | If you would like to contribute to ``django-environ``, please take a look at the | |
4 | `current issues <https://github.com/joke2k/django-environ/issues>`_. If there is | |
5 | a bug or feature that you want but it isn't listed, make an issue and work on it. | |
6 | ||
7 | How to Contribute | |
8 | ----------------- | |
9 | ||
10 | 1. Check for open issues or open a fresh issue to start a discussion around a | |
11 | feature idea or a bug. | |
12 | 2. Fork `the repository <https://github.com/joke2k/django-environ>`_ on GitHub | |
13 | to start making your changes to the **develop** branch (or branch off of it). | |
14 | 3. Write a test which shows that the bug was fixed or that the feature works as | |
15 | expected. | |
16 | 4. Send a pull request and bug the maintainer until it gets merged and published. |
0 | Copyright (c) 2013-2017, Daniele Faraglia | |
0 | Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
1 | Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
1 | 2 | |
2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 4 | of this software and associated documentation files (the "Software"), to deal |
15 | 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
16 | 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
17 | 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
18 | THE SOFTWARE.⏎ | |
19 | THE SOFTWARE. |
0 | include *.rst | |
1 | include *.txt | |
2 | include environ/test_env.txt⏎ | |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | # This file consists of commands, one per line, instructing setuptools to add | |
9 | # or remove some set of files from the sdist. | |
10 | ||
11 | # Include all files matching any of the listed patterns. | |
12 | include *.rst LICENSE.txt *.yml | |
13 | graft .github | |
14 | ||
15 | # The contents of the directory tree tests will first be added to the sdist. | |
16 | # Many OS distributions prefers provide an ability run the tests | |
17 | # during the package installation. | |
18 | recursive-include tests *.py | |
19 | recursive-include tests *.txt | |
20 | include tox.ini | |
21 | ||
22 | # All files in the sdist with a .pyc, .pyo, or .pyd extension will be removed | |
23 | # from the sdist. | |
24 | global-exclude *.py[cod] | |
25 | ||
26 | # Documentation | |
27 | include docs/docutils.conf docs/Makefile | |
28 | recursive-include docs *.png | |
29 | recursive-include docs *.svg | |
30 | recursive-include docs *.py | |
31 | recursive-include docs *.rst | |
32 | recursive-include docs *.gitkeep | |
33 | prune docs/_build |
0 | Metadata-Version: 1.1 | |
0 | Metadata-Version: 2.1 | |
1 | 1 | Name: django-environ |
2 | Version: 0.4.4 | |
3 | Summary: Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application. | |
4 | Home-page: https://github.com/joke2k/django-environ | |
5 | Author: joke2k | |
6 | Author-email: joke2k@gmail.com | |
7 | License: MIT License | |
8 | Description: ============== | |
9 | Django-environ | |
10 | ============== | |
11 | ||
12 | Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application. | |
13 | ||
14 | |pypi| |unix_build| |windows_build| |coverage| |license| | |
15 | ||
16 | ||
17 | This module is a merge of: | |
18 | ||
19 | * `envparse`_ | |
20 | * `honcho`_ | |
21 | * `dj-database-url`_ | |
22 | * `dj-search-url`_ | |
23 | * `dj-config-url`_ | |
24 | * `django-cache-url`_ | |
25 | ||
26 | and inspired by: | |
27 | ||
28 | * `12factor`_ | |
29 | * `12factor-django`_ | |
30 | * `Two Scoops of Django`_ | |
31 | ||
32 | This is your `settings.py` file before you have installed **django-environ** | |
33 | ||
34 | .. code-block:: python | |
35 | ||
36 | import os | |
37 | SITE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) | |
38 | ||
39 | DEBUG = True | |
40 | TEMPLATE_DEBUG = DEBUG | |
41 | ||
42 | DATABASES = { | |
43 | 'default': { | |
44 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', | |
45 | 'NAME': 'database', | |
46 | 'USER': 'user', | |
47 | 'PASSWORD': 'githubbedpassword', | |
48 | 'HOST': '127.0.0.1', | |
49 | 'PORT': '8458', | |
50 | }, | |
51 | 'extra': { | |
52 | 'ENGINE': 'django.db.backends.sqlite3', | |
53 | 'NAME': os.path.join(SITE_ROOT, 'database.sqlite') | |
54 | } | |
55 | } | |
56 | ||
57 | MEDIA_ROOT = os.path.join(SITE_ROOT, 'assets') | |
58 | MEDIA_URL = 'media/' | |
59 | STATIC_ROOT = os.path.join(SITE_ROOT, 'static') | |
60 | STATIC_URL = 'static/' | |
61 | ||
62 | SECRET_KEY = '...im incredibly still here...' | |
63 | ||
64 | CACHES = { | |
65 | 'default': { | |
66 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', | |
67 | 'LOCATION': [ | |
68 | '127.0.0.1:11211', '127.0.0.1:11212', '127.0.0.1:11213', | |
69 | ] | |
70 | }, | |
71 | 'redis': { | |
72 | 'BACKEND': 'django_redis.cache.RedisCache', | |
73 | 'LOCATION': '127.0.0.1:6379/1', | |
74 | 'OPTIONS': { | |
75 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
76 | 'PASSWORD': 'redis-githubbed-password', | |
77 | } | |
78 | } | |
79 | } | |
80 | ||
81 | After: | |
82 | ||
83 | .. code-block:: python | |
84 | ||
85 | import environ | |
86 | root = environ.Path(__file__) - 3 # three folder back (/a/b/c/ - 3 = /) | |
87 | env = environ.Env(DEBUG=(bool, False),) # set default values and casting | |
88 | environ.Env.read_env() # reading .env file | |
89 | ||
90 | SITE_ROOT = root() | |
91 | ||
92 | DEBUG = env('DEBUG') # False if not in os.environ | |
93 | TEMPLATE_DEBUG = DEBUG | |
94 | ||
95 | DATABASES = { | |
96 | 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ | |
97 | 'extra': env.db('SQLITE_URL', default='sqlite:////tmp/my-tmp-sqlite.db') | |
98 | } | |
99 | ||
100 | public_root = root.path('public/') | |
101 | ||
102 | MEDIA_ROOT = public_root('media') | |
103 | MEDIA_URL = 'media/' | |
104 | STATIC_ROOT = public_root('static') | |
105 | STATIC_URL = 'static/' | |
106 | ||
107 | SECRET_KEY = env('SECRET_KEY') # Raises ImproperlyConfigured exception if SECRET_KEY not in os.environ | |
108 | ||
109 | CACHES = { | |
110 | 'default': env.cache(), | |
111 | 'redis': env.cache('REDIS_URL') | |
112 | } | |
113 | ||
114 | You can also pass ``read_env()`` an explicit path to the ``.env`` file. | |
115 | ||
116 | Create a ``.env`` file: | |
117 | ||
118 | .. code-block:: bash | |
119 | ||
120 | DEBUG=on | |
121 | # DJANGO_SETTINGS_MODULE=myapp.settings.dev | |
122 | SECRET_KEY=your-secret-key | |
123 | DATABASE_URL=psql://urser:un-githubbedpassword@127.0.0.1:8458/database | |
124 | # SQLITE_URL=sqlite:///my-local-sqlite.db | |
125 | CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213 | |
126 | REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=redis-un-githubbed-password | |
127 | ||
128 | ||
129 | How to install | |
130 | ============== | |
131 | ||
132 | :: | |
133 | ||
134 | $ pip install django-environ | |
135 | ||
136 | ||
137 | How to use | |
138 | ========== | |
139 | ||
140 | There are only two classes, ``environ.Env`` and ``environ.Path`` | |
141 | ||
142 | .. code-block:: python | |
143 | ||
144 | >>> import environ | |
145 | >>> env = environ.Env( | |
146 | DEBUG=(bool, False), | |
147 | ) | |
148 | >>> env('DEBUG') | |
149 | False | |
150 | >>> env('DEBUG', default=True) | |
151 | True | |
152 | ||
153 | >>> open('.myenv', 'a').write('DEBUG=on') | |
154 | >>> environ.Env.read_env('.myenv') # or env.read_env('.myenv') | |
155 | >>> env('DEBUG') | |
156 | True | |
157 | ||
158 | >>> open('.myenv', 'a').write('\nINT_VAR=1010') | |
159 | >>> env.int('INT_VAR'), env.str('INT_VAR') | |
160 | 1010, '1010' | |
161 | ||
162 | >>> open('.myenv', 'a').write('\nDATABASE_URL=sqlite:///my-local-sqlite.db') | |
163 | >>> env.read_env('.myenv') | |
164 | >>> env.db() | |
165 | {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'my-local-sqlite.db', 'HOST': '', 'USER': '', 'PASSWORD': '', 'PORT': ''} | |
166 | ||
167 | >>> root = env.path('/home/myproject/') | |
168 | >>> root('static') | |
169 | '/home/myproject/static' | |
170 | ||
171 | ||
172 | See `cookiecutter-django`_ for a concrete example on using with a django project. | |
173 | ||
174 | ||
175 | Supported Types | |
176 | =============== | |
177 | ||
178 | - str | |
179 | - bool | |
180 | - int | |
181 | - float | |
182 | - json | |
183 | - list (FOO=a,b,c) | |
184 | - tuple (FOO=(a,b,c)) | |
185 | - dict (BAR=key=val,foo=bar) #environ.Env(BAR=(dict, {})) | |
186 | - dict (BAR=key=val;foo=1.1;baz=True) #environ.Env(BAR=(dict(value=unicode, cast=dict(foo=float,baz=bool)), {})) | |
187 | - url | |
188 | - path (environ.Path) | |
189 | - db_url | |
190 | - PostgreSQL: postgres://, pgsql://, psql:// or postgresql:// | |
191 | - PostGIS: postgis:// | |
192 | - MySQL: mysql:// or mysql2:// | |
193 | - MySQL for GeoDjango: mysqlgis:// | |
194 | - SQLITE: sqlite:// | |
195 | - SQLITE with SPATIALITE for GeoDjango: spatialite:// | |
196 | - Oracle: oracle:// | |
197 | - PyODBC: pyodbc:// | |
198 | - Redshift: redshift:// | |
199 | - LDAP: ldap:// | |
200 | - cache_url | |
201 | - Database: dbcache:// | |
202 | - Dummy: dummycache:// | |
203 | - File: filecache:// | |
204 | - Memory: locmemcache:// | |
205 | - Memcached: memcache:// | |
206 | - Python memory: pymemcache:// | |
207 | - Redis: rediscache:// | |
208 | - search_url | |
209 | - ElasticSearch: elasticsearch:// | |
210 | - Solr: solr:// | |
211 | - Whoosh: whoosh:// | |
212 | - Xapian: xapian:// | |
213 | - Simple cache: simple:// | |
214 | - email_url | |
215 | - SMTP: smtp:// | |
216 | - SMTP+SSL: smtp+ssl:// | |
217 | - SMTP+TLS: smtp+tls:// | |
218 | - Console mail: consolemail:// | |
219 | - File mail: filemail:// | |
220 | - LocMem mail: memorymail:// | |
221 | - Dummy mail: dummymail:// | |
222 | ||
223 | Tips | |
224 | ==== | |
225 | ||
226 | Using unsafe characters in URLs | |
227 | ------------------------------- | |
228 | ||
229 | In order to use unsafe characters you have to encode with ``urllib.parse.encode`` before you set into ``.env`` file. | |
230 | ||
231 | .. code-block:: | |
232 | ||
233 | DATABASE_URL=mysql://user:%23password@127.0.0.1:3306/dbname | |
234 | ||
235 | ||
236 | See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for reference. | |
237 | ||
238 | Multiple redis cache locations | |
239 | ------------------------------ | |
240 | ||
241 | For redis cache, `multiple master/slave or shard locations <http://niwinz.github.io/django-redis/latest/#_pluggable_clients>`_ can be configured as follows: | |
242 | ||
243 | .. code-block:: | |
244 | ||
245 | CACHE_URL='rediscache://master:6379,slave1:6379,slave2:6379/1' | |
246 | ||
247 | Email settings | |
248 | -------------- | |
249 | ||
250 | In order to set email configuration for django you can use this code: | |
251 | ||
252 | .. code-block:: python | |
253 | ||
254 | EMAIL_CONFIG = env.email_url( | |
255 | 'EMAIL_URL', default='smtp://user@:password@localhost:25') | |
256 | ||
257 | vars().update(EMAIL_CONFIG) | |
258 | ||
259 | ||
260 | SQLite urls | |
261 | ----------- | |
262 | ||
263 | SQLite connects to file based databases. The same URL format is used, omitting the hostname, | |
264 | and using the "file" portion as the filename of the database. | |
265 | This has the effect of four slashes being present for an absolute | |
266 | file path: sqlite:////full/path/to/your/database/file.sqlite. | |
267 | ||
268 | ||
269 | Tests | |
270 | ===== | |
271 | ||
272 | :: | |
273 | ||
274 | $ git clone git@github.com:joke2k/django-environ.git | |
275 | $ cd django-environ/ | |
276 | $ python setup.py test | |
277 | ||
278 | ||
279 | License | |
280 | ======= | |
281 | ||
282 | Django-environ is licensed under the MIT License - see the `LICENSE_FILE`_ file for details | |
283 | ||
284 | Changelog | |
285 | ========= | |
286 | ||
287 | ||
288 | `0.4.4 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.3...v0.4.4>`__ | |
289 | --------------------------------------------------------------------------------------------- | |
290 | ||
291 | - Support for django-redis multiple locations (master/slave, shards) | |
292 | - Support for Elasticsearch2 | |
293 | - Support for Mysql-connector | |
294 | - Support for pyodbc | |
295 | - Add __contains__ feature to Environ class | |
296 | - Fix Path subtracting | |
297 | ||
298 | ||
299 | `0.4.3 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.2...v0.4.3>`__ | |
300 | --------------------------------------------------------------------------------------------- | |
301 | ||
302 | - Rollback the default Environ to os.environ | |
303 | ||
304 | `0.4.2 - 13-April-2017 <https://github.com/joke2k/django-environ/compare/v0.4.1...v0.4.2>`__ | |
305 | -------------------------------------------------------------------------------------------- | |
306 | ||
307 | - Confirm support for Django 1.11. | |
308 | - Support for Redshift database URL | |
309 | - Fix uwsgi settings reload problem (#55) | |
310 | - Update support for django-redis urls (#109) | |
311 | ||
312 | `0.4.1 - 13-November-2016 <https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1>`__ | |
313 | --------------------------------------------------------------------------------------------- | |
314 | - Fix for unsafe characters into URLs | |
315 | - Clarifying warning on missing or unreadable file. Thanks to @nickcatal | |
316 | - Add support for Django 1.10. | |
317 | - Fix support for Oracle urls | |
318 | - Fix support for django-redis | |
319 | ||
320 | ||
321 | `0.4.0 - 23-September-2015 <https://github.com/joke2k/django-environ/compare/v0.3...v0.4>`__ | |
322 | -------------------------------------------------------------------------------------------- | |
323 | - Fix non-ascii values (broken in Python 2.x) | |
324 | - New email schemes - smtp+ssl and smtp+tls (smtps would be deprecated) | |
325 | - redis_cache replaced by django_redis | |
326 | - Add tuple support. Thanks to @anonymouzz | |
327 | - Add LDAP url support for database (django-ldapdb) | |
328 | - Fix psql/pgsql url | |
329 | ||
330 | `0.3 - 03-June-2014 <https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3>`__ | |
331 | --------------------------------------------------------------------------------------- | |
332 | - Add cache url support | |
333 | - Add email url support | |
334 | - Add search url support | |
335 | - Rewriting README.rst | |
336 | ||
337 | 0.2.1 19-April-2013 | |
338 | ------------------- | |
339 | - environ/environ.py: Env.__call__ now uses Env.get_value instance method | |
340 | ||
341 | 0.2 16-April-2013 | |
342 | ----------------- | |
343 | - environ/environ.py, environ/test.py, environ/test_env.txt: add advanced | |
344 | float parsing (comma and dot symbols to separate thousands and decimals) | |
345 | - README.rst, docs/index.rst: fix TYPO in documentation | |
346 | ||
347 | 0.1 02-April-2013 | |
348 | ----------------- | |
349 | - initial release | |
350 | ||
351 | Credits | |
352 | ======= | |
353 | ||
354 | - `12factor`_ | |
355 | - `12factor-django`_ | |
356 | - `Two Scoops of Django`_ | |
357 | - `rconradharris`_ / `envparse`_ | |
358 | - `kennethreitz`_ / `dj-database-url`_ | |
359 | - `migonzalvar`_ / `dj-email-url`_ | |
360 | - `ghickman`_ / `django-cache-url`_ | |
361 | - `dstufft`_ / `dj-search-url`_ | |
362 | - `julianwachholz`_ / `dj-config-url`_ | |
363 | - `nickstenning`_ / `honcho`_ | |
364 | - `envparse`_ | |
365 | - `Distribute`_ | |
366 | - `modern-package-template`_ | |
367 | ||
368 | .. _rconradharris: https://github.com/rconradharris | |
369 | .. _envparse: https://github.com/rconradharris/envparse | |
370 | ||
371 | .. _kennethreitz: https://github.com/kennethreitz | |
372 | .. _dj-database-url: https://github.com/kennethreitz/dj-database-url | |
373 | ||
374 | .. _migonzalvar: https://github.com/migonzalvar | |
375 | .. _dj-email-url: https://github.com/migonzalvar/dj-email-url | |
376 | ||
377 | .. _ghickman: https://github.com/ghickman | |
378 | .. _django-cache-url: https://github.com/ghickman/django-cache-url | |
379 | ||
380 | .. _julianwachholz: https://github.com/julianwachholz | |
381 | .. _dj-config-url: https://github.com/julianwachholz/dj-config-url | |
382 | ||
383 | .. _dstufft: https://github.com/dstufft | |
384 | .. _dj-search-url: https://github.com/dstufft/dj-search-url | |
385 | ||
386 | .. _nickstenning: https://github.com/nickstenning | |
387 | .. _honcho: https://github.com/nickstenning/honcho | |
388 | ||
389 | .. _12factor: http://www.12factor.net/ | |
390 | .. _12factor-django: http://www.wellfireinteractive.com/blog/easier-12-factor-django/ | |
391 | .. _`Two Scoops of Django`: http://twoscoopspress.org/ | |
392 | ||
393 | .. _Distribute: http://pypi.python.org/pypi/distribute | |
394 | .. _`modern-package-template`: http://pypi.python.org/pypi/modern-package-template | |
395 | ||
396 | .. _cookiecutter-django: https://github.com/pydanny/cookiecutter-django | |
397 | ||
398 | .. |pypi| image:: https://img.shields.io/pypi/v/django-environ.svg?style=flat-square&label=version | |
399 | :target: https://pypi.python.org/pypi/django-environ | |
400 | :alt: Latest version released on PyPi | |
401 | ||
402 | .. |coverage| image:: https://img.shields.io/coveralls/joke2k/django-environ/master.svg?style=flat-square | |
403 | :target: https://coveralls.io/r/joke2k/django-environ?branch=master | |
404 | :alt: Test coverage | |
405 | ||
406 | .. |unix_build| image:: https://img.shields.io/travis/joke2k/django-environ/master.svg?style=flat-square&label=unix%20build | |
407 | :target: http://travis-ci.org/joke2k/django-environ | |
408 | :alt: Build status of the master branch on Mac/Linux | |
409 | ||
410 | .. |windows_build| image:: https://img.shields.io/appveyor/ci/joke2k/django-environ.svg?style=flat-square&label=windows%20build | |
411 | :target: https://ci.appveyor.com/project/joke2k/django-environ | |
412 | :alt: Build status of the master branch on Windows | |
413 | ||
414 | .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square | |
415 | :target: https://raw.githubusercontent.com/joke2k/django-environ/master/LICENSE.txt | |
416 | :alt: Package license | |
417 | ||
418 | .. _LICENSE_FILE: https://github.com/joke2k/django-environ/blob/master/LICENSE.txt | |
419 | ||
420 | Keywords: django environment variables 12factor | |
2 | Version: 0.9.0 | |
3 | Summary: A package that allows you to utilize 12factor inspired environment variables to configure your Django application. | |
4 | Home-page: https://django-environ.readthedocs.org | |
5 | Author: Daniele Faraglia | |
6 | Author-email: daniele.faraglia@gmail.com | |
7 | Maintainer: Serghei Iakovlev | |
8 | Maintainer-email: egrep@protonmail.ch | |
9 | License: MIT | |
10 | Project-URL: Documentation, https://django-environ.readthedocs.org | |
11 | Project-URL: Funding, https://opencollective.com/django-environ | |
12 | Project-URL: Say Thanks!, https://saythanks.io/to/joke2k | |
13 | Project-URL: Changelog, https://django-environ.readthedocs.org/en/latest/changelog.html | |
14 | Project-URL: Bug Tracker, https://github.com/joke2k/django-environ/issues | |
15 | Project-URL: Source Code, https://github.com/joke2k/django-environ | |
16 | Keywords: environment,django,variables,12factor | |
421 | 17 | Platform: any |
422 | Classifier: Development Status :: 3 - Alpha | |
423 | Classifier: Intended Audience :: Information Technology | |
18 | Classifier: Development Status :: 5 - Production/Stable | |
424 | 19 | Classifier: Framework :: Django |
425 | Classifier: Framework :: Django :: 1.8 | |
426 | Classifier: Framework :: Django :: 1.9 | |
427 | Classifier: Framework :: Django :: 1.10 | |
428 | 20 | Classifier: Framework :: Django :: 1.11 |
21 | Classifier: Framework :: Django :: 2.0 | |
22 | Classifier: Framework :: Django :: 2.1 | |
23 | Classifier: Framework :: Django :: 2.2 | |
24 | Classifier: Framework :: Django :: 3.0 | |
25 | Classifier: Framework :: Django :: 3.1 | |
26 | Classifier: Framework :: Django :: 3.2 | |
27 | Classifier: Framework :: Django :: 4.0 | |
28 | Classifier: Operating System :: OS Independent | |
29 | Classifier: Intended Audience :: Developers | |
30 | Classifier: Natural Language :: English | |
429 | 31 | Classifier: Programming Language :: Python |
430 | Classifier: Programming Language :: Python :: 2 | |
431 | Classifier: Programming Language :: Python :: 2.7 | |
432 | 32 | Classifier: Programming Language :: Python :: 3 |
433 | 33 | Classifier: Programming Language :: Python :: 3.4 |
434 | 34 | Classifier: Programming Language :: Python :: 3.5 |
35 | Classifier: Programming Language :: Python :: 3.6 | |
36 | Classifier: Programming Language :: Python :: 3.7 | |
37 | Classifier: Programming Language :: Python :: 3.8 | |
38 | Classifier: Programming Language :: Python :: 3.9 | |
39 | Classifier: Programming Language :: Python :: 3.10 | |
435 | 40 | Classifier: Programming Language :: Python :: Implementation :: CPython |
436 | 41 | Classifier: Programming Language :: Python :: Implementation :: PyPy |
437 | 42 | Classifier: Topic :: Software Development :: Libraries :: Python Modules |
438 | 43 | Classifier: Topic :: Utilities |
439 | 44 | Classifier: License :: OSI Approved :: MIT License |
440 | Classifier: Framework :: Django | |
45 | Requires-Python: >=3.4,<4 | |
46 | Description-Content-Type: text/x-rst | |
47 | Provides-Extra: develop | |
48 | Provides-Extra: docs | |
49 | Provides-Extra: testing | |
50 | License-File: LICENSE.txt | |
51 | License-File: AUTHORS.rst | |
52 | ||
53 | ============== | |
54 | django-environ | |
55 | ============== | |
56 | ||
57 | ||
58 | ``django-environ`` is the Python package that allows you to use | |
59 | `Twelve-factor methodology <https://www.12factor.net/>`_ to configure your | |
60 | Django application with environment variables. | |
61 | ||
62 | .. -teaser-end- | |
63 | ||
64 | For that, it gives you an easy way to configure Django application using | |
65 | environment variables obtained from an environment file and provided by the OS: | |
66 | ||
67 | .. -code-begin- | |
68 | ||
69 | .. code-block:: python | |
70 | ||
71 | import environ | |
72 | import os | |
73 | ||
74 | env = environ.Env( | |
75 | # set casting, default value | |
76 | DEBUG=(bool, False) | |
77 | ) | |
78 | ||
79 | # Set the project base directory | |
80 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
81 | ||
82 | # Take environment variables from .env file | |
83 | environ.Env.read_env(os.path.join(BASE_DIR, '.env')) | |
84 | ||
85 | # False if not in os.environ because of casting above | |
86 | DEBUG = env('DEBUG') | |
87 | ||
88 | # Raises Django's ImproperlyConfigured | |
89 | # exception if SECRET_KEY not in os.environ | |
90 | SECRET_KEY = env('SECRET_KEY') | |
91 | ||
92 | # Parse database connection url strings | |
93 | # like psql://user:pass@127.0.0.1:8458/db | |
94 | DATABASES = { | |
95 | # read os.environ['DATABASE_URL'] and raises | |
96 | # ImproperlyConfigured exception if not found | |
97 | # | |
98 | # The db() method is an alias for db_url(). | |
99 | 'default': env.db(), | |
100 | ||
101 | # read os.environ['SQLITE_URL'] | |
102 | 'extra': env.db_url( | |
103 | 'SQLITE_URL', | |
104 | default='sqlite:////tmp/my-tmp-sqlite.db' | |
105 | ) | |
106 | } | |
107 | ||
108 | CACHES = { | |
109 | # Read os.environ['CACHE_URL'] and raises | |
110 | # ImproperlyConfigured exception if not found. | |
111 | # | |
112 | # The cache() method is an alias for cache_url(). | |
113 | 'default': env.cache(), | |
114 | ||
115 | # read os.environ['REDIS_URL'] | |
116 | 'redis': env.cache_url('REDIS_URL') | |
117 | } | |
118 | ||
119 | .. -overview- | |
120 | ||
121 | The idea of this package is to unify a lot of packages that make the same stuff: | |
122 | Take a string from ``os.environ``, parse and cast it to some of useful python | |
123 | typed variables. To do that and to use the `12factor <https://www.12factor.net/>`_ | |
124 | approach, some connection strings are expressed as url, so this package can parse | |
125 | it and return a ``urllib.parse.ParseResult``. These strings from ``os.environ`` | |
126 | are loaded from a ``.env`` file and filled in ``os.environ`` with ``setdefault`` | |
127 | method, to avoid to overwrite the real environ. | |
128 | A similar approach is used in `Two Scoops of Django <https://www.feldroy.com/books/two-scoops-of-django-3-x>`_ | |
129 | book and explained in `12factor-django <https://wellfire.co/learn/easier-12-factor-django>`_ | |
130 | article. | |
131 | ||
132 | ||
133 | Using ``django-environ`` you can stop to make a lot of unversioned | |
134 | ``settings_*.py`` to configure your app. | |
135 | See `cookiecutter-django <https://github.com/cookiecutter/cookiecutter-django>`_ | |
136 | for a concrete example on using with a django project. | |
137 | ||
138 | **Feature Support** | |
139 | ||
140 | - Fast and easy multi environment for deploy | |
141 | - Fill ``os.environ`` with .env file variables | |
142 | - Variables casting | |
143 | - Url variables exploded to django specific package settings | |
144 | - Optional support for Docker-style file based config variables (use | |
145 | ``environ.FileAwareEnv`` instead of ``environ.Env``) | |
146 | ||
147 | .. -project-information- | |
148 | ||
149 | Project Information | |
150 | =================== | |
151 | ||
152 | ``django-environ`` is released under the `MIT / X11 License <https://choosealicense.com/licenses/mit/>`__, | |
153 | its documentation lives at `Read the Docs <https://django-environ.readthedocs.io/en/latest/>`_, | |
154 | the code on `GitHub <https://github.com/joke2k/django-environ>`_, | |
155 | and the latest release on `PyPI <https://pypi.org/project/django-environ/>`_. | |
156 | ||
157 | It’s rigorously tested on Python 3.5+, and officially supports | |
158 | Django 1.11, 2.2, 3.0, 3.1, 3.2 and 4.0. | |
159 | ||
160 | If you'd like to contribute to ``django-environ`` you're most welcome! | |
161 | ||
162 | .. -support- | |
163 | ||
164 | Support | |
165 | ======= | |
166 | ||
167 | Should you have any question, any remark, or if you find a bug, or if there is | |
168 | something you can't do with the ``django-environ``, please | |
169 | `open an issue <https://github.com/joke2k/django-environ>`_. | |
170 | ||
171 | ||
172 | Contributing | |
173 | ============ | |
174 | ||
175 | If you would like to contribute to ``django-environ``, please take a look at the | |
176 | `current issues <https://github.com/joke2k/django-environ/issues>`_. If there is | |
177 | a bug or feature that you want but it isn't listed, make an issue and work on it. | |
178 | ||
179 | How to Contribute | |
180 | ----------------- | |
181 | ||
182 | 1. Check for open issues or open a fresh issue to start a discussion around a | |
183 | feature idea or a bug. | |
184 | 2. Fork `the repository <https://github.com/joke2k/django-environ>`_ on GitHub | |
185 | to start making your changes to the **develop** branch (or branch off of it). | |
186 | 3. Write a test which shows that the bug was fixed or that the feature works as | |
187 | expected. | |
188 | 4. Send a pull request and bug the maintainer until it gets merged and published. | |
189 | ||
190 | ||
191 | Release Information | |
192 | =================== | |
193 | ||
194 | v0.9.0 - 15-June-2022 | |
195 | ------------------------------ | |
196 | Added | |
197 | +++++ | |
198 | - Added support for Postgresql cluster URI | |
199 | `#355 <https://github.com/joke2k/django-environ/pull/355>`_. | |
200 | - Added support for Django 4.0 | |
201 | `#371 <https://github.com/joke2k/django-environ/issues/371>`_. | |
202 | - Added support for prefixed variables | |
203 | `#362 <https://github.com/joke2k/django-environ/issues/362>`_. | |
204 | - Amended documentation. | |
205 | ||
206 | ||
207 | Deprecated | |
208 | ++++++++++ | |
209 | - ``Env.unicode()`` is deprecated and will be removed in the next | |
210 | major release. Use ``Env.str()`` instead. | |
211 | ||
212 | ||
213 | Changed | |
214 | +++++++ | |
215 | - Attach cause to ``ImproperlyConfigured`` exception | |
216 | `#360 <https://github.com/joke2k/django-environ/issues/360>`_. | |
217 | ||
218 | ||
219 | Fixed | |
220 | +++++ | |
221 | - Fixed ``_cast_urlstr`` unquoting | |
222 | `#357 <https://github.com/joke2k/django-environ/issues/357>`_. | |
223 | - Fixed documentation regarding unsafe characters in URLs | |
224 | `#220 <https://github.com/joke2k/django-environ/issues/220>`_. | |
225 | - Fixed ``environ.Path.__eq__()`` to compare paths correctly | |
226 | `#86 <https://github.com/joke2k/django-environ/issues/86>`_, | |
227 | `#197 <https://github.com/joke2k/django-environ/issues/197>`_. | |
228 | ||
229 | `Full changelog <https://django-environ.readthedocs.org/en/latest/changelog.html>`_. | |
230 | ||
231 | Security Policy | |
232 | =============== | |
233 | ||
234 | ||
235 | Reporting a Vulnerability | |
236 | ------------------------- | |
237 | ||
238 | If you discover a security vulnerability within ``django-environ``, please | |
239 | send an e-mail to Serghei Iakovlev via egrep@protonmail.ch. All security | |
240 | vulnerabilities will be promptly addressed. | |
241 | ||
242 | ||
243 | Credits | |
244 | ======= | |
245 | ||
246 | ``django-environ`` was initially created by `Daniele Faraglia <https://github.com/joke2k>`_ | |
247 | and currently maintained by `Serghei Iakovlev <https://github.com/sergeyklay/>`_. | |
248 | ||
249 | A full list of contributors can be found in `GitHub <https://github.com/joke2k/django-environ/graphs/contributors>`__. | |
250 | ||
251 | Acknowledgments | |
252 | =============== | |
253 | ||
254 | The existence of ``django-environ`` would have been impossible without these | |
255 | projects: | |
256 | ||
257 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ | |
258 | - `jazzband/dj-database-url <https://github.com/jazzband/dj-database-url>`_ | |
259 | - `migonzalvar/dj-email-url <https://github.com/migonzalvar/dj-email-url>`_ | |
260 | - `ghickman/django-cache-url <https://github.com/ghickman/django-cache-url>`_ | |
261 | - `dstufft/dj-search-url <https://github.com/dstufft/dj-search-url>`_ | |
262 | - `julianwachholz/dj-config-url <https://github.com/julianwachholz/dj-config-url>`_ | |
263 | - `nickstenning/honcho <https://github.com/nickstenning/honcho>`_ | |
264 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ |
0 | ============== | |
1 | Django-environ | |
2 | ============== | |
0 | .. raw:: html | |
3 | 1 | |
4 | Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application. | |
2 | <h1 align="center">django-environ</h1> | |
3 | <p align="center"> | |
4 | <a href="https://pypi.python.org/pypi/django-environ"> | |
5 | <img src="https://img.shields.io/pypi/v/django-environ.svg" alt="Latest version released on PyPi" /> | |
6 | </a> | |
7 | <a href="https://coveralls.io/github/joke2k/django-environ"> | |
8 | <img src="https://coveralls.io/repos/github/joke2k/django-environ/badge.svg" alt="Coverage Status" /> | |
9 | </a> | |
10 | <a href="https://github.com/joke2k/django-environ/actions?workflow=CI"> | |
11 | <img src="https://github.com/joke2k/django-environ/workflows/CI/badge.svg?branch=develop" alt="CI Status" /> | |
12 | </a> | |
13 | <a href="https://opencollective.com/django-environ"> | |
14 | <img src="https://opencollective.com/django-environ/sponsors/badge.svg" alt="Sponsors on Open Collective" /> | |
15 | </a> | |
16 | <a href="https://opencollective.com/django-environ"> | |
17 | <img src="https://opencollective.com/django-environ/backers/badge.svg" alt="Backers on Open Collective" /> | |
18 | </a> | |
19 | <a href="https://saythanks.io/to/joke2k"> | |
20 | <img src="https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg" alt="Say Thanks!" /> | |
21 | </a> | |
22 | <a href="https://raw.githubusercontent.com/joke2k/django-environ/main/LICENSE.txt"> | |
23 | <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Package license" /> | |
24 | </a> | |
25 | </p> | |
5 | 26 | |
6 | |pypi| |unix_build| |windows_build| |coverage| |license| | |
27 | .. -teaser-begin- | |
7 | 28 | |
29 | ``django-environ`` is the Python package that allows you to use | |
30 | `Twelve-factor methodology <https://www.12factor.net/>`_ to configure your | |
31 | Django application with environment variables. | |
8 | 32 | |
9 | This module is a merge of: | |
33 | .. -teaser-end- | |
10 | 34 | |
11 | * `envparse`_ | |
12 | * `honcho`_ | |
13 | * `dj-database-url`_ | |
14 | * `dj-search-url`_ | |
15 | * `dj-config-url`_ | |
16 | * `django-cache-url`_ | |
35 | For that, it gives you an easy way to configure Django application using | |
36 | environment variables obtained from an environment file and provided by the OS: | |
17 | 37 | |
18 | and inspired by: | |
19 | ||
20 | * `12factor`_ | |
21 | * `12factor-django`_ | |
22 | * `Two Scoops of Django`_ | |
23 | ||
24 | This is your `settings.py` file before you have installed **django-environ** | |
38 | .. -code-begin- | |
25 | 39 | |
26 | 40 | .. code-block:: python |
27 | 41 | |
28 | import os | |
29 | SITE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) | |
42 | import environ | |
43 | import os | |
30 | 44 | |
31 | DEBUG = True | |
32 | TEMPLATE_DEBUG = DEBUG | |
45 | env = environ.Env( | |
46 | # set casting, default value | |
47 | DEBUG=(bool, False) | |
48 | ) | |
33 | 49 | |
34 | DATABASES = { | |
35 | 'default': { | |
36 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', | |
37 | 'NAME': 'database', | |
38 | 'USER': 'user', | |
39 | 'PASSWORD': 'githubbedpassword', | |
40 | 'HOST': '127.0.0.1', | |
41 | 'PORT': '8458', | |
42 | }, | |
43 | 'extra': { | |
44 | 'ENGINE': 'django.db.backends.sqlite3', | |
45 | 'NAME': os.path.join(SITE_ROOT, 'database.sqlite') | |
46 | } | |
47 | } | |
50 | # Set the project base directory | |
51 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
48 | 52 | |
49 | MEDIA_ROOT = os.path.join(SITE_ROOT, 'assets') | |
50 | MEDIA_URL = 'media/' | |
51 | STATIC_ROOT = os.path.join(SITE_ROOT, 'static') | |
52 | STATIC_URL = 'static/' | |
53 | # Take environment variables from .env file | |
54 | environ.Env.read_env(os.path.join(BASE_DIR, '.env')) | |
53 | 55 | |
54 | SECRET_KEY = '...im incredibly still here...' | |
56 | # False if not in os.environ because of casting above | |
57 | DEBUG = env('DEBUG') | |
55 | 58 | |
56 | CACHES = { | |
57 | 'default': { | |
58 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', | |
59 | 'LOCATION': [ | |
60 | '127.0.0.1:11211', '127.0.0.1:11212', '127.0.0.1:11213', | |
61 | ] | |
62 | }, | |
63 | 'redis': { | |
64 | 'BACKEND': 'django_redis.cache.RedisCache', | |
65 | 'LOCATION': '127.0.0.1:6379/1', | |
66 | 'OPTIONS': { | |
67 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
68 | 'PASSWORD': 'redis-githubbed-password', | |
69 | } | |
70 | } | |
71 | } | |
59 | # Raises Django's ImproperlyConfigured | |
60 | # exception if SECRET_KEY not in os.environ | |
61 | SECRET_KEY = env('SECRET_KEY') | |
72 | 62 | |
73 | After: | |
63 | # Parse database connection url strings | |
64 | # like psql://user:pass@127.0.0.1:8458/db | |
65 | DATABASES = { | |
66 | # read os.environ['DATABASE_URL'] and raises | |
67 | # ImproperlyConfigured exception if not found | |
68 | # | |
69 | # The db() method is an alias for db_url(). | |
70 | 'default': env.db(), | |
74 | 71 | |
75 | .. code-block:: python | |
72 | # read os.environ['SQLITE_URL'] | |
73 | 'extra': env.db_url( | |
74 | 'SQLITE_URL', | |
75 | default='sqlite:////tmp/my-tmp-sqlite.db' | |
76 | ) | |
77 | } | |
76 | 78 | |
77 | import environ | |
78 | root = environ.Path(__file__) - 3 # three folder back (/a/b/c/ - 3 = /) | |
79 | env = environ.Env(DEBUG=(bool, False),) # set default values and casting | |
80 | environ.Env.read_env() # reading .env file | |
79 | CACHES = { | |
80 | # Read os.environ['CACHE_URL'] and raises | |
81 | # ImproperlyConfigured exception if not found. | |
82 | # | |
83 | # The cache() method is an alias for cache_url(). | |
84 | 'default': env.cache(), | |
81 | 85 | |
82 | SITE_ROOT = root() | |
86 | # read os.environ['REDIS_URL'] | |
87 | 'redis': env.cache_url('REDIS_URL') | |
88 | } | |
83 | 89 | |
84 | DEBUG = env('DEBUG') # False if not in os.environ | |
85 | TEMPLATE_DEBUG = DEBUG | |
90 | .. -overview- | |
86 | 91 | |
87 | DATABASES = { | |
88 | 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ | |
89 | 'extra': env.db('SQLITE_URL', default='sqlite:////tmp/my-tmp-sqlite.db') | |
90 | } | |
91 | ||
92 | public_root = root.path('public/') | |
93 | ||
94 | MEDIA_ROOT = public_root('media') | |
95 | MEDIA_URL = 'media/' | |
96 | STATIC_ROOT = public_root('static') | |
97 | STATIC_URL = 'static/' | |
98 | ||
99 | SECRET_KEY = env('SECRET_KEY') # Raises ImproperlyConfigured exception if SECRET_KEY not in os.environ | |
100 | ||
101 | CACHES = { | |
102 | 'default': env.cache(), | |
103 | 'redis': env.cache('REDIS_URL') | |
104 | } | |
105 | ||
106 | You can also pass ``read_env()`` an explicit path to the ``.env`` file. | |
107 | ||
108 | Create a ``.env`` file: | |
109 | ||
110 | .. code-block:: bash | |
111 | ||
112 | DEBUG=on | |
113 | # DJANGO_SETTINGS_MODULE=myapp.settings.dev | |
114 | SECRET_KEY=your-secret-key | |
115 | DATABASE_URL=psql://urser:un-githubbedpassword@127.0.0.1:8458/database | |
116 | # SQLITE_URL=sqlite:///my-local-sqlite.db | |
117 | CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213 | |
118 | REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=redis-un-githubbed-password | |
92 | The idea of this package is to unify a lot of packages that make the same stuff: | |
93 | Take a string from ``os.environ``, parse and cast it to some of useful python | |
94 | typed variables. To do that and to use the `12factor <https://www.12factor.net/>`_ | |
95 | approach, some connection strings are expressed as url, so this package can parse | |
96 | it and return a ``urllib.parse.ParseResult``. These strings from ``os.environ`` | |
97 | are loaded from a ``.env`` file and filled in ``os.environ`` with ``setdefault`` | |
98 | method, to avoid to overwrite the real environ. | |
99 | A similar approach is used in `Two Scoops of Django <https://www.feldroy.com/books/two-scoops-of-django-3-x>`_ | |
100 | book and explained in `12factor-django <https://wellfire.co/learn/easier-12-factor-django>`_ | |
101 | article. | |
119 | 102 | |
120 | 103 | |
121 | How to install | |
122 | ============== | |
104 | Using ``django-environ`` you can stop to make a lot of unversioned | |
105 | ``settings_*.py`` to configure your app. | |
106 | See `cookiecutter-django <https://github.com/cookiecutter/cookiecutter-django>`_ | |
107 | for a concrete example on using with a django project. | |
123 | 108 | |
124 | :: | |
109 | **Feature Support** | |
125 | 110 | |
126 | $ pip install django-environ | |
111 | - Fast and easy multi environment for deploy | |
112 | - Fill ``os.environ`` with .env file variables | |
113 | - Variables casting | |
114 | - Url variables exploded to django specific package settings | |
115 | - Optional support for Docker-style file based config variables (use | |
116 | ``environ.FileAwareEnv`` instead of ``environ.Env``) | |
127 | 117 | |
118 | .. -project-information- | |
128 | 119 | |
129 | How to use | |
130 | ========== | |
120 | Project Information | |
121 | =================== | |
131 | 122 | |
132 | There are only two classes, ``environ.Env`` and ``environ.Path`` | |
123 | ``django-environ`` is released under the `MIT / X11 License <https://choosealicense.com/licenses/mit/>`__, | |
124 | its documentation lives at `Read the Docs <https://django-environ.readthedocs.io/en/latest/>`_, | |
125 | the code on `GitHub <https://github.com/joke2k/django-environ>`_, | |
126 | and the latest release on `PyPI <https://pypi.org/project/django-environ/>`_. | |
133 | 127 | |
134 | .. code-block:: python | |
128 | It’s rigorously tested on Python 3.5+, and officially supports | |
129 | Django 1.11, 2.2, 3.0, 3.1, 3.2 and 4.0. | |
135 | 130 | |
136 | >>> import environ | |
137 | >>> env = environ.Env( | |
138 | DEBUG=(bool, False), | |
139 | ) | |
140 | >>> env('DEBUG') | |
141 | False | |
142 | >>> env('DEBUG', default=True) | |
143 | True | |
131 | If you'd like to contribute to ``django-environ`` you're most welcome! | |
144 | 132 | |
145 | >>> open('.myenv', 'a').write('DEBUG=on') | |
146 | >>> environ.Env.read_env('.myenv') # or env.read_env('.myenv') | |
147 | >>> env('DEBUG') | |
148 | True | |
133 | .. -support- | |
149 | 134 | |
150 | >>> open('.myenv', 'a').write('\nINT_VAR=1010') | |
151 | >>> env.int('INT_VAR'), env.str('INT_VAR') | |
152 | 1010, '1010' | |
153 | ||
154 | >>> open('.myenv', 'a').write('\nDATABASE_URL=sqlite:///my-local-sqlite.db') | |
155 | >>> env.read_env('.myenv') | |
156 | >>> env.db() | |
157 | {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'my-local-sqlite.db', 'HOST': '', 'USER': '', 'PASSWORD': '', 'PORT': ''} | |
158 | ||
159 | >>> root = env.path('/home/myproject/') | |
160 | >>> root('static') | |
161 | '/home/myproject/static' | |
162 | ||
163 | ||
164 | See `cookiecutter-django`_ for a concrete example on using with a django project. | |
165 | ||
166 | ||
167 | Supported Types | |
168 | =============== | |
169 | ||
170 | - str | |
171 | - bool | |
172 | - int | |
173 | - float | |
174 | - json | |
175 | - list (FOO=a,b,c) | |
176 | - tuple (FOO=(a,b,c)) | |
177 | - dict (BAR=key=val,foo=bar) #environ.Env(BAR=(dict, {})) | |
178 | - dict (BAR=key=val;foo=1.1;baz=True) #environ.Env(BAR=(dict(value=unicode, cast=dict(foo=float,baz=bool)), {})) | |
179 | - url | |
180 | - path (environ.Path) | |
181 | - db_url | |
182 | - PostgreSQL: postgres://, pgsql://, psql:// or postgresql:// | |
183 | - PostGIS: postgis:// | |
184 | - MySQL: mysql:// or mysql2:// | |
185 | - MySQL for GeoDjango: mysqlgis:// | |
186 | - SQLITE: sqlite:// | |
187 | - SQLITE with SPATIALITE for GeoDjango: spatialite:// | |
188 | - Oracle: oracle:// | |
189 | - PyODBC: pyodbc:// | |
190 | - Redshift: redshift:// | |
191 | - LDAP: ldap:// | |
192 | - cache_url | |
193 | - Database: dbcache:// | |
194 | - Dummy: dummycache:// | |
195 | - File: filecache:// | |
196 | - Memory: locmemcache:// | |
197 | - Memcached: memcache:// | |
198 | - Python memory: pymemcache:// | |
199 | - Redis: rediscache:// | |
200 | - search_url | |
201 | - ElasticSearch: elasticsearch:// | |
202 | - Solr: solr:// | |
203 | - Whoosh: whoosh:// | |
204 | - Xapian: xapian:// | |
205 | - Simple cache: simple:// | |
206 | - email_url | |
207 | - SMTP: smtp:// | |
208 | - SMTP+SSL: smtp+ssl:// | |
209 | - SMTP+TLS: smtp+tls:// | |
210 | - Console mail: consolemail:// | |
211 | - File mail: filemail:// | |
212 | - LocMem mail: memorymail:// | |
213 | - Dummy mail: dummymail:// | |
214 | ||
215 | Tips | |
216 | ==== | |
217 | ||
218 | Using unsafe characters in URLs | |
219 | ------------------------------- | |
220 | ||
221 | In order to use unsafe characters you have to encode with ``urllib.parse.encode`` before you set into ``.env`` file. | |
222 | ||
223 | .. code-block:: | |
224 | ||
225 | DATABASE_URL=mysql://user:%23password@127.0.0.1:3306/dbname | |
226 | ||
227 | ||
228 | See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for reference. | |
229 | ||
230 | Multiple redis cache locations | |
231 | ------------------------------ | |
232 | ||
233 | For redis cache, `multiple master/slave or shard locations <http://niwinz.github.io/django-redis/latest/#_pluggable_clients>`_ can be configured as follows: | |
234 | ||
235 | .. code-block:: | |
236 | ||
237 | CACHE_URL='rediscache://master:6379,slave1:6379,slave2:6379/1' | |
238 | ||
239 | Email settings | |
240 | -------------- | |
241 | ||
242 | In order to set email configuration for django you can use this code: | |
243 | ||
244 | .. code-block:: python | |
245 | ||
246 | EMAIL_CONFIG = env.email_url( | |
247 | 'EMAIL_URL', default='smtp://user@:password@localhost:25') | |
248 | ||
249 | vars().update(EMAIL_CONFIG) | |
250 | ||
251 | ||
252 | SQLite urls | |
253 | ----------- | |
254 | ||
255 | SQLite connects to file based databases. The same URL format is used, omitting the hostname, | |
256 | and using the "file" portion as the filename of the database. | |
257 | This has the effect of four slashes being present for an absolute | |
258 | file path: sqlite:////full/path/to/your/database/file.sqlite. | |
259 | ||
260 | ||
261 | Tests | |
262 | ===== | |
263 | ||
264 | :: | |
265 | ||
266 | $ git clone git@github.com:joke2k/django-environ.git | |
267 | $ cd django-environ/ | |
268 | $ python setup.py test | |
269 | ||
270 | ||
271 | License | |
135 | Support | |
272 | 136 | ======= |
273 | 137 | |
274 | Django-environ is licensed under the MIT License - see the `LICENSE_FILE`_ file for details | |
275 | ||
276 | Changelog | |
277 | ========= | |
278 | ||
279 | ||
280 | `0.4.4 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.3...v0.4.4>`__ | |
281 | --------------------------------------------------------------------------------------------- | |
282 | ||
283 | - Support for django-redis multiple locations (master/slave, shards) | |
284 | - Support for Elasticsearch2 | |
285 | - Support for Mysql-connector | |
286 | - Support for pyodbc | |
287 | - Add __contains__ feature to Environ class | |
288 | - Fix Path subtracting | |
289 | ||
290 | ||
291 | `0.4.3 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.2...v0.4.3>`__ | |
292 | --------------------------------------------------------------------------------------------- | |
293 | ||
294 | - Rollback the default Environ to os.environ | |
295 | ||
296 | `0.4.2 - 13-April-2017 <https://github.com/joke2k/django-environ/compare/v0.4.1...v0.4.2>`__ | |
297 | -------------------------------------------------------------------------------------------- | |
298 | ||
299 | - Confirm support for Django 1.11. | |
300 | - Support for Redshift database URL | |
301 | - Fix uwsgi settings reload problem (#55) | |
302 | - Update support for django-redis urls (#109) | |
303 | ||
304 | `0.4.1 - 13-November-2016 <https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1>`__ | |
305 | --------------------------------------------------------------------------------------------- | |
306 | - Fix for unsafe characters into URLs | |
307 | - Clarifying warning on missing or unreadable file. Thanks to @nickcatal | |
308 | - Add support for Django 1.10. | |
309 | - Fix support for Oracle urls | |
310 | - Fix support for django-redis | |
311 | ||
312 | ||
313 | `0.4.0 - 23-September-2015 <https://github.com/joke2k/django-environ/compare/v0.3...v0.4>`__ | |
314 | -------------------------------------------------------------------------------------------- | |
315 | - Fix non-ascii values (broken in Python 2.x) | |
316 | - New email schemes - smtp+ssl and smtp+tls (smtps would be deprecated) | |
317 | - redis_cache replaced by django_redis | |
318 | - Add tuple support. Thanks to @anonymouzz | |
319 | - Add LDAP url support for database (django-ldapdb) | |
320 | - Fix psql/pgsql url | |
321 | ||
322 | `0.3 - 03-June-2014 <https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3>`__ | |
323 | --------------------------------------------------------------------------------------- | |
324 | - Add cache url support | |
325 | - Add email url support | |
326 | - Add search url support | |
327 | - Rewriting README.rst | |
328 | ||
329 | 0.2.1 19-April-2013 | |
330 | ------------------- | |
331 | - environ/environ.py: Env.__call__ now uses Env.get_value instance method | |
332 | ||
333 | 0.2 16-April-2013 | |
334 | ----------------- | |
335 | - environ/environ.py, environ/test.py, environ/test_env.txt: add advanced | |
336 | float parsing (comma and dot symbols to separate thousands and decimals) | |
337 | - README.rst, docs/index.rst: fix TYPO in documentation | |
338 | ||
339 | 0.1 02-April-2013 | |
340 | ----------------- | |
341 | - initial release | |
342 | ||
343 | Credits | |
344 | ======= | |
345 | ||
346 | - `12factor`_ | |
347 | - `12factor-django`_ | |
348 | - `Two Scoops of Django`_ | |
349 | - `rconradharris`_ / `envparse`_ | |
350 | - `kennethreitz`_ / `dj-database-url`_ | |
351 | - `migonzalvar`_ / `dj-email-url`_ | |
352 | - `ghickman`_ / `django-cache-url`_ | |
353 | - `dstufft`_ / `dj-search-url`_ | |
354 | - `julianwachholz`_ / `dj-config-url`_ | |
355 | - `nickstenning`_ / `honcho`_ | |
356 | - `envparse`_ | |
357 | - `Distribute`_ | |
358 | - `modern-package-template`_ | |
359 | ||
360 | .. _rconradharris: https://github.com/rconradharris | |
361 | .. _envparse: https://github.com/rconradharris/envparse | |
362 | ||
363 | .. _kennethreitz: https://github.com/kennethreitz | |
364 | .. _dj-database-url: https://github.com/kennethreitz/dj-database-url | |
365 | ||
366 | .. _migonzalvar: https://github.com/migonzalvar | |
367 | .. _dj-email-url: https://github.com/migonzalvar/dj-email-url | |
368 | ||
369 | .. _ghickman: https://github.com/ghickman | |
370 | .. _django-cache-url: https://github.com/ghickman/django-cache-url | |
371 | ||
372 | .. _julianwachholz: https://github.com/julianwachholz | |
373 | .. _dj-config-url: https://github.com/julianwachholz/dj-config-url | |
374 | ||
375 | .. _dstufft: https://github.com/dstufft | |
376 | .. _dj-search-url: https://github.com/dstufft/dj-search-url | |
377 | ||
378 | .. _nickstenning: https://github.com/nickstenning | |
379 | .. _honcho: https://github.com/nickstenning/honcho | |
380 | ||
381 | .. _12factor: http://www.12factor.net/ | |
382 | .. _12factor-django: http://www.wellfireinteractive.com/blog/easier-12-factor-django/ | |
383 | .. _`Two Scoops of Django`: http://twoscoopspress.org/ | |
384 | ||
385 | .. _Distribute: http://pypi.python.org/pypi/distribute | |
386 | .. _`modern-package-template`: http://pypi.python.org/pypi/modern-package-template | |
387 | ||
388 | .. _cookiecutter-django: https://github.com/pydanny/cookiecutter-django | |
389 | ||
390 | .. |pypi| image:: https://img.shields.io/pypi/v/django-environ.svg?style=flat-square&label=version | |
391 | :target: https://pypi.python.org/pypi/django-environ | |
392 | :alt: Latest version released on PyPi | |
393 | ||
394 | .. |coverage| image:: https://img.shields.io/coveralls/joke2k/django-environ/master.svg?style=flat-square | |
395 | :target: https://coveralls.io/r/joke2k/django-environ?branch=master | |
396 | :alt: Test coverage | |
397 | ||
398 | .. |unix_build| image:: https://img.shields.io/travis/joke2k/django-environ/master.svg?style=flat-square&label=unix%20build | |
399 | :target: http://travis-ci.org/joke2k/django-environ | |
400 | :alt: Build status of the master branch on Mac/Linux | |
401 | ||
402 | .. |windows_build| image:: https://img.shields.io/appveyor/ci/joke2k/django-environ.svg?style=flat-square&label=windows%20build | |
403 | :target: https://ci.appveyor.com/project/joke2k/django-environ | |
404 | :alt: Build status of the master branch on Windows | |
405 | ||
406 | .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square | |
407 | :target: https://raw.githubusercontent.com/joke2k/django-environ/master/LICENSE.txt | |
408 | :alt: Package license | |
409 | ||
410 | .. _LICENSE_FILE: https://github.com/joke2k/django-environ/blob/master/LICENSE.txt | |
138 | Should you have any question, any remark, or if you find a bug, or if there is | |
139 | something you can't do with the ``django-environ``, please | |
140 | `open an issue <https://github.com/joke2k/django-environ>`_. |
0 | Security Policy | |
1 | =============== | |
2 | ||
3 | ||
4 | Reporting a Vulnerability | |
5 | ------------------------- | |
6 | ||
7 | If you discover a security vulnerability within ``django-environ``, please | |
8 | send an e-mail to Serghei Iakovlev via egrep@protonmail.ch. All security | |
9 | vulnerabilities will be promptly addressed. |
0 | Metadata-Version: 1.1 | |
0 | Metadata-Version: 2.1 | |
1 | 1 | Name: django-environ |
2 | Version: 0.4.4 | |
3 | Summary: Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application. | |
4 | Home-page: https://github.com/joke2k/django-environ | |
5 | Author: joke2k | |
6 | Author-email: joke2k@gmail.com | |
7 | License: MIT License | |
8 | Description: ============== | |
9 | Django-environ | |
10 | ============== | |
11 | ||
12 | Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application. | |
13 | ||
14 | |pypi| |unix_build| |windows_build| |coverage| |license| | |
15 | ||
16 | ||
17 | This module is a merge of: | |
18 | ||
19 | * `envparse`_ | |
20 | * `honcho`_ | |
21 | * `dj-database-url`_ | |
22 | * `dj-search-url`_ | |
23 | * `dj-config-url`_ | |
24 | * `django-cache-url`_ | |
25 | ||
26 | and inspired by: | |
27 | ||
28 | * `12factor`_ | |
29 | * `12factor-django`_ | |
30 | * `Two Scoops of Django`_ | |
31 | ||
32 | This is your `settings.py` file before you have installed **django-environ** | |
33 | ||
34 | .. code-block:: python | |
35 | ||
36 | import os | |
37 | SITE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) | |
38 | ||
39 | DEBUG = True | |
40 | TEMPLATE_DEBUG = DEBUG | |
41 | ||
42 | DATABASES = { | |
43 | 'default': { | |
44 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', | |
45 | 'NAME': 'database', | |
46 | 'USER': 'user', | |
47 | 'PASSWORD': 'githubbedpassword', | |
48 | 'HOST': '127.0.0.1', | |
49 | 'PORT': '8458', | |
50 | }, | |
51 | 'extra': { | |
52 | 'ENGINE': 'django.db.backends.sqlite3', | |
53 | 'NAME': os.path.join(SITE_ROOT, 'database.sqlite') | |
54 | } | |
55 | } | |
56 | ||
57 | MEDIA_ROOT = os.path.join(SITE_ROOT, 'assets') | |
58 | MEDIA_URL = 'media/' | |
59 | STATIC_ROOT = os.path.join(SITE_ROOT, 'static') | |
60 | STATIC_URL = 'static/' | |
61 | ||
62 | SECRET_KEY = '...im incredibly still here...' | |
63 | ||
64 | CACHES = { | |
65 | 'default': { | |
66 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', | |
67 | 'LOCATION': [ | |
68 | '127.0.0.1:11211', '127.0.0.1:11212', '127.0.0.1:11213', | |
69 | ] | |
70 | }, | |
71 | 'redis': { | |
72 | 'BACKEND': 'django_redis.cache.RedisCache', | |
73 | 'LOCATION': '127.0.0.1:6379/1', | |
74 | 'OPTIONS': { | |
75 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
76 | 'PASSWORD': 'redis-githubbed-password', | |
77 | } | |
78 | } | |
79 | } | |
80 | ||
81 | After: | |
82 | ||
83 | .. code-block:: python | |
84 | ||
85 | import environ | |
86 | root = environ.Path(__file__) - 3 # three folder back (/a/b/c/ - 3 = /) | |
87 | env = environ.Env(DEBUG=(bool, False),) # set default values and casting | |
88 | environ.Env.read_env() # reading .env file | |
89 | ||
90 | SITE_ROOT = root() | |
91 | ||
92 | DEBUG = env('DEBUG') # False if not in os.environ | |
93 | TEMPLATE_DEBUG = DEBUG | |
94 | ||
95 | DATABASES = { | |
96 | 'default': env.db(), # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ | |
97 | 'extra': env.db('SQLITE_URL', default='sqlite:////tmp/my-tmp-sqlite.db') | |
98 | } | |
99 | ||
100 | public_root = root.path('public/') | |
101 | ||
102 | MEDIA_ROOT = public_root('media') | |
103 | MEDIA_URL = 'media/' | |
104 | STATIC_ROOT = public_root('static') | |
105 | STATIC_URL = 'static/' | |
106 | ||
107 | SECRET_KEY = env('SECRET_KEY') # Raises ImproperlyConfigured exception if SECRET_KEY not in os.environ | |
108 | ||
109 | CACHES = { | |
110 | 'default': env.cache(), | |
111 | 'redis': env.cache('REDIS_URL') | |
112 | } | |
113 | ||
114 | You can also pass ``read_env()`` an explicit path to the ``.env`` file. | |
115 | ||
116 | Create a ``.env`` file: | |
117 | ||
118 | .. code-block:: bash | |
119 | ||
120 | DEBUG=on | |
121 | # DJANGO_SETTINGS_MODULE=myapp.settings.dev | |
122 | SECRET_KEY=your-secret-key | |
123 | DATABASE_URL=psql://urser:un-githubbedpassword@127.0.0.1:8458/database | |
124 | # SQLITE_URL=sqlite:///my-local-sqlite.db | |
125 | CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213 | |
126 | REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=redis-un-githubbed-password | |
127 | ||
128 | ||
129 | How to install | |
130 | ============== | |
131 | ||
132 | :: | |
133 | ||
134 | $ pip install django-environ | |
135 | ||
136 | ||
137 | How to use | |
138 | ========== | |
139 | ||
140 | There are only two classes, ``environ.Env`` and ``environ.Path`` | |
141 | ||
142 | .. code-block:: python | |
143 | ||
144 | >>> import environ | |
145 | >>> env = environ.Env( | |
146 | DEBUG=(bool, False), | |
147 | ) | |
148 | >>> env('DEBUG') | |
149 | False | |
150 | >>> env('DEBUG', default=True) | |
151 | True | |
152 | ||
153 | >>> open('.myenv', 'a').write('DEBUG=on') | |
154 | >>> environ.Env.read_env('.myenv') # or env.read_env('.myenv') | |
155 | >>> env('DEBUG') | |
156 | True | |
157 | ||
158 | >>> open('.myenv', 'a').write('\nINT_VAR=1010') | |
159 | >>> env.int('INT_VAR'), env.str('INT_VAR') | |
160 | 1010, '1010' | |
161 | ||
162 | >>> open('.myenv', 'a').write('\nDATABASE_URL=sqlite:///my-local-sqlite.db') | |
163 | >>> env.read_env('.myenv') | |
164 | >>> env.db() | |
165 | {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'my-local-sqlite.db', 'HOST': '', 'USER': '', 'PASSWORD': '', 'PORT': ''} | |
166 | ||
167 | >>> root = env.path('/home/myproject/') | |
168 | >>> root('static') | |
169 | '/home/myproject/static' | |
170 | ||
171 | ||
172 | See `cookiecutter-django`_ for a concrete example on using with a django project. | |
173 | ||
174 | ||
175 | Supported Types | |
176 | =============== | |
177 | ||
178 | - str | |
179 | - bool | |
180 | - int | |
181 | - float | |
182 | - json | |
183 | - list (FOO=a,b,c) | |
184 | - tuple (FOO=(a,b,c)) | |
185 | - dict (BAR=key=val,foo=bar) #environ.Env(BAR=(dict, {})) | |
186 | - dict (BAR=key=val;foo=1.1;baz=True) #environ.Env(BAR=(dict(value=unicode, cast=dict(foo=float,baz=bool)), {})) | |
187 | - url | |
188 | - path (environ.Path) | |
189 | - db_url | |
190 | - PostgreSQL: postgres://, pgsql://, psql:// or postgresql:// | |
191 | - PostGIS: postgis:// | |
192 | - MySQL: mysql:// or mysql2:// | |
193 | - MySQL for GeoDjango: mysqlgis:// | |
194 | - SQLITE: sqlite:// | |
195 | - SQLITE with SPATIALITE for GeoDjango: spatialite:// | |
196 | - Oracle: oracle:// | |
197 | - PyODBC: pyodbc:// | |
198 | - Redshift: redshift:// | |
199 | - LDAP: ldap:// | |
200 | - cache_url | |
201 | - Database: dbcache:// | |
202 | - Dummy: dummycache:// | |
203 | - File: filecache:// | |
204 | - Memory: locmemcache:// | |
205 | - Memcached: memcache:// | |
206 | - Python memory: pymemcache:// | |
207 | - Redis: rediscache:// | |
208 | - search_url | |
209 | - ElasticSearch: elasticsearch:// | |
210 | - Solr: solr:// | |
211 | - Whoosh: whoosh:// | |
212 | - Xapian: xapian:// | |
213 | - Simple cache: simple:// | |
214 | - email_url | |
215 | - SMTP: smtp:// | |
216 | - SMTP+SSL: smtp+ssl:// | |
217 | - SMTP+TLS: smtp+tls:// | |
218 | - Console mail: consolemail:// | |
219 | - File mail: filemail:// | |
220 | - LocMem mail: memorymail:// | |
221 | - Dummy mail: dummymail:// | |
222 | ||
223 | Tips | |
224 | ==== | |
225 | ||
226 | Using unsafe characters in URLs | |
227 | ------------------------------- | |
228 | ||
229 | In order to use unsafe characters you have to encode with ``urllib.parse.encode`` before you set into ``.env`` file. | |
230 | ||
231 | .. code-block:: | |
232 | ||
233 | DATABASE_URL=mysql://user:%23password@127.0.0.1:3306/dbname | |
234 | ||
235 | ||
236 | See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for reference. | |
237 | ||
238 | Multiple redis cache locations | |
239 | ------------------------------ | |
240 | ||
241 | For redis cache, `multiple master/slave or shard locations <http://niwinz.github.io/django-redis/latest/#_pluggable_clients>`_ can be configured as follows: | |
242 | ||
243 | .. code-block:: | |
244 | ||
245 | CACHE_URL='rediscache://master:6379,slave1:6379,slave2:6379/1' | |
246 | ||
247 | Email settings | |
248 | -------------- | |
249 | ||
250 | In order to set email configuration for django you can use this code: | |
251 | ||
252 | .. code-block:: python | |
253 | ||
254 | EMAIL_CONFIG = env.email_url( | |
255 | 'EMAIL_URL', default='smtp://user@:password@localhost:25') | |
256 | ||
257 | vars().update(EMAIL_CONFIG) | |
258 | ||
259 | ||
260 | SQLite urls | |
261 | ----------- | |
262 | ||
263 | SQLite connects to file based databases. The same URL format is used, omitting the hostname, | |
264 | and using the "file" portion as the filename of the database. | |
265 | This has the effect of four slashes being present for an absolute | |
266 | file path: sqlite:////full/path/to/your/database/file.sqlite. | |
267 | ||
268 | ||
269 | Tests | |
270 | ===== | |
271 | ||
272 | :: | |
273 | ||
274 | $ git clone git@github.com:joke2k/django-environ.git | |
275 | $ cd django-environ/ | |
276 | $ python setup.py test | |
277 | ||
278 | ||
279 | License | |
280 | ======= | |
281 | ||
282 | Django-environ is licensed under the MIT License - see the `LICENSE_FILE`_ file for details | |
283 | ||
284 | Changelog | |
285 | ========= | |
286 | ||
287 | ||
288 | `0.4.4 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.3...v0.4.4>`__ | |
289 | --------------------------------------------------------------------------------------------- | |
290 | ||
291 | - Support for django-redis multiple locations (master/slave, shards) | |
292 | - Support for Elasticsearch2 | |
293 | - Support for Mysql-connector | |
294 | - Support for pyodbc | |
295 | - Add __contains__ feature to Environ class | |
296 | - Fix Path subtracting | |
297 | ||
298 | ||
299 | `0.4.3 - 21-August-2017 <https://github.com/joke2k/django-environ/compare/v0.4.2...v0.4.3>`__ | |
300 | --------------------------------------------------------------------------------------------- | |
301 | ||
302 | - Rollback the default Environ to os.environ | |
303 | ||
304 | `0.4.2 - 13-April-2017 <https://github.com/joke2k/django-environ/compare/v0.4.1...v0.4.2>`__ | |
305 | -------------------------------------------------------------------------------------------- | |
306 | ||
307 | - Confirm support for Django 1.11. | |
308 | - Support for Redshift database URL | |
309 | - Fix uwsgi settings reload problem (#55) | |
310 | - Update support for django-redis urls (#109) | |
311 | ||
312 | `0.4.1 - 13-November-2016 <https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1>`__ | |
313 | --------------------------------------------------------------------------------------------- | |
314 | - Fix for unsafe characters into URLs | |
315 | - Clarifying warning on missing or unreadable file. Thanks to @nickcatal | |
316 | - Add support for Django 1.10. | |
317 | - Fix support for Oracle urls | |
318 | - Fix support for django-redis | |
319 | ||
320 | ||
321 | `0.4.0 - 23-September-2015 <https://github.com/joke2k/django-environ/compare/v0.3...v0.4>`__ | |
322 | -------------------------------------------------------------------------------------------- | |
323 | - Fix non-ascii values (broken in Python 2.x) | |
324 | - New email schemes - smtp+ssl and smtp+tls (smtps would be deprecated) | |
325 | - redis_cache replaced by django_redis | |
326 | - Add tuple support. Thanks to @anonymouzz | |
327 | - Add LDAP url support for database (django-ldapdb) | |
328 | - Fix psql/pgsql url | |
329 | ||
330 | `0.3 - 03-June-2014 <https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3>`__ | |
331 | --------------------------------------------------------------------------------------- | |
332 | - Add cache url support | |
333 | - Add email url support | |
334 | - Add search url support | |
335 | - Rewriting README.rst | |
336 | ||
337 | 0.2.1 19-April-2013 | |
338 | ------------------- | |
339 | - environ/environ.py: Env.__call__ now uses Env.get_value instance method | |
340 | ||
341 | 0.2 16-April-2013 | |
342 | ----------------- | |
343 | - environ/environ.py, environ/test.py, environ/test_env.txt: add advanced | |
344 | float parsing (comma and dot symbols to separate thousands and decimals) | |
345 | - README.rst, docs/index.rst: fix TYPO in documentation | |
346 | ||
347 | 0.1 02-April-2013 | |
348 | ----------------- | |
349 | - initial release | |
350 | ||
351 | Credits | |
352 | ======= | |
353 | ||
354 | - `12factor`_ | |
355 | - `12factor-django`_ | |
356 | - `Two Scoops of Django`_ | |
357 | - `rconradharris`_ / `envparse`_ | |
358 | - `kennethreitz`_ / `dj-database-url`_ | |
359 | - `migonzalvar`_ / `dj-email-url`_ | |
360 | - `ghickman`_ / `django-cache-url`_ | |
361 | - `dstufft`_ / `dj-search-url`_ | |
362 | - `julianwachholz`_ / `dj-config-url`_ | |
363 | - `nickstenning`_ / `honcho`_ | |
364 | - `envparse`_ | |
365 | - `Distribute`_ | |
366 | - `modern-package-template`_ | |
367 | ||
368 | .. _rconradharris: https://github.com/rconradharris | |
369 | .. _envparse: https://github.com/rconradharris/envparse | |
370 | ||
371 | .. _kennethreitz: https://github.com/kennethreitz | |
372 | .. _dj-database-url: https://github.com/kennethreitz/dj-database-url | |
373 | ||
374 | .. _migonzalvar: https://github.com/migonzalvar | |
375 | .. _dj-email-url: https://github.com/migonzalvar/dj-email-url | |
376 | ||
377 | .. _ghickman: https://github.com/ghickman | |
378 | .. _django-cache-url: https://github.com/ghickman/django-cache-url | |
379 | ||
380 | .. _julianwachholz: https://github.com/julianwachholz | |
381 | .. _dj-config-url: https://github.com/julianwachholz/dj-config-url | |
382 | ||
383 | .. _dstufft: https://github.com/dstufft | |
384 | .. _dj-search-url: https://github.com/dstufft/dj-search-url | |
385 | ||
386 | .. _nickstenning: https://github.com/nickstenning | |
387 | .. _honcho: https://github.com/nickstenning/honcho | |
388 | ||
389 | .. _12factor: http://www.12factor.net/ | |
390 | .. _12factor-django: http://www.wellfireinteractive.com/blog/easier-12-factor-django/ | |
391 | .. _`Two Scoops of Django`: http://twoscoopspress.org/ | |
392 | ||
393 | .. _Distribute: http://pypi.python.org/pypi/distribute | |
394 | .. _`modern-package-template`: http://pypi.python.org/pypi/modern-package-template | |
395 | ||
396 | .. _cookiecutter-django: https://github.com/pydanny/cookiecutter-django | |
397 | ||
398 | .. |pypi| image:: https://img.shields.io/pypi/v/django-environ.svg?style=flat-square&label=version | |
399 | :target: https://pypi.python.org/pypi/django-environ | |
400 | :alt: Latest version released on PyPi | |
401 | ||
402 | .. |coverage| image:: https://img.shields.io/coveralls/joke2k/django-environ/master.svg?style=flat-square | |
403 | :target: https://coveralls.io/r/joke2k/django-environ?branch=master | |
404 | :alt: Test coverage | |
405 | ||
406 | .. |unix_build| image:: https://img.shields.io/travis/joke2k/django-environ/master.svg?style=flat-square&label=unix%20build | |
407 | :target: http://travis-ci.org/joke2k/django-environ | |
408 | :alt: Build status of the master branch on Mac/Linux | |
409 | ||
410 | .. |windows_build| image:: https://img.shields.io/appveyor/ci/joke2k/django-environ.svg?style=flat-square&label=windows%20build | |
411 | :target: https://ci.appveyor.com/project/joke2k/django-environ | |
412 | :alt: Build status of the master branch on Windows | |
413 | ||
414 | .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square | |
415 | :target: https://raw.githubusercontent.com/joke2k/django-environ/master/LICENSE.txt | |
416 | :alt: Package license | |
417 | ||
418 | .. _LICENSE_FILE: https://github.com/joke2k/django-environ/blob/master/LICENSE.txt | |
419 | ||
420 | Keywords: django environment variables 12factor | |
2 | Version: 0.9.0 | |
3 | Summary: A package that allows you to utilize 12factor inspired environment variables to configure your Django application. | |
4 | Home-page: https://django-environ.readthedocs.org | |
5 | Author: Daniele Faraglia | |
6 | Author-email: daniele.faraglia@gmail.com | |
7 | Maintainer: Serghei Iakovlev | |
8 | Maintainer-email: egrep@protonmail.ch | |
9 | License: MIT | |
10 | Project-URL: Documentation, https://django-environ.readthedocs.org | |
11 | Project-URL: Funding, https://opencollective.com/django-environ | |
12 | Project-URL: Say Thanks!, https://saythanks.io/to/joke2k | |
13 | Project-URL: Changelog, https://django-environ.readthedocs.org/en/latest/changelog.html | |
14 | Project-URL: Bug Tracker, https://github.com/joke2k/django-environ/issues | |
15 | Project-URL: Source Code, https://github.com/joke2k/django-environ | |
16 | Keywords: environment,django,variables,12factor | |
421 | 17 | Platform: any |
422 | Classifier: Development Status :: 3 - Alpha | |
423 | Classifier: Intended Audience :: Information Technology | |
18 | Classifier: Development Status :: 5 - Production/Stable | |
424 | 19 | Classifier: Framework :: Django |
425 | Classifier: Framework :: Django :: 1.8 | |
426 | Classifier: Framework :: Django :: 1.9 | |
427 | Classifier: Framework :: Django :: 1.10 | |
428 | 20 | Classifier: Framework :: Django :: 1.11 |
21 | Classifier: Framework :: Django :: 2.0 | |
22 | Classifier: Framework :: Django :: 2.1 | |
23 | Classifier: Framework :: Django :: 2.2 | |
24 | Classifier: Framework :: Django :: 3.0 | |
25 | Classifier: Framework :: Django :: 3.1 | |
26 | Classifier: Framework :: Django :: 3.2 | |
27 | Classifier: Framework :: Django :: 4.0 | |
28 | Classifier: Operating System :: OS Independent | |
29 | Classifier: Intended Audience :: Developers | |
30 | Classifier: Natural Language :: English | |
429 | 31 | Classifier: Programming Language :: Python |
430 | Classifier: Programming Language :: Python :: 2 | |
431 | Classifier: Programming Language :: Python :: 2.7 | |
432 | 32 | Classifier: Programming Language :: Python :: 3 |
433 | 33 | Classifier: Programming Language :: Python :: 3.4 |
434 | 34 | Classifier: Programming Language :: Python :: 3.5 |
35 | Classifier: Programming Language :: Python :: 3.6 | |
36 | Classifier: Programming Language :: Python :: 3.7 | |
37 | Classifier: Programming Language :: Python :: 3.8 | |
38 | Classifier: Programming Language :: Python :: 3.9 | |
39 | Classifier: Programming Language :: Python :: 3.10 | |
435 | 40 | Classifier: Programming Language :: Python :: Implementation :: CPython |
436 | 41 | Classifier: Programming Language :: Python :: Implementation :: PyPy |
437 | 42 | Classifier: Topic :: Software Development :: Libraries :: Python Modules |
438 | 43 | Classifier: Topic :: Utilities |
439 | 44 | Classifier: License :: OSI Approved :: MIT License |
440 | Classifier: Framework :: Django | |
45 | Requires-Python: >=3.4,<4 | |
46 | Description-Content-Type: text/x-rst | |
47 | Provides-Extra: develop | |
48 | Provides-Extra: docs | |
49 | Provides-Extra: testing | |
50 | License-File: LICENSE.txt | |
51 | License-File: AUTHORS.rst | |
52 | ||
53 | ============== | |
54 | django-environ | |
55 | ============== | |
56 | ||
57 | ||
58 | ``django-environ`` is the Python package that allows you to use | |
59 | `Twelve-factor methodology <https://www.12factor.net/>`_ to configure your | |
60 | Django application with environment variables. | |
61 | ||
62 | .. -teaser-end- | |
63 | ||
64 | For that, it gives you an easy way to configure Django application using | |
65 | environment variables obtained from an environment file and provided by the OS: | |
66 | ||
67 | .. -code-begin- | |
68 | ||
69 | .. code-block:: python | |
70 | ||
71 | import environ | |
72 | import os | |
73 | ||
74 | env = environ.Env( | |
75 | # set casting, default value | |
76 | DEBUG=(bool, False) | |
77 | ) | |
78 | ||
79 | # Set the project base directory | |
80 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
81 | ||
82 | # Take environment variables from .env file | |
83 | environ.Env.read_env(os.path.join(BASE_DIR, '.env')) | |
84 | ||
85 | # False if not in os.environ because of casting above | |
86 | DEBUG = env('DEBUG') | |
87 | ||
88 | # Raises Django's ImproperlyConfigured | |
89 | # exception if SECRET_KEY not in os.environ | |
90 | SECRET_KEY = env('SECRET_KEY') | |
91 | ||
92 | # Parse database connection url strings | |
93 | # like psql://user:pass@127.0.0.1:8458/db | |
94 | DATABASES = { | |
95 | # read os.environ['DATABASE_URL'] and raises | |
96 | # ImproperlyConfigured exception if not found | |
97 | # | |
98 | # The db() method is an alias for db_url(). | |
99 | 'default': env.db(), | |
100 | ||
101 | # read os.environ['SQLITE_URL'] | |
102 | 'extra': env.db_url( | |
103 | 'SQLITE_URL', | |
104 | default='sqlite:////tmp/my-tmp-sqlite.db' | |
105 | ) | |
106 | } | |
107 | ||
108 | CACHES = { | |
109 | # Read os.environ['CACHE_URL'] and raises | |
110 | # ImproperlyConfigured exception if not found. | |
111 | # | |
112 | # The cache() method is an alias for cache_url(). | |
113 | 'default': env.cache(), | |
114 | ||
115 | # read os.environ['REDIS_URL'] | |
116 | 'redis': env.cache_url('REDIS_URL') | |
117 | } | |
118 | ||
119 | .. -overview- | |
120 | ||
121 | The idea of this package is to unify a lot of packages that make the same stuff: | |
122 | Take a string from ``os.environ``, parse and cast it to some of useful python | |
123 | typed variables. To do that and to use the `12factor <https://www.12factor.net/>`_ | |
124 | approach, some connection strings are expressed as url, so this package can parse | |
125 | it and return a ``urllib.parse.ParseResult``. These strings from ``os.environ`` | |
126 | are loaded from a ``.env`` file and filled in ``os.environ`` with ``setdefault`` | |
127 | method, to avoid to overwrite the real environ. | |
128 | A similar approach is used in `Two Scoops of Django <https://www.feldroy.com/books/two-scoops-of-django-3-x>`_ | |
129 | book and explained in `12factor-django <https://wellfire.co/learn/easier-12-factor-django>`_ | |
130 | article. | |
131 | ||
132 | ||
133 | Using ``django-environ`` you can stop to make a lot of unversioned | |
134 | ``settings_*.py`` to configure your app. | |
135 | See `cookiecutter-django <https://github.com/cookiecutter/cookiecutter-django>`_ | |
136 | for a concrete example on using with a django project. | |
137 | ||
138 | **Feature Support** | |
139 | ||
140 | - Fast and easy multi environment for deploy | |
141 | - Fill ``os.environ`` with .env file variables | |
142 | - Variables casting | |
143 | - Url variables exploded to django specific package settings | |
144 | - Optional support for Docker-style file based config variables (use | |
145 | ``environ.FileAwareEnv`` instead of ``environ.Env``) | |
146 | ||
147 | .. -project-information- | |
148 | ||
149 | Project Information | |
150 | =================== | |
151 | ||
152 | ``django-environ`` is released under the `MIT / X11 License <https://choosealicense.com/licenses/mit/>`__, | |
153 | its documentation lives at `Read the Docs <https://django-environ.readthedocs.io/en/latest/>`_, | |
154 | the code on `GitHub <https://github.com/joke2k/django-environ>`_, | |
155 | and the latest release on `PyPI <https://pypi.org/project/django-environ/>`_. | |
156 | ||
157 | It’s rigorously tested on Python 3.5+, and officially supports | |
158 | Django 1.11, 2.2, 3.0, 3.1, 3.2 and 4.0. | |
159 | ||
160 | If you'd like to contribute to ``django-environ`` you're most welcome! | |
161 | ||
162 | .. -support- | |
163 | ||
164 | Support | |
165 | ======= | |
166 | ||
167 | Should you have any question, any remark, or if you find a bug, or if there is | |
168 | something you can't do with the ``django-environ``, please | |
169 | `open an issue <https://github.com/joke2k/django-environ>`_. | |
170 | ||
171 | ||
172 | Contributing | |
173 | ============ | |
174 | ||
175 | If you would like to contribute to ``django-environ``, please take a look at the | |
176 | `current issues <https://github.com/joke2k/django-environ/issues>`_. If there is | |
177 | a bug or feature that you want but it isn't listed, make an issue and work on it. | |
178 | ||
179 | How to Contribute | |
180 | ----------------- | |
181 | ||
182 | 1. Check for open issues or open a fresh issue to start a discussion around a | |
183 | feature idea or a bug. | |
184 | 2. Fork `the repository <https://github.com/joke2k/django-environ>`_ on GitHub | |
185 | to start making your changes to the **develop** branch (or branch off of it). | |
186 | 3. Write a test which shows that the bug was fixed or that the feature works as | |
187 | expected. | |
188 | 4. Send a pull request and bug the maintainer until it gets merged and published. | |
189 | ||
190 | ||
191 | Release Information | |
192 | =================== | |
193 | ||
194 | v0.9.0 - 15-June-2022 | |
195 | ------------------------------ | |
196 | Added | |
197 | +++++ | |
198 | - Added support for Postgresql cluster URI | |
199 | `#355 <https://github.com/joke2k/django-environ/pull/355>`_. | |
200 | - Added support for Django 4.0 | |
201 | `#371 <https://github.com/joke2k/django-environ/issues/371>`_. | |
202 | - Added support for prefixed variables | |
203 | `#362 <https://github.com/joke2k/django-environ/issues/362>`_. | |
204 | - Amended documentation. | |
205 | ||
206 | ||
207 | Deprecated | |
208 | ++++++++++ | |
209 | - ``Env.unicode()`` is deprecated and will be removed in the next | |
210 | major release. Use ``Env.str()`` instead. | |
211 | ||
212 | ||
213 | Changed | |
214 | +++++++ | |
215 | - Attach cause to ``ImproperlyConfigured`` exception | |
216 | `#360 <https://github.com/joke2k/django-environ/issues/360>`_. | |
217 | ||
218 | ||
219 | Fixed | |
220 | +++++ | |
221 | - Fixed ``_cast_urlstr`` unquoting | |
222 | `#357 <https://github.com/joke2k/django-environ/issues/357>`_. | |
223 | - Fixed documentation regarding unsafe characters in URLs | |
224 | `#220 <https://github.com/joke2k/django-environ/issues/220>`_. | |
225 | - Fixed ``environ.Path.__eq__()`` to compare paths correctly | |
226 | `#86 <https://github.com/joke2k/django-environ/issues/86>`_, | |
227 | `#197 <https://github.com/joke2k/django-environ/issues/197>`_. | |
228 | ||
229 | `Full changelog <https://django-environ.readthedocs.org/en/latest/changelog.html>`_. | |
230 | ||
231 | Security Policy | |
232 | =============== | |
233 | ||
234 | ||
235 | Reporting a Vulnerability | |
236 | ------------------------- | |
237 | ||
238 | If you discover a security vulnerability within ``django-environ``, please | |
239 | send an e-mail to Serghei Iakovlev via egrep@protonmail.ch. All security | |
240 | vulnerabilities will be promptly addressed. | |
241 | ||
242 | ||
243 | Credits | |
244 | ======= | |
245 | ||
246 | ``django-environ`` was initially created by `Daniele Faraglia <https://github.com/joke2k>`_ | |
247 | and currently maintained by `Serghei Iakovlev <https://github.com/sergeyklay/>`_. | |
248 | ||
249 | A full list of contributors can be found in `GitHub <https://github.com/joke2k/django-environ/graphs/contributors>`__. | |
250 | ||
251 | Acknowledgments | |
252 | =============== | |
253 | ||
254 | The existence of ``django-environ`` would have been impossible without these | |
255 | projects: | |
256 | ||
257 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ | |
258 | - `jazzband/dj-database-url <https://github.com/jazzband/dj-database-url>`_ | |
259 | - `migonzalvar/dj-email-url <https://github.com/migonzalvar/dj-email-url>`_ | |
260 | - `ghickman/django-cache-url <https://github.com/ghickman/django-cache-url>`_ | |
261 | - `dstufft/dj-search-url <https://github.com/dstufft/dj-search-url>`_ | |
262 | - `julianwachholz/dj-config-url <https://github.com/julianwachholz/dj-config-url>`_ | |
263 | - `nickstenning/honcho <https://github.com/nickstenning/honcho>`_ | |
264 | - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_ |
0 | .readthedocs.yml | |
1 | AUTHORS.rst | |
2 | BACKERS.rst | |
3 | CHANGELOG.rst | |
4 | CONTRIBUTING.rst | |
0 | 5 | LICENSE.txt |
1 | 6 | MANIFEST.in |
2 | 7 | README.rst |
8 | SECURITY.rst | |
3 | 9 | setup.cfg |
4 | 10 | setup.py |
11 | tox.ini | |
5 | 12 | django_environ.egg-info/PKG-INFO |
6 | 13 | django_environ.egg-info/SOURCES.txt |
7 | 14 | django_environ.egg-info/dependency_links.txt |
8 | 15 | django_environ.egg-info/not-zip-safe |
9 | 16 | django_environ.egg-info/requires.txt |
10 | 17 | django_environ.egg-info/top_level.txt |
18 | docs/Makefile | |
19 | docs/api.rst | |
20 | docs/backers.rst | |
21 | docs/changelog.rst | |
22 | docs/conf.py | |
23 | docs/contributing.rst | |
24 | docs/deprecations.rst | |
25 | docs/docutils.conf | |
26 | docs/faq.rst | |
27 | docs/index.rst | |
28 | docs/install.rst | |
29 | docs/license.rst | |
30 | docs/quickstart.rst | |
31 | docs/tips.rst | |
32 | docs/types.rst | |
33 | docs/_static/.gitkeep | |
11 | 34 | environ/__init__.py |
35 | environ/compat.py | |
12 | 36 | environ/environ.py |
13 | environ/test.py | |
14 | environ/test_env.txt⏎ | |
37 | environ/fileaware_mapping.py | |
38 | tests/__init__.py | |
39 | tests/asserts.py | |
40 | tests/conftest.py | |
41 | tests/fixtures.py | |
42 | tests/test_cache.py | |
43 | tests/test_db.py | |
44 | tests/test_email.py | |
45 | tests/test_env.py | |
46 | tests/test_env.txt | |
47 | tests/test_fileaware.py | |
48 | tests/test_path.py | |
49 | tests/test_schema.py | |
50 | tests/test_search.py | |
51 | tests/test_utils.py⏎ |
0 | django | |
1 | six | |
0 | ||
1 | [develop] | |
2 | coverage[toml]>=5.0a4 | |
3 | furo==2021.8.*,>=2021.8.17b43 | |
4 | pytest>=4.6.11 | |
5 | sphinx-notfound-page | |
6 | sphinx>=3.5.0 | |
7 | ||
8 | [docs] | |
9 | furo==2021.8.*,>=2021.8.17b43 | |
10 | sphinx-notfound-page | |
11 | sphinx>=3.5.0 | |
12 | ||
13 | [testing] | |
14 | coverage[toml]>=5.0a4 | |
15 | pytest>=4.6.11 |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | # Makefile for Sphinx documentation | |
9 | ||
10 | # You can set these variables from the command line. | |
11 | SPHINXOPTS = | |
12 | SPHINXBUILD = sphinx-build | |
13 | PAPER = | |
14 | BUILDDIR = _build | |
15 | ||
16 | # User-friendly check for sphinx-build | |
17 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) | |
18 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) | |
19 | endif | |
20 | ||
21 | # Internal variables. | |
22 | PAPEROPT_a4 = -D latex_paper_size=a4 | |
23 | PAPEROPT_letter = -D latex_paper_size=letter | |
24 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
25 | # the i18n builder cannot share the environment and doctrees with the others | |
26 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
27 | ||
28 | .PHONY: help | |
29 | help: | |
30 | @echo "Please use \`make <target>' where <target> is one of" | |
31 | @echo " html to make standalone HTML files" | |
32 | @echo " dirhtml to make HTML files named index.html in directories" | |
33 | @echo " singlehtml to make a single large HTML file" | |
34 | @echo " pickle to make pickle files" | |
35 | @echo " json to make JSON files" | |
36 | @echo " htmlhelp to make HTML files and a HTML help project" | |
37 | @echo " qthelp to make HTML files and a qthelp project" | |
38 | @echo " devhelp to make HTML files and a Devhelp project" | |
39 | @echo " epub to make an epub" | |
40 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | |
41 | @echo " latexpdf to make LaTeX files and run them through pdflatex" | |
42 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | |
43 | @echo " text to make text files" | |
44 | @echo " man to make manual pages" | |
45 | @echo " texinfo to make Texinfo files" | |
46 | @echo " info to make Texinfo files and run them through makeinfo" | |
47 | @echo " gettext to make PO message catalogs" | |
48 | @echo " changes to make an overview of all changed/added/deprecated items" | |
49 | @echo " xml to make Docutils-native XML files" | |
50 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" | |
51 | @echo " linkcheck to check all external links for integrity" | |
52 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" | |
53 | ||
54 | .PHONY: clean | |
55 | clean: | |
56 | rm -rf $(BUILDDIR)/* | |
57 | ||
58 | .PHONY: html | |
59 | html: | |
60 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | |
61 | @echo | |
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | |
63 | ||
64 | .PHONY: dirhtml | |
65 | dirhtml: | |
66 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | |
67 | @echo | |
68 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | |
69 | ||
70 | .PHONY: singlehtml | |
71 | singlehtml: | |
72 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | |
73 | @echo | |
74 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | |
75 | ||
76 | .PHONY: pickle | |
77 | pickle: | |
78 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | |
79 | @echo | |
80 | @echo "Build finished; now you can process the pickle files." | |
81 | ||
82 | .PHONY: json | |
83 | json: | |
84 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | |
85 | @echo | |
86 | @echo "Build finished; now you can process the JSON files." | |
87 | ||
88 | .PHONY: htmlhelp | |
89 | htmlhelp: | |
90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | |
91 | @echo | |
92 | @echo "Build finished; now you can run HTML Help Workshop with the" \ | |
93 | ".hhp project file in $(BUILDDIR)/htmlhelp." | |
94 | ||
95 | .PHONY: qthelp | |
96 | qthelp: | |
97 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | |
98 | @echo | |
99 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ | |
100 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | |
101 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-environ.qhcp" | |
102 | @echo "To view the help file:" | |
103 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-environ.qhc" | |
104 | ||
105 | .PHONY: devhelp | |
106 | devhelp: | |
107 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | |
108 | @echo | |
109 | @echo "Build finished." | |
110 | @echo "To view the help file:" | |
111 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-environ" | |
112 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-environ" | |
113 | @echo "# devhelp" | |
114 | ||
115 | .PHONY: epub | |
116 | epub: | |
117 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | |
118 | @echo | |
119 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." | |
120 | ||
121 | .PHONY: latex | |
122 | latex: | |
123 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
124 | @echo | |
125 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | |
126 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ | |
127 | "(use \`make latexpdf' here to do that automatically)." | |
128 | ||
129 | .PHONY: latexpdf | |
130 | latexpdf: | |
131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
132 | @echo "Running LaTeX files through pdflatex..." | |
133 | $(MAKE) -C $(BUILDDIR)/latex all-pdf | |
134 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
135 | ||
136 | .PHONY: | |
137 | latexpdfja: | |
138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
139 | @echo "Running LaTeX files through platex and dvipdfmx..." | |
140 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | |
141 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
142 | ||
143 | .PHONY: text | |
144 | text: | |
145 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | |
146 | @echo | |
147 | @echo "Build finished. The text files are in $(BUILDDIR)/text." | |
148 | ||
149 | .PHONY: man | |
150 | man: | |
151 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | |
152 | @echo | |
153 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." | |
154 | ||
155 | .PHONY: texinfo | |
156 | texinfo: | |
157 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
158 | @echo | |
159 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | |
160 | @echo "Run \`make' in that directory to run these through makeinfo" \ | |
161 | "(use \`make info' here to do that automatically)." | |
162 | ||
163 | .PHONY: info | |
164 | info: | |
165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
166 | @echo "Running Texinfo files through makeinfo..." | |
167 | make -C $(BUILDDIR)/texinfo info | |
168 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | |
169 | ||
170 | .PHONY: gettext | |
171 | gettext: | |
172 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | |
173 | @echo | |
174 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | |
175 | ||
176 | .PHONY: changes | |
177 | changes: | |
178 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | |
179 | @echo | |
180 | @echo "The overview file is in $(BUILDDIR)/changes." | |
181 | ||
182 | .PHONY: linkcheck | |
183 | linkcheck: | |
184 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | |
185 | @echo | |
186 | @echo "Link check complete; look for any errors in the above output " \ | |
187 | "or in $(BUILDDIR)/linkcheck/output.txt." | |
188 | ||
189 | .PHONY: doctest | |
190 | doctest: | |
191 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | |
192 | @echo "Testing of doctests in the sources finished, look at the " \ | |
193 | "results in $(BUILDDIR)/doctest/output.txt." | |
194 | ||
195 | .PHONY: xml | |
196 | xml: | |
197 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | |
198 | @echo | |
199 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." | |
200 | ||
201 | .PHONY: pseudoxml | |
202 | pseudoxml: | |
203 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | |
204 | @echo | |
205 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." |
0 | ============= | |
1 | API Reference | |
2 | ============= | |
3 | ||
4 | .. currentmodule:: environ | |
5 | ||
6 | ||
7 | The ``__init__`` module | |
8 | ======================= | |
9 | ||
10 | .. automodule:: environ | |
11 | :members: | |
12 | :special-members: | |
13 | :no-undoc-members: | |
14 | ||
15 | ||
16 | The ``compat`` module | |
17 | ====================== | |
18 | ||
19 | .. automodule:: environ.compat | |
20 | :members: | |
21 | :no-undoc-members: | |
22 | ||
23 | ||
24 | The ``environ`` module | |
25 | ====================== | |
26 | ||
27 | .. autoclass:: environ.Env | |
28 | :members: | |
29 | :no-undoc-members: | |
30 | ||
31 | .. autoclass:: environ.FileAwareEnv | |
32 | :members: | |
33 | :no-undoc-members: | |
34 | ||
35 | .. autoclass:: environ.Path | |
36 | :members: | |
37 | :no-undoc-members: | |
38 | ||
39 | ||
40 | The ``fileaware_mapping`` module | |
41 | ================================ | |
42 | ||
43 | .. autoclass:: environ.fileaware_mapping.FileAwareMapping | |
44 | :members: | |
45 | :no-undoc-members: |
0 | .. include:: ../BACKERS.rst |
0 | .. include:: ../CHANGELOG.rst |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | # | |
9 | # -- Utils --------------------------------------------------------- | |
10 | # | |
11 | ||
12 | import codecs | |
13 | import os | |
14 | import sys | |
15 | import re | |
16 | ||
17 | from datetime import date | |
18 | ||
19 | ||
20 | PROJECT_DIR = os.path.abspath('..') | |
21 | sys.path.insert(0, PROJECT_DIR) | |
22 | ||
23 | ||
24 | def read_file(filepath): | |
25 | """Read content from a UTF-8 encoded text file.""" | |
26 | with codecs.open(filepath, 'rb', 'utf-8') as file_handle: | |
27 | return file_handle.read() | |
28 | ||
29 | ||
30 | def find_version(meta_file): | |
31 | """Extract ``__version__`` from meta_file.""" | |
32 | contents = read_file(os.path.join(PROJECT_DIR, meta_file)) | |
33 | meta_match = re.search( | |
34 | r"^__version__\s+=\s+['\"]([^'\"]*)['\"]", | |
35 | contents, | |
36 | re.M | |
37 | ) | |
38 | ||
39 | if meta_match: | |
40 | return meta_match.group(1) | |
41 | raise RuntimeError( | |
42 | "Unable to find __version__ string in package meta file") | |
43 | ||
44 | ||
45 | # | |
46 | # -- Project information ----------------------------------------------------- | |
47 | # | |
48 | ||
49 | # General information about the project. | |
50 | project = "django-environ" | |
51 | copyright = f'2013-{date.today().year}, Daniele Faraglia and other contributors' | |
52 | author = u"Daniele Faraglia \\and Serghei Iakovlev" | |
53 | ||
54 | # | |
55 | # -- General configuration --------------------------------------------------- | |
56 | # | |
57 | ||
58 | extensions = [ | |
59 | "sphinx.ext.autodoc", | |
60 | "sphinx.ext.doctest", | |
61 | "sphinx.ext.intersphinx", | |
62 | "sphinx.ext.todo", | |
63 | "sphinx.ext.viewcode", | |
64 | "notfound.extension", | |
65 | ] | |
66 | ||
67 | # Add any paths that contain templates here, relative to this directory. | |
68 | templates_path = ["_templates"] | |
69 | ||
70 | # The suffix of source filenames. | |
71 | source_suffix = ".rst" | |
72 | ||
73 | # Allow non-local URIs so we can have images in CHANGELOG etc. | |
74 | suppress_warnings = [ | |
75 | "image.nonlocal_uri", | |
76 | ] | |
77 | ||
78 | # The master toctree document. | |
79 | master_doc = "index" | |
80 | ||
81 | # The version info | |
82 | # The short X.Y version. | |
83 | release = find_version(os.path.join("environ", "__init__.py")) | |
84 | version = release.rsplit(u".", 1)[0] | |
85 | # The full version, including alpha/beta/rc tags. | |
86 | ||
87 | # List of patterns, relative to source directory, that match files and | |
88 | # directories to ignore when looking for source files. | |
89 | exclude_patterns = ["_build"] | |
90 | ||
91 | # The reST default role (used for this markup: `text`) to use for all | |
92 | # documents. | |
93 | default_role = "any" | |
94 | ||
95 | # If true, '()' will be appended to :func: etc. cross-reference text. | |
96 | add_function_parentheses = True | |
97 | ||
98 | # | |
99 | # -- Options for autodoc --------------------------------------------------- | |
100 | # | |
101 | ||
102 | # This value selects if automatically documented members are sorted alphabetical | |
103 | # (value 'alphabetical'), by member type (value 'groupwise') or by source order | |
104 | # (value 'bysource'). The default is alphabetical. | |
105 | # | |
106 | # Note that for source order, the module must be a Python module with the | |
107 | # source code available. | |
108 | autodoc_member_order = 'bysource' | |
109 | ||
110 | # | |
111 | # -- Options for linkcheck --------------------------------------------------- | |
112 | # | |
113 | ||
114 | linkcheck_ignore = [ | |
115 | # We run into GitHub's rate limits. | |
116 | r"https://github.com/.*/(issues|pull)/\d+", | |
117 | # Do not check links to compare tags. | |
118 | r"https://github.com/joke2k/django-environ/compare/.*", | |
119 | ] | |
120 | ||
121 | # | |
122 | # -- Options for nitpick ----------------------------------------------------- | |
123 | # | |
124 | ||
125 | # In nitpick mode (-n), still ignore any of the following "broken" references | |
126 | # to non-types. | |
127 | nitpick_ignore = [ | |
128 | ] | |
129 | ||
130 | # | |
131 | # -- Options for extlinks ---------------------------------------------------- | |
132 | # | |
133 | ||
134 | extlinks = { | |
135 | "pypi": ("https://pypi.org/project/%s/", ""), | |
136 | } | |
137 | ||
138 | # | |
139 | # -- Options for intersphinx ------------------------------------------------- | |
140 | # | |
141 | ||
142 | intersphinx_mapping = { | |
143 | "python": ("https://docs.python.org/3", None), | |
144 | "sphinx": ("https://www.sphinx-doc.org/en/master", None), | |
145 | } | |
146 | ||
147 | # | |
148 | # -- Options for TODOs ------------------------------------------------------- | |
149 | # | |
150 | ||
151 | todo_include_todos = True | |
152 | ||
153 | # -- Options for HTML output ------------------------------------------------- | |
154 | ||
155 | # html_favicon = None | |
156 | ||
157 | html_theme = "furo" | |
158 | html_title = project | |
159 | ||
160 | html_theme_options = {} | |
161 | ||
162 | # Add any paths that contain custom static files (such as style sheets) here, | |
163 | # relative to this directory. They are copied after the builtin static files, | |
164 | # so a file named "default.css" will overwrite the builtin "default.css". | |
165 | html_static_path = ["_static"] | |
166 | ||
167 | # If false, no module index is generated. | |
168 | html_domain_indices = True | |
169 | ||
170 | # If false, no index is generated. | |
171 | html_use_index = True | |
172 | ||
173 | # If true, the index is split into individual pages for each letter. | |
174 | html_split_index = False | |
175 | ||
176 | # If true, links to the reST sources are added to the pages. | |
177 | html_show_sourcelink = False | |
178 | ||
179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | |
180 | html_show_sphinx = True | |
181 | ||
182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | |
183 | html_show_copyright = True | |
184 | ||
185 | # If true, an OpenSearch description file will be output, and all pages will | |
186 | # contain a <link> tag referring to it. The value of this option must be the | |
187 | # base URL from which the finished HTML is served. | |
188 | # html_use_openserver = '' | |
189 | ||
190 | # Output file base name for HTML help builder. | |
191 | htmlhelp_basename = "django-environ-doc" | |
192 | ||
193 | # | |
194 | # -- Options for manual page output ------------------------------------------ | |
195 | # | |
196 | ||
197 | # One entry per manual page. List of tuples | |
198 | # (source start file, name, description, authors, manual section). | |
199 | man_pages = [ | |
200 | ("index", project, "django-environ Documentation", [author], 1) | |
201 | ] | |
202 | ||
203 | # | |
204 | # -- Options for Texinfo output ---------------------------------------------- | |
205 | # | |
206 | ||
207 | # Grouping the document tree into Texinfo files. List of tuples | |
208 | # (source start file, target name, title, author, | |
209 | # dir menu entry, description, category) | |
210 | texinfo_documents = [ | |
211 | ( | |
212 | "index", | |
213 | project, | |
214 | "django-environ Documentation", | |
215 | author, | |
216 | project, | |
217 | "Configure Django made easy.", | |
218 | "Miscellaneous", | |
219 | ) | |
220 | ] |
0 | .. include:: ../CONTRIBUTING.rst |
0 | ============ | |
1 | Deprecations | |
2 | ============ | |
3 | ||
4 | Features deprecated in 0.9.0 | |
5 | ============================ | |
6 | ||
7 | Methods | |
8 | ------- | |
9 | ||
10 | * The :meth:`.environ.Env.unicode` method is deprecated as it was used | |
11 | for Python 2.x only. Use :meth:`.environ.Env.str` instead. |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | [parsers] | |
9 | ||
10 | [restructuredtext parser] | |
11 | smart_quotes=yes |
0 | === | |
1 | FAQ | |
2 | === | |
3 | ||
4 | ||
5 | #. **Can django-environ determine the location of .env file automatically?** | |
6 | ||
7 | ``django-environ`` will try to get and read ``.env`` file from the project | |
8 | root if you haven't specified the path for it when call :meth:`.environ.Env.read_env`. | |
9 | However, this is not the recommended way. When it is possible always specify | |
10 | the path tho ``.env`` file. Alternatively, you can use a trick with a | |
11 | environment variable pointing to the actual location of ``.env`` file. | |
12 | For details see ":ref:`multiple-env-files-label`". | |
13 | ||
14 | #. **What (where) is the root part of the project, is it part of the project where are settings?** | |
15 | ||
16 | Where your ``manage.py`` file is (that is your project root directory). | |
17 | ||
18 | #. **What kind of file should .env be?** | |
19 | ||
20 | ``.env`` is a plain text file. | |
21 | ||
22 | #. **Should name of the file be simply .env (or something.env)?** | |
23 | ||
24 | Just ``.env``. However, this is not a strict rule, but just a common | |
25 | practice. Formally, you can use any filename. | |
26 | ||
27 | #. **Is .env file going to be imported in settings file?** | |
28 | ||
29 | No need to import, ``django-environ`` automatically picks variables | |
30 | from there. |
0 | ======================================= | |
1 | Welcome to django-environ documentation | |
2 | ======================================= | |
3 | ||
4 | Release v\ |release| (`What's new? <changelog>`). | |
5 | ||
6 | .. include:: ../README.rst | |
7 | :start-after: -teaser-begin- | |
8 | :end-before: -teaser-end- | |
9 | ||
10 | Overview | |
11 | ======== | |
12 | ||
13 | .. include:: ../README.rst | |
14 | :start-after: -overview- | |
15 | :end-before: -project-information- | |
16 | ||
17 | ---- | |
18 | ||
19 | Full Table of Contents | |
20 | ====================== | |
21 | ||
22 | The User Guide | |
23 | -------------- | |
24 | ||
25 | This part of the documentation, which is mostly prose, begins with some | |
26 | background information about django-environ, then focuses on step-by-step | |
27 | instructions for getting the most out of django-environ. | |
28 | ||
29 | .. toctree:: | |
30 | :maxdepth: 2 | |
31 | ||
32 | install | |
33 | quickstart | |
34 | ||
35 | ||
36 | The Community Guide | |
37 | ------------------- | |
38 | ||
39 | This part of the documentation, which is mostly prose, details the | |
40 | django-environ ecosystem and community. | |
41 | ||
42 | .. toctree:: | |
43 | :maxdepth: 2 | |
44 | ||
45 | faq | |
46 | types | |
47 | tips | |
48 | ||
49 | .. toctree:: | |
50 | :maxdepth: 1 | |
51 | ||
52 | deprecations | |
53 | changelog | |
54 | ||
55 | ||
56 | The API Documentation / Guide | |
57 | ----------------------------- | |
58 | ||
59 | If you are looking for information on a specific function, class, or method, | |
60 | this part of the documentation is for you. | |
61 | ||
62 | .. toctree:: | |
63 | :maxdepth: 2 | |
64 | ||
65 | api | |
66 | ||
67 | ||
68 | The Contributor Guide | |
69 | --------------------- | |
70 | ||
71 | If you want to contribute to the project, this part of the documentation is for | |
72 | you. | |
73 | ||
74 | .. toctree:: | |
75 | :maxdepth: 3 | |
76 | ||
77 | contributing | |
78 | backers | |
79 | license | |
80 | ||
81 | .. include:: ../README.rst | |
82 | :start-after: -support- | |
83 | ||
84 | .. include:: ../README.rst | |
85 | :start-after: -project-information- | |
86 | :end-before: -support- |
0 | ============ | |
1 | Installation | |
2 | ============ | |
3 | ||
4 | ||
5 | Requirements | |
6 | ============ | |
7 | ||
8 | * `Django <https://www.djangoproject.com/>`_ >= 1.11 | |
9 | * `Python <https://www.python.org/>`_ >= 3.5 | |
10 | ||
11 | Installing django-environ | |
12 | ========================= | |
13 | ||
14 | ``django-environ`` is a Python-only package `hosted_on_pypi`_. | |
15 | The recommended installation method is `pip`_-installing into a | |
16 | :mod:`virtualenv <python:venv>`: | |
17 | ||
18 | .. code-block:: console | |
19 | ||
20 | $ python -m pip install django-environ | |
21 | ||
22 | .. note:: | |
23 | ||
24 | After installing ``django-environ``, no need to add it to ``INSTALLED_APPS``. | |
25 | ||
26 | ||
27 | .. _hosted_on_pypi: https://pypi.org/project/django-environ/ | |
28 | .. _pip: https://pip.pypa.io/en/stable/ | |
29 | ||
30 | ||
31 | Unstable version | |
32 | ================ | |
33 | ||
34 | The master of all the material is the Git repository at https://github.com/joke2k/django-environ. | |
35 | So, you can also install the latest unreleased development version directly from the | |
36 | ``develop`` branch on GitHub. It is a work-in-progress of a future stable release so the | |
37 | experience might be not as smooth: | |
38 | ||
39 | .. code-block:: console | |
40 | ||
41 | $ pip install -e git://github.com/joke2k/django-environ.git#egg=django-environ | |
42 | # OR | |
43 | $ pip install --upgrade https://github.com/joke2k/django-environ.git/archive/develop.tar.gz | |
44 | ||
45 | This command will download the latest version of ``django-environ`` and install | |
46 | it to your system. | |
47 | ||
48 | .. note:: | |
49 | ||
50 | The ``develop`` branch will always contain the latest unstable version, so the experience | |
51 | might be not as smooth. If you wish to check older versions or formal, tagged release, | |
52 | please switch to the relevant `tag <https://github.com/joke2k/django-environ/tags>`_. | |
53 | ||
54 | More information about ``pip`` and PyPI can be found here: | |
55 | ||
56 | * `Install pip <https://pip.pypa.io/en/latest/installing/>`_ | |
57 | * `Python Packaging User Guide <https://packaging.python.org/en/latest/>`_ |
0 | =================== | |
1 | License and Credits | |
2 | =================== | |
3 | ||
4 | ``django-environ`` is open source software licensed under the | |
5 | `MIT / X11 License <https://choosealicense.com/licenses/mit/>`_. | |
6 | The full license text can be also found in the `source code repository <https://github.com/joke2k/django-environ/blob/main/LICENSE.txt>`_. | |
7 | ||
8 | .. include:: ../AUTHORS.rst |
0 | =========== | |
1 | Quick Start | |
2 | =========== | |
3 | ||
4 | .. include:: ../README.rst | |
5 | :start-after: -code-begin- | |
6 | :end-before: -overview- | |
7 | ||
8 | Usage | |
9 | ===== | |
10 | ||
11 | Create a ``.env`` file in project root directory. The file format can be understood | |
12 | from the example below: | |
13 | ||
14 | .. code-block:: shell | |
15 | ||
16 | DEBUG=on | |
17 | SECRET_KEY=your-secret-key | |
18 | DATABASE_URL=psql://user:un-githubbedpassword@127.0.0.1:8458/database | |
19 | SQLITE_URL=sqlite:///my-local-sqlite.db | |
20 | CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213 | |
21 | REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=ungithubbed-secret | |
22 | ||
23 | And use it with ``settings.py`` as follows: | |
24 | ||
25 | .. include:: ../README.rst | |
26 | :start-after: -code-begin- | |
27 | :end-before: -overview- | |
28 | ||
29 | The ``.env`` file should be specific to the environment and not checked into | |
30 | version control, it is best practice documenting the ``.env`` file with an example. | |
31 | For example, you can also add ``.env.dist`` with a template of your variables to | |
32 | the project repo. This file should describe the mandatory variables for the | |
33 | Django application, and it can be committed to version control. This provides a | |
34 | useful reference and speeds up the on-boarding process for new team members, since | |
35 | the time to dig through the codebase to find out what has to be set up is reduced. | |
36 | ||
37 | A good ``.env.dist`` could look like this: | |
38 | ||
39 | .. code-block:: shell | |
40 | ||
41 | # SECURITY WARNING: don't run with the debug turned on in production! | |
42 | DEBUG=True | |
43 | ||
44 | # Should robots.txt allow everything to be crawled? | |
45 | ALLOW_ROBOTS=False | |
46 | ||
47 | # SECURITY WARNING: keep the secret key used in production secret! | |
48 | SECRET_KEY=secret | |
49 | ||
50 | # A list of all the people who get code error notifications. | |
51 | ADMINS="John Doe <john@example.com>, Mary <mary@example.com>" | |
52 | ||
53 | # A list of all the people who should get broken link notifications. | |
54 | MANAGERS="Blake <blake@cyb.org>, Alice Judge <alice@cyb.org>" | |
55 | ||
56 | # By default, Django will send system email from root@localhost. | |
57 | # However, some mail providers reject all email from this address. | |
58 | SERVER_EMAIL=webmaster@example.com |
0 | ==== | |
1 | Tips | |
2 | ==== | |
3 | ||
4 | ||
5 | Docker-style file based variables | |
6 | ================================= | |
7 | ||
8 | Docker (swarm) and Kubernetes are two widely used platforms that store their | |
9 | secrets in tmpfs inside containers as individual files, providing a secure way | |
10 | to be able to share configuration data between containers. | |
11 | ||
12 | Use :class:`.environ.FileAwareEnv` rather than :class:`.environ.Env` to first look for | |
13 | environment variables with ``_FILE`` appended. If found, their contents will be | |
14 | read from the file system and used instead. | |
15 | ||
16 | For example, given an app with the following in its settings module: | |
17 | ||
18 | .. code-block:: python | |
19 | ||
20 | import environ | |
21 | ||
22 | env = environ.FileAwareEnv() | |
23 | SECRET_KEY = env("SECRET_KEY") | |
24 | ||
25 | the example ``docker-compose.yml`` for would contain: | |
26 | ||
27 | .. code-block:: yaml | |
28 | ||
29 | secrets: | |
30 | secret_key: | |
31 | external: true | |
32 | ||
33 | services: | |
34 | app: | |
35 | secrets: | |
36 | - secret_key | |
37 | environment: | |
38 | - SECRET_KEY_FILE=/run/secrets/secret_key | |
39 | ||
40 | ||
41 | Using unsafe characters in URLs | |
42 | =============================== | |
43 | ||
44 | In order to use unsafe characters you have to encode with :py:func:`urllib.parse.quote` | |
45 | before you set into ``.env`` file. Encode only the value (i.e. the password) not the whole url. | |
46 | ||
47 | .. code-block:: shell | |
48 | ||
49 | DATABASE_URL=mysql://user:%23password@127.0.0.1:3306/dbname | |
50 | ||
51 | See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for reference. | |
52 | ||
53 | ||
54 | Smart Casting | |
55 | ============= | |
56 | ||
57 | ``django-environ`` has a "Smart-casting" enabled by default, if you don't provide a ``cast`` type, it will be detected from ``default`` type. | |
58 | This could raise side effects (see `#192 <https://github.com/joke2k/django-environ/issues/192>`_). | |
59 | To disable it use ``env.smart_cast = False``. | |
60 | ||
61 | .. note:: | |
62 | ||
63 | The next major release will disable it by default. | |
64 | ||
65 | ||
66 | Multiple redis cache locations | |
67 | ============================== | |
68 | ||
69 | For redis cache, multiple master/slave or shard locations can be configured as follows: | |
70 | ||
71 | .. code-block:: shell | |
72 | ||
73 | CACHE_URL='rediscache://master:6379,slave1:6379,slave2:6379/1' | |
74 | ||
75 | ||
76 | Email settings | |
77 | ============== | |
78 | ||
79 | In order to set email configuration for Django you can use this code: | |
80 | ||
81 | .. code-block:: python | |
82 | ||
83 | # The email() method is an alias for email_url(). | |
84 | EMAIL_CONFIG = env.email( | |
85 | 'EMAIL_URL', | |
86 | default='smtp://user:password@localhost:25' | |
87 | ) | |
88 | ||
89 | vars().update(EMAIL_CONFIG) | |
90 | ||
91 | ||
92 | SQLite urls | |
93 | =========== | |
94 | ||
95 | SQLite connects to file based databases. The same URL format is used, omitting the hostname, | |
96 | and using the "file" portion as the filename of the database. | |
97 | This has the effect of four slashes being present for an absolute | |
98 | ||
99 | file path: ``sqlite:////full/path/to/your/database/file.sqlite``. | |
100 | ||
101 | ||
102 | Nested lists | |
103 | ============ | |
104 | ||
105 | Some settings such as Django's ``ADMINS`` make use of nested lists. | |
106 | You can use something like this to handle similar cases. | |
107 | ||
108 | .. code-block:: python | |
109 | ||
110 | # DJANGO_ADMINS=Blake:blake@cyb.org,Alice:alice@cyb.org | |
111 | ADMINS = [x.split(':') for x in env.list('DJANGO_ADMINS')] | |
112 | ||
113 | # or use more specific function | |
114 | ||
115 | from email.utils import getaddresses | |
116 | ||
117 | # DJANGO_ADMINS=Alice Judge <alice@cyb.org>,blake@cyb.org | |
118 | ADMINS = getaddresses([env('DJANGO_ADMINS')]) | |
119 | ||
120 | # another option is to use parseaddr from email.utils | |
121 | ||
122 | # DJANGO_ADMINS="Blake <blake@cyb.org>, Alice Judge <alice@cyb.org>" | |
123 | from email.utils import parseaddr | |
124 | ||
125 | ADMINS = tuple(parseaddr(email) for email in env.list('DJANGO_ADMINS')) | |
126 | ||
127 | ||
128 | .. _complex_dict_format: | |
129 | ||
130 | Complex dict format | |
131 | =================== | |
132 | ||
133 | Sometimes we need to get a bit more complex dict type than usual. For example, | |
134 | consider Djangosaml2's ``SAML_ATTRIBUTE_MAPPING``: | |
135 | ||
136 | .. code-block:: python | |
137 | ||
138 | SAML_ATTRIBUTE_MAPPING = { | |
139 | 'uid': ('username', ), | |
140 | 'mail': ('email', ), | |
141 | 'cn': ('first_name', ), | |
142 | 'sn': ('last_name', ), | |
143 | } | |
144 | ||
145 | A dict of this format can be obtained as shown below: | |
146 | ||
147 | **.env file**: | |
148 | ||
149 | .. code-block:: shell | |
150 | ||
151 | # .env file contents | |
152 | SAML_ATTRIBUTE_MAPPING="uid=username;mail=email;cn=first_name;sn=last_name;" | |
153 | ||
154 | **settings.py file**: | |
155 | ||
156 | .. code-block:: python | |
157 | ||
158 | # settings.py file contents | |
159 | import environ | |
160 | ||
161 | ||
162 | env = environ.Env() | |
163 | ||
164 | # {'uid': ('username',), 'mail': ('email',), 'cn': ('first_name',), 'sn': ('last_name',)} | |
165 | SAML_ATTRIBUTE_MAPPING = env.dict( | |
166 | 'SAML_ATTRIBUTE_MAPPING', | |
167 | cast={'value': tuple}, | |
168 | default={} | |
169 | ) | |
170 | ||
171 | ||
172 | Multiline value | |
173 | =============== | |
174 | ||
175 | To get multiline value pass ``multiline=True`` to ```str()```. | |
176 | ||
177 | .. note:: | |
178 | ||
179 | You shouldn't escape newline/tab characters yourself if you want to preserve | |
180 | the formatting. | |
181 | ||
182 | The following example demonstrates the above: | |
183 | ||
184 | **.env file**: | |
185 | ||
186 | .. code-block:: shell | |
187 | ||
188 | # .env file contents | |
189 | UNQUOTED_CERT=---BEGIN---\r\n---END--- | |
190 | QUOTED_CERT="---BEGIN---\r\n---END---" | |
191 | ESCAPED_CERT=---BEGIN---\\n---END--- | |
192 | ||
193 | **settings.py file**: | |
194 | ||
195 | .. code-block:: python | |
196 | ||
197 | # settings.py file contents | |
198 | import environ | |
199 | ||
200 | ||
201 | env = environ.Env() | |
202 | ||
203 | print(env.str('UNQUOTED_CERT', multiline=True)) | |
204 | # ---BEGIN--- | |
205 | # ---END--- | |
206 | ||
207 | print(env.str('UNQUOTED_CERT', multiline=False)) | |
208 | # ---BEGIN---\r\n---END--- | |
209 | ||
210 | print(env.str('QUOTED_CERT', multiline=True)) | |
211 | # ---BEGIN--- | |
212 | # ---END--- | |
213 | ||
214 | print(env.str('QUOTED_CERT', multiline=False)) | |
215 | # ---BEGIN---\r\n---END--- | |
216 | ||
217 | print(env.str('ESCAPED_CERT', multiline=True)) | |
218 | # ---BEGIN---\ | |
219 | # ---END--- | |
220 | ||
221 | print(env.str('ESCAPED_CERT', multiline=False)) | |
222 | # ---BEGIN---\\n---END--- | |
223 | ||
224 | Proxy value | |
225 | =========== | |
226 | ||
227 | Values that being with a ``$`` may be interpolated. Pass ``interpolate=True`` to | |
228 | ``environ.Env()`` to enable this feature: | |
229 | ||
230 | .. code-block:: python | |
231 | ||
232 | import environ | |
233 | ||
234 | env = environ.Env(interpolate=True) | |
235 | ||
236 | # BAR=FOO | |
237 | # PROXY=$BAR | |
238 | >>> print env.str('PROXY') | |
239 | FOO | |
240 | ||
241 | ||
242 | Escape Proxy | |
243 | ============ | |
244 | ||
245 | If you're having trouble with values starting with dollar sign ($) without the intention of proxying the value to | |
246 | another, You should enable the ``escape_proxy`` and prepend a backslash to it. | |
247 | ||
248 | .. code-block:: python | |
249 | ||
250 | import environ | |
251 | ||
252 | env = environ.Env() | |
253 | env.escape_proxy = True | |
254 | ||
255 | # ESCAPED_VAR=\$baz | |
256 | env.str('ESCAPED_VAR') # $baz | |
257 | ||
258 | ||
259 | Reading env files | |
260 | ================= | |
261 | ||
262 | .. _multiple-env-files-label: | |
263 | ||
264 | Multiple env files | |
265 | ------------------ | |
266 | ||
267 | There is an ability point to the .env file location using an environment | |
268 | variable. This feature may be convenient in a production systems with a | |
269 | different .env file location. | |
270 | ||
271 | The following example demonstrates the above: | |
272 | ||
273 | .. code-block:: shell | |
274 | ||
275 | # /etc/environment file contents | |
276 | DEBUG=False | |
277 | ||
278 | .. code-block:: shell | |
279 | ||
280 | # .env file contents | |
281 | DEBUG=True | |
282 | ||
283 | .. code-block:: python | |
284 | ||
285 | env = environ.Env() | |
286 | env.read_env(env.str('ENV_PATH', '.env')) | |
287 | ||
288 | ||
289 | Now ``ENV_PATH=/etc/environment ./manage.py runserver`` uses ``/etc/environment`` | |
290 | while ``./manage.py runserver`` uses ``.env``. | |
291 | ||
292 | ||
293 | Using Path objects when reading env | |
294 | ----------------------------------- | |
295 | ||
296 | It is possible to use of :py:class:`pathlib.Path` objects when reading environment | |
297 | file from the filesystem: | |
298 | ||
299 | .. code-block:: python | |
300 | ||
301 | import os | |
302 | import pathlib | |
303 | ||
304 | import environ | |
305 | ||
306 | ||
307 | # Build paths inside the project like this: BASE_DIR('subdir'). | |
308 | BASE_DIR = environ.Path(__file__) - 3 | |
309 | ||
310 | env = environ.Env() | |
311 | ||
312 | # The four lines below do the same: | |
313 | env.read_env(BASE_DIR('.env')) | |
314 | env.read_env(os.path.join(BASE_DIR, '.env')) | |
315 | env.read_env(pathlib.Path(str(BASE_DIR)).joinpath('.env')) | |
316 | env.read_env(pathlib.Path(str(BASE_DIR)) / '.env') | |
317 | ||
318 | ||
319 | Overwriting existing environment values from env files | |
320 | ------------------------------------------------------ | |
321 | ||
322 | If you want variables set within your env files to take higher precedence than | |
323 | an existing set environment variable, use the ``overwrite=True`` argument of | |
324 | :meth:`.environ.Env.read_env`. For example: | |
325 | ||
326 | .. code-block:: python | |
327 | ||
328 | env = environ.Env() | |
329 | env.read_env(BASE_DIR('.env'), overwrite=True) | |
330 | ||
331 | ||
332 | Handling prefixes | |
333 | ================= | |
334 | ||
335 | Sometimes it is desirable to be able to prefix all environment variables. For | |
336 | example, if you are using Django, you may want to prefix all environment | |
337 | variables with ``DJANGO_``. This can be done by setting the ``prefix`` | |
338 | to desired prefix. For example: | |
339 | ||
340 | **.env file**: | |
341 | ||
342 | .. code-block:: shell | |
343 | ||
344 | # .env file contents | |
345 | DJANGO_TEST="foo" | |
346 | ||
347 | **settings.py file**: | |
348 | ||
349 | .. code-block:: python | |
350 | ||
351 | # settings.py file contents | |
352 | import environ | |
353 | ||
354 | ||
355 | env = environ.Env() | |
356 | env.prefix = 'DJANGO_' | |
357 | ||
358 | env.str('TEST') # foo |
0 | =============== | |
1 | Supported types | |
2 | =============== | |
3 | ||
4 | The following are all type-casting methods of :py:class:`.environ.Env`. | |
5 | ||
6 | * :py:meth:`~.environ.Env.str` | |
7 | * :py:meth:`~.environ.Env.bool` | |
8 | * :py:meth:`~.environ.Env.int` | |
9 | * :py:meth:`~.environ.Env.float` | |
10 | * :py:meth:`~.environ.Env.json` | |
11 | * :py:meth:`~.environ.Env.url` | |
12 | * :py:meth:`~.environ.Env.list`: (accepts values like ``(FOO=a,b,c)``) | |
13 | * :py:meth:`~.environ.Env.tuple`: (accepts values like ``(FOO=(a,b,c))``) | |
14 | * :py:meth:`~.environ.Env.path`: (accepts values like ``(environ.Path)``) | |
15 | * :py:meth:`~.environ.Env.dict`: (see below, ":ref:`environ-env-dict`" section) | |
16 | * :py:meth:`~.environ.Env.db_url` (see below, ":ref:`environ-env-db-url`" section) | |
17 | * :py:meth:`~.environ.Env.cache_url` (see below, ":ref:`environ-env-cache-url`" section) | |
18 | * :py:meth:`~.environ.Env.search_url` (see below, ":ref:`environ-env-search-url`" section) | |
19 | * :py:meth:`~.environ.Env.email_url` (see below, ":ref:`environ-env-email-url`" section) | |
20 | ||
21 | ||
22 | .. _environ-env-dict: | |
23 | ||
24 | ``environ.Env.dict`` | |
25 | ====================== | |
26 | ||
27 | :py:class:`.environ.Env` may parse complex variables like with the complex type-casting. | |
28 | For example: | |
29 | ||
30 | .. code-block:: python | |
31 | ||
32 | import environ | |
33 | ||
34 | ||
35 | env = environ.Env() | |
36 | ||
37 | # {'key': 'val', 'foo': 'bar'} | |
38 | env.parse_value('key=val,foo=bar', dict) | |
39 | ||
40 | # {'key': 'val', 'foo': 1.1, 'baz': True} | |
41 | env.parse_value( | |
42 | 'key=val;foo=1.1;baz=True', | |
43 | dict(value=str, cast=dict(foo=float,baz=bool)) | |
44 | ) | |
45 | ||
46 | For more detailed example see ":ref:`complex_dict_format`". | |
47 | ||
48 | ||
49 | .. _environ-env-db-url: | |
50 | ||
51 | ``environ.Env.db_url`` | |
52 | ====================== | |
53 | ||
54 | :py:meth:`~.environ.Env.db_url` supports the following URL schemas: | |
55 | ||
56 | .. glossary:: | |
57 | ||
58 | Amazon Redshift | |
59 | **Database Backend:** ``django_redshift_backend`` | |
60 | ||
61 | **URL schema:** ``redshift://`` | |
62 | ||
63 | LDAP | |
64 | **Database Backend:** ``ldapdb.backends.ldap`` | |
65 | ||
66 | **URL schema:** ``ldap://host:port/dn?attrs?scope?filter?exts`` | |
67 | ||
68 | MSSQL | |
69 | **Database Backend:** ``sql_server.pyodbc`` | |
70 | ||
71 | **URL schema:** ``mssql://user:password@host:port/dbname`` | |
72 | ||
73 | With MySQL you can use the following schemas: ``mysql``, ``mysql2``. | |
74 | ||
75 | MySQL (GIS) | |
76 | **Database Backend:** ``django.contrib.gis.db.backends.mysql`` | |
77 | ||
78 | **URL schema:** ``mysqlgis://user:password@host:port/dbname`` | |
79 | ||
80 | MySQL | |
81 | **Database Backend:** ``django.db.backends.mysql`` | |
82 | ||
83 | **URL schema:** ``mysql://user:password@host:port/dbname`` | |
84 | ||
85 | MySQL Connector Python from Oracle | |
86 | **Database Backend:** ``mysql.connector.django`` | |
87 | ||
88 | **URL schema:** ``mysql-connector://`` | |
89 | ||
90 | Oracle | |
91 | **Database Backend:** ``django.db.backends.oracle`` | |
92 | ||
93 | **URL schema:** ``oracle://user:password@host:port/dbname`` | |
94 | ||
95 | PostgreSQL | |
96 | **Database Backend:** ``django.db.backends.postgresql`` | |
97 | ||
98 | **URL schema:** ``postgres://user:password@host:port/dbname`` | |
99 | ||
100 | With PostgreSQL you can use the following schemas: ``postgres``, ``postgresql``, ``psql``, ``pgsql``, ``postgis``. | |
101 | You can also use UNIX domain sockets path instead of hostname. For example: ``postgres://path/dbname``. | |
102 | The ``django.db.backends.postgresql_psycopg2`` will be used if the Django version is less than ``2.0``. | |
103 | ||
104 | PostGIS | |
105 | **Database Backend:** ``django.contrib.gis.db.backends.postgis`` | |
106 | ||
107 | **URL schema:** ``postgis://user:password@host:port/dbname`` | |
108 | ||
109 | PyODBC | |
110 | **Database Backend:** ``sql_server.pyodbc`` | |
111 | ||
112 | **URL schema:** ``pyodbc://`` | |
113 | ||
114 | SQLite | |
115 | **Database Backend:** ``django.db.backends.sqlite3`` | |
116 | ||
117 | **URL schema:** ``sqlite:////absolute/path/to/db/file`` | |
118 | ||
119 | SQLite connects to file based databases. URL schemas ``sqlite://`` or | |
120 | ``sqlite://:memory:`` means the database is in the memory (not a file on disk). | |
121 | ||
122 | SpatiaLite | |
123 | **Database Backend:** ``django.contrib.gis.db.backends.spatialite`` | |
124 | ||
125 | **URL schema:** ``spatialite:///PATH`` | |
126 | ||
127 | SQLite connects to file based databases. URL schemas ``sqlite://`` or | |
128 | ``sqlite://:memory:`` means the database is in the memory (not a file on disk). | |
129 | ||
130 | ||
131 | .. _environ-env-cache-url: | |
132 | ||
133 | ``environ.Env.cache_url`` | |
134 | ========================= | |
135 | ||
136 | :py:meth:`~.environ.Env.cache_url` supports the following URL schemas: | |
137 | ||
138 | * Database: ``dbcache://`` | |
139 | * Dummy: ``dummycache://`` | |
140 | * File: ``filecache://`` | |
141 | * Memory: ``locmemcache://`` | |
142 | * Memcached: | |
143 | ||
144 | * ``memcache://`` (uses ``python-memcached`` backend, deprecated in Django 3.2) | |
145 | * ``pymemcache://`` (uses ``pymemcache`` backend if Django >=3.2 and package is installed, otherwise will use ``pylibmc`` backend to keep config backwards compatibility) | |
146 | * ``pylibmc://`` | |
147 | ||
148 | * Redis: ``rediscache://``, ``redis://``, or ``rediss://`` | |
149 | ||
150 | ||
151 | .. _environ-env-search-url: | |
152 | ||
153 | ``environ.Env.search_url`` | |
154 | ========================== | |
155 | ||
156 | :py:meth:`~.environ.Env.search_url` supports the following URL schemas: | |
157 | ||
158 | * Elasticsearch: ``elasticsearch://`` | |
159 | * Elasticsearch2: ``elasticsearch2://`` | |
160 | * Elasticsearch5: ``elasticsearch5://`` | |
161 | * Elasticsearch7: ``elasticsearch7://`` | |
162 | * Solr: ``solr://`` | |
163 | * Whoosh: ``whoosh://`` | |
164 | * Xapian: ``xapian://`` | |
165 | * Simple cache: ``simple://`` | |
166 | ||
167 | ||
168 | .. _environ-env-email-url: | |
169 | ||
170 | ``environ.Env.email_url`` | |
171 | ========================== | |
172 | ||
173 | :py:meth:`~.environ.Env.email_url` supports the following URL schemas: | |
174 | ||
175 | * SMTP: ``smtp://`` | |
176 | * SMTP+SSL: ``smtp+ssl://`` | |
177 | * SMTP+TLS: ``smtp+tls://`` | |
178 | * Console mail: ``consolemail://`` | |
179 | * File mail: ``filemail://`` | |
180 | * LocMem mail: ``memorymail://`` | |
181 | * Dummy mail: ``dummymail://`` |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | """The top-level module for django-environ package. | |
9 | ||
10 | This module tracks the version of the package as well as the base | |
11 | package info used by various functions within django-environ. | |
12 | ||
13 | Refer to the `documentation <https://django-environ.readthedocs.io/en/latest/>`_ | |
14 | for details on the use of this package. | |
15 | """ # noqa: E501 | |
16 | ||
0 | 17 | from .environ import * |
18 | ||
19 | ||
20 | __copyright__ = 'Copyright (C) 2021 Daniele Faraglia' | |
21 | """The copyright notice of the package.""" | |
22 | ||
23 | __version__ = '0.9.0' | |
24 | """The version of the package.""" | |
25 | ||
26 | __license__ = 'MIT' | |
27 | """The license of the package.""" | |
28 | ||
29 | __author__ = 'Daniele Faraglia' | |
30 | """The author of the package.""" | |
31 | ||
32 | __author_email__ = 'daniele.faraglia@gmail.com' | |
33 | """The email of the author of the package.""" | |
34 | ||
35 | __maintainer__ = 'Serghei Iakovlev' | |
36 | """The maintainer of the package.""" | |
37 | ||
38 | __maintainer_email__ = 'egrep@protonmail.ch' | |
39 | """The email of the maintainer of the package.""" | |
40 | ||
41 | __url__ = 'https://django-environ.readthedocs.org' | |
42 | """The URL of the package.""" | |
43 | ||
44 | __description__ = 'A package that allows you to utilize 12factor inspired environment variables to configure your Django application.' # noqa: E501 | |
45 | """The description of the package.""" |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | """This module handles import compatibility issues.""" | |
9 | ||
10 | from pkgutil import find_loader | |
11 | ||
12 | ||
13 | if find_loader('simplejson'): | |
14 | import simplejson as json | |
15 | else: | |
16 | import json | |
17 | ||
18 | if find_loader('django'): | |
19 | from django import VERSION as DJANGO_VERSION | |
20 | from django.core.exceptions import ImproperlyConfigured | |
21 | else: | |
22 | DJANGO_VERSION = None | |
23 | ||
24 | class ImproperlyConfigured(Exception): | |
25 | pass | |
26 | ||
27 | # back compatibility with django postgresql package | |
28 | if DJANGO_VERSION is not None and DJANGO_VERSION < (2, 0): | |
29 | DJANGO_POSTGRES = 'django.db.backends.postgresql_psycopg2' | |
30 | else: | |
31 | # https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 | |
32 | DJANGO_POSTGRES = 'django.db.backends.postgresql' | |
33 | ||
34 | # back compatibility with redis_cache package | |
35 | if find_loader('redis_cache'): | |
36 | REDIS_DRIVER = 'redis_cache.RedisCache' | |
37 | else: | |
38 | REDIS_DRIVER = 'django_redis.cache.RedisCache' | |
39 | ||
40 | ||
41 | def choose_pymemcache_driver(): | |
42 | """Backward compatibility for pymemcache.""" | |
43 | old_django = DJANGO_VERSION is not None and DJANGO_VERSION < (3, 2) | |
44 | if old_django or not find_loader('pymemcache'): | |
45 | # The original backend choice for the 'pymemcache' scheme is | |
46 | # unfortunately 'pylibmc'. | |
47 | return 'django.core.cache.backends.memcached.PyLibMCCache' | |
48 | return 'django.core.cache.backends.memcached.PyMemcacheCache' | |
49 | ||
50 | ||
51 | PYMEMCACHE_DRIVER = choose_pymemcache_driver() |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
0 | 8 | """ |
1 | 9 | Django-environ allows you to utilize 12factor inspired environment |
2 | 10 | variables to configure your Django application. |
3 | 11 | """ |
4 | import json | |
12 | ||
13 | import ast | |
14 | import itertools | |
5 | 15 | import logging |
6 | 16 | import os |
7 | 17 | import re |
8 | 18 | import sys |
19 | import urllib.parse as urlparselib | |
9 | 20 | import warnings |
21 | from urllib.parse import ( | |
22 | parse_qs, | |
23 | ParseResult, | |
24 | unquote, | |
25 | unquote_plus, | |
26 | urlparse, | |
27 | urlunparse, | |
28 | ) | |
29 | ||
30 | from .compat import ( | |
31 | DJANGO_POSTGRES, | |
32 | ImproperlyConfigured, | |
33 | json, | |
34 | PYMEMCACHE_DRIVER, | |
35 | REDIS_DRIVER, | |
36 | ) | |
37 | from .fileaware_mapping import FileAwareMapping | |
10 | 38 | |
11 | 39 | try: |
12 | from django.core.exceptions import ImproperlyConfigured | |
13 | except ImportError: | |
14 | class ImproperlyConfigured(Exception): | |
15 | pass | |
16 | ||
17 | from six.moves import urllib | |
18 | from six import string_types | |
40 | from os import PathLike | |
41 | except ImportError: # Python 3.5 support | |
42 | from pathlib import PurePath as PathLike | |
43 | ||
44 | Openable = (str, PathLike) | |
19 | 45 | |
20 | 46 | logger = logging.getLogger(__name__) |
21 | 47 | |
22 | 48 | |
23 | VERSION = '0.4.4' | |
24 | __author__ = 'joke2k' | |
25 | __version__ = tuple(VERSION.split('.')) | |
26 | ||
27 | ||
28 | # return int if possible | |
49 | def _cast(value): | |
50 | # Safely evaluate an expression node or a string containing a Python | |
51 | # literal or container display. | |
52 | # https://docs.python.org/3/library/ast.html#ast.literal_eval | |
53 | try: | |
54 | return ast.literal_eval(value) | |
55 | except (ValueError, SyntaxError): | |
56 | return value | |
57 | ||
58 | ||
29 | 59 | def _cast_int(v): |
60 | """Return int if possible.""" | |
30 | 61 | return int(v) if hasattr(v, 'isdigit') and v.isdigit() else v |
31 | 62 | |
63 | ||
32 | 64 | def _cast_urlstr(v): |
33 | return urllib.parse.unquote_plus(v) if isinstance(v, str) else v | |
34 | ||
35 | # back compatibility with redis_cache package | |
36 | DJANGO_REDIS_DRIVER = 'django_redis.cache.RedisCache' | |
37 | DJANGO_REDIS_CACHE_DRIVER = 'redis_cache.RedisCache' | |
38 | ||
39 | REDIS_DRIVER = DJANGO_REDIS_DRIVER | |
40 | try: | |
41 | import redis_cache | |
42 | REDIS_DRIVER = DJANGO_REDIS_CACHE_DRIVER | |
43 | except: | |
44 | pass | |
45 | ||
46 | ||
47 | class NoValue(object): | |
65 | return unquote(v) if isinstance(v, str) else v | |
66 | ||
67 | ||
68 | class NoValue: | |
48 | 69 | |
49 | 70 | def __repr__(self): |
50 | return '<{0}>'.format(self.__class__.__name__) | |
51 | ||
52 | ||
53 | class Env(object): | |
54 | ||
71 | return '<{}>'.format(self.__class__.__name__) | |
72 | ||
73 | ||
74 | class Env: | |
55 | 75 | """Provide scheme-based lookups of environment variables so that each |
56 | caller doesn't have to pass in `cast` and `default` parameters. | |
76 | caller doesn't have to pass in ``cast`` and ``default`` parameters. | |
57 | 77 | |
58 | 78 | Usage::: |
59 | 79 | |
60 | env = Env(MAIL_ENABLED=bool, SMTP_LOGIN=(str, 'DEFAULT')) | |
61 | if env('MAIL_ENABLED'): | |
62 | ... | |
80 | import environ | |
81 | import os | |
82 | ||
83 | env = environ.Env( | |
84 | # set casting, default value | |
85 | MAIL_ENABLED=(bool, False), | |
86 | SMTP_LOGIN=(str, 'DEFAULT') | |
87 | ) | |
88 | ||
89 | # Set the project base directory | |
90 | BASE_DIR = os.path.dirname( | |
91 | os.path.dirname(os.path.abspath(__file__)) | |
92 | ) | |
93 | ||
94 | # Take environment variables from .env file | |
95 | environ.Env.read_env(os.path.join(BASE_DIR, '.env')) | |
96 | ||
97 | # False if not in os.environ due to casting above | |
98 | MAIL_ENABLED = env('MAIL_ENABLED') | |
99 | ||
100 | # 'DEFAULT' if not in os.environ due to casting above | |
101 | SMTP_LOGIN = env('SMTP_LOGIN') | |
63 | 102 | """ |
64 | 103 | |
65 | 104 | ENVIRON = os.environ |
66 | 105 | NOTSET = NoValue() |
67 | 106 | BOOLEAN_TRUE_STRINGS = ('true', 'on', 'ok', 'y', 'yes', '1') |
68 | URL_CLASS = urllib.parse.ParseResult | |
107 | URL_CLASS = ParseResult | |
108 | ||
109 | POSTGRES_FAMILY = ['postgres', 'postgresql', 'psql', 'pgsql', 'postgis'] | |
110 | ELASTICSEARCH_FAMILY = ['elasticsearch' + x for x in ['', '2', '5', '7']] | |
111 | ||
69 | 112 | DEFAULT_DATABASE_ENV = 'DATABASE_URL' |
70 | 113 | DB_SCHEMES = { |
71 | 'postgres': 'django.db.backends.postgresql_psycopg2', | |
72 | 'postgresql': 'django.db.backends.postgresql_psycopg2', | |
73 | 'psql': 'django.db.backends.postgresql_psycopg2', | |
74 | 'pgsql': 'django.db.backends.postgresql_psycopg2', | |
114 | 'postgres': DJANGO_POSTGRES, | |
115 | 'postgresql': DJANGO_POSTGRES, | |
116 | 'psql': DJANGO_POSTGRES, | |
117 | 'pgsql': DJANGO_POSTGRES, | |
75 | 118 | 'postgis': 'django.contrib.gis.db.backends.postgis', |
76 | 119 | 'mysql': 'django.db.backends.mysql', |
77 | 120 | 'mysql2': 'django.db.backends.mysql', |
78 | 121 | 'mysql-connector': 'mysql.connector.django', |
79 | 122 | 'mysqlgis': 'django.contrib.gis.db.backends.mysql', |
123 | 'mssql': 'sql_server.pyodbc', | |
80 | 124 | 'oracle': 'django.db.backends.oracle', |
81 | 125 | 'pyodbc': 'sql_server.pyodbc', |
82 | 126 | 'redshift': 'django_redshift_backend', |
84 | 128 | 'sqlite': 'django.db.backends.sqlite3', |
85 | 129 | 'ldap': 'ldapdb.backends.ldap', |
86 | 130 | } |
87 | _DB_BASE_OPTIONS = ['CONN_MAX_AGE', 'ATOMIC_REQUESTS', 'AUTOCOMMIT'] | |
131 | _DB_BASE_OPTIONS = [ | |
132 | 'CONN_MAX_AGE', | |
133 | 'ATOMIC_REQUESTS', | |
134 | 'AUTOCOMMIT', | |
135 | 'DISABLE_SERVER_SIDE_CURSORS', | |
136 | ] | |
88 | 137 | |
89 | 138 | DEFAULT_CACHE_ENV = 'CACHE_URL' |
90 | 139 | CACHE_SCHEMES = { |
93 | 142 | 'filecache': 'django.core.cache.backends.filebased.FileBasedCache', |
94 | 143 | 'locmemcache': 'django.core.cache.backends.locmem.LocMemCache', |
95 | 144 | 'memcache': 'django.core.cache.backends.memcached.MemcachedCache', |
96 | 'pymemcache': 'django.core.cache.backends.memcached.PyLibMCCache', | |
145 | 'pymemcache': PYMEMCACHE_DRIVER, | |
146 | 'pylibmc': 'django.core.cache.backends.memcached.PyLibMCCache', | |
97 | 147 | 'rediscache': REDIS_DRIVER, |
98 | 148 | 'redis': REDIS_DRIVER, |
149 | 'rediss': REDIS_DRIVER, | |
99 | 150 | } |
100 | _CACHE_BASE_OPTIONS = ['TIMEOUT', 'KEY_PREFIX', 'VERSION', 'KEY_FUNCTION', 'BINARY'] | |
151 | _CACHE_BASE_OPTIONS = [ | |
152 | 'TIMEOUT', | |
153 | 'KEY_PREFIX', | |
154 | 'VERSION', | |
155 | 'KEY_FUNCTION', | |
156 | 'BINARY', | |
157 | ] | |
101 | 158 | |
102 | 159 | DEFAULT_EMAIL_ENV = 'EMAIL_URL' |
103 | 160 | EMAIL_SCHEMES = { |
114 | 171 | |
115 | 172 | DEFAULT_SEARCH_ENV = 'SEARCH_URL' |
116 | 173 | SEARCH_SCHEMES = { |
117 | "elasticsearch": "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine", | |
118 | "elasticsearch2": "haystack.backends.elasticsearch2_backend.Elasticsearch2SearchEngine", | |
174 | "elasticsearch": "haystack.backends.elasticsearch_backend." | |
175 | "ElasticsearchSearchEngine", | |
176 | "elasticsearch2": "haystack.backends.elasticsearch2_backend." | |
177 | "Elasticsearch2SearchEngine", | |
178 | "elasticsearch5": "haystack.backends.elasticsearch5_backend." | |
179 | "Elasticsearch5SearchEngine", | |
180 | "elasticsearch7": "haystack.backends.elasticsearch7_backend." | |
181 | "Elasticsearch7SearchEngine", | |
119 | 182 | "solr": "haystack.backends.solr_backend.SolrEngine", |
120 | 183 | "whoosh": "haystack.backends.whoosh_backend.WhooshEngine", |
121 | 184 | "xapian": "haystack.backends.xapian_backend.XapianEngine", |
122 | 185 | "simple": "haystack.backends.simple_backend.SimpleEngine", |
123 | 186 | } |
187 | CLOUDSQL = 'cloudsql' | |
124 | 188 | |
125 | 189 | def __init__(self, **scheme): |
190 | self.smart_cast = True | |
191 | self.escape_proxy = False | |
192 | self.prefix = "" | |
126 | 193 | self.scheme = scheme |
127 | 194 | |
128 | 195 | def __call__(self, var, cast=None, default=NOTSET, parse_default=False): |
129 | return self.get_value(var, cast=cast, default=default, parse_default=parse_default) | |
196 | return self.get_value( | |
197 | var, | |
198 | cast=cast, | |
199 | default=default, | |
200 | parse_default=parse_default | |
201 | ) | |
130 | 202 | |
131 | 203 | def __contains__(self, var): |
132 | 204 | return var in self.ENVIRON |
133 | 205 | |
134 | 206 | # Shortcuts |
135 | 207 | |
136 | def str(self, var, default=NOTSET): | |
208 | def str(self, var, default=NOTSET, multiline=False): | |
137 | 209 | """ |
138 | 210 | :rtype: str |
139 | 211 | """ |
140 | return self.get_value(var, default=default) | |
212 | value = self.get_value(var, cast=str, default=default) | |
213 | if multiline: | |
214 | return re.sub(r'(\\r)?\\n', r'\n', value) | |
215 | return value | |
141 | 216 | |
142 | 217 | def unicode(self, var, default=NOTSET): |
143 | 218 | """Helper for python2 |
144 | 219 | :rtype: unicode |
145 | 220 | """ |
221 | warnings.warn( | |
222 | '`%s.unicode` is deprecated, use `%s.str` instead' % ( | |
223 | self.__class__.__name__, | |
224 | self.__class__.__name__, | |
225 | ), | |
226 | DeprecationWarning, | |
227 | stacklevel=2 | |
228 | ) | |
229 | ||
146 | 230 | return self.get_value(var, cast=str, default=default) |
147 | 231 | |
232 | def bytes(self, var, default=NOTSET, encoding='utf8'): | |
233 | """ | |
234 | :rtype: bytes | |
235 | """ | |
236 | value = self.get_value(var, cast=str, default=default) | |
237 | if hasattr(value, 'encode'): | |
238 | return value.encode(encoding) | |
239 | return value | |
240 | ||
148 | 241 | def bool(self, var, default=NOTSET): |
149 | 242 | """ |
150 | 243 | :rtype: bool |
173 | 266 | """ |
174 | 267 | :rtype: list |
175 | 268 | """ |
176 | return self.get_value(var, cast=list if not cast else [cast], default=default) | |
269 | return self.get_value( | |
270 | var, | |
271 | cast=list if not cast else [cast], | |
272 | default=default | |
273 | ) | |
177 | 274 | |
178 | 275 | def tuple(self, var, cast=None, default=NOTSET): |
179 | 276 | """ |
180 | 277 | :rtype: tuple |
181 | 278 | """ |
182 | return self.get_value(var, cast=tuple if not cast else (cast,), default=default) | |
279 | return self.get_value( | |
280 | var, | |
281 | cast=tuple if not cast else (cast,), | |
282 | default=default | |
283 | ) | |
183 | 284 | |
184 | 285 | def dict(self, var, cast=dict, default=NOTSET): |
185 | 286 | """ |
189 | 290 | |
190 | 291 | def url(self, var, default=NOTSET): |
191 | 292 | """ |
192 | :rtype: urlparse.ParseResult | |
193 | """ | |
194 | return self.get_value(var, cast=urllib.parse.urlparse, default=default, parse_default=True) | |
293 | :rtype: urllib.parse.ParseResult | |
294 | """ | |
295 | return self.get_value( | |
296 | var, | |
297 | cast=urlparse, | |
298 | default=default, | |
299 | parse_default=True | |
300 | ) | |
195 | 301 | |
196 | 302 | def db_url(self, var=DEFAULT_DATABASE_ENV, default=NOTSET, engine=None): |
197 | 303 | """Returns a config dictionary, defaulting to DATABASE_URL. |
198 | 304 | |
305 | The db method is an alias for db_url. | |
306 | ||
199 | 307 | :rtype: dict |
200 | 308 | """ |
201 | return self.db_url_config(self.get_value(var, default=default), engine=engine) | |
309 | return self.db_url_config( | |
310 | self.get_value(var, default=default), | |
311 | engine=engine | |
312 | ) | |
313 | ||
202 | 314 | db = db_url |
203 | 315 | |
204 | 316 | def cache_url(self, var=DEFAULT_CACHE_ENV, default=NOTSET, backend=None): |
205 | 317 | """Returns a config dictionary, defaulting to CACHE_URL. |
206 | 318 | |
319 | The cache method is an alias for cache_url. | |
320 | ||
207 | 321 | :rtype: dict |
208 | 322 | """ |
209 | return self.cache_url_config(self.url(var, default=default), backend=backend) | |
323 | return self.cache_url_config( | |
324 | self.url(var, default=default), | |
325 | backend=backend | |
326 | ) | |
327 | ||
210 | 328 | cache = cache_url |
211 | 329 | |
212 | 330 | def email_url(self, var=DEFAULT_EMAIL_ENV, default=NOTSET, backend=None): |
213 | 331 | """Returns a config dictionary, defaulting to EMAIL_URL. |
214 | 332 | |
333 | The email method is an alias for email_url. | |
334 | ||
215 | 335 | :rtype: dict |
216 | 336 | """ |
217 | return self.email_url_config(self.url(var, default=default), backend=backend) | |
337 | return self.email_url_config( | |
338 | self.url(var, default=default), | |
339 | backend=backend | |
340 | ) | |
341 | ||
218 | 342 | email = email_url |
219 | 343 | |
220 | 344 | def search_url(self, var=DEFAULT_SEARCH_ENV, default=NOTSET, engine=None): |
222 | 346 | |
223 | 347 | :rtype: dict |
224 | 348 | """ |
225 | return self.search_url_config(self.url(var, default=default), engine=engine) | |
349 | return self.search_url_config( | |
350 | self.url(var, default=default), | |
351 | engine=engine | |
352 | ) | |
226 | 353 | |
227 | 354 | def path(self, var, default=NOTSET, **kwargs): |
228 | 355 | """ |
233 | 360 | def get_value(self, var, cast=None, default=NOTSET, parse_default=False): |
234 | 361 | """Return value for given environment variable. |
235 | 362 | |
236 | :param var: Name of variable. | |
237 | :param cast: Type to cast return value as. | |
238 | :param default: If var not present in environ, return this instead. | |
239 | :param parse_default: force to parse default.. | |
240 | ||
241 | :returns: Value from environment or default (if set) | |
242 | """ | |
243 | ||
244 | logger.debug("get '{0}' casted as '{1}' with default '{2}'".format( | |
363 | :param str var: | |
364 | Name of variable. | |
365 | :param collections.abc.Callable or None cast: | |
366 | Type to cast return value as. | |
367 | :param default: | |
368 | If var not present in environ, return this instead. | |
369 | :param bool parse_default: | |
370 | Force to parse default. | |
371 | :returns: Value from environment or default (if set). | |
372 | :rtype: typing.IO[typing.Any] | |
373 | """ | |
374 | ||
375 | logger.debug("get '{}' casted as '{}' with default '{}'".format( | |
245 | 376 | var, cast, default |
246 | 377 | )) |
247 | 378 | |
248 | if var in self.scheme: | |
249 | var_info = self.scheme[var] | |
379 | var_name = "{}{}".format(self.prefix, var) | |
380 | if var_name in self.scheme: | |
381 | var_info = self.scheme[var_name] | |
250 | 382 | |
251 | 383 | try: |
252 | 384 | has_default = len(var_info) == 2 |
267 | 399 | cast = var_info |
268 | 400 | |
269 | 401 | try: |
270 | value = self.ENVIRON[var] | |
271 | except KeyError: | |
402 | value = self.ENVIRON[var_name] | |
403 | except KeyError as exc: | |
272 | 404 | if default is self.NOTSET: |
273 | error_msg = "Set the {0} environment variable".format(var) | |
274 | raise ImproperlyConfigured(error_msg) | |
405 | error_msg = "Set the {} environment variable".format(var) | |
406 | raise ImproperlyConfigured(error_msg) from exc | |
275 | 407 | |
276 | 408 | value = default |
277 | 409 | |
278 | 410 | # Resolve any proxied values |
279 | if hasattr(value, 'startswith') and value.startswith('$'): | |
280 | value = value.lstrip('$') | |
411 | prefix = b'$' if isinstance(value, bytes) else '$' | |
412 | escape = rb'\$' if isinstance(value, bytes) else r'\$' | |
413 | if hasattr(value, 'startswith') and value.startswith(prefix): | |
414 | value = value.lstrip(prefix) | |
281 | 415 | value = self.get_value(value, cast=cast, default=default) |
416 | ||
417 | if self.escape_proxy and hasattr(value, 'replace'): | |
418 | value = value.replace(escape, prefix) | |
419 | ||
420 | # Smart casting | |
421 | if self.smart_cast: | |
422 | if cast is None and default is not None and \ | |
423 | not isinstance(default, NoValue): | |
424 | cast = type(default) | |
425 | ||
426 | value = None if default is None and value == '' else value | |
282 | 427 | |
283 | 428 | if value != default or (parse_default and value): |
284 | 429 | value = self.parse_value(value, cast) |
315 | 460 | value = dict(map( |
316 | 461 | lambda kv: ( |
317 | 462 | key_cast(kv[0]), |
318 | cls.parse_value(kv[1], value_cast_by_key.get(kv[0], value_cast)) | |
463 | cls.parse_value( | |
464 | kv[1], | |
465 | value_cast_by_key.get(kv[0], value_cast) | |
466 | ) | |
319 | 467 | ), |
320 | 468 | [val.split('=') for val in value.split(';') if val] |
321 | 469 | )) |
328 | 476 | value = tuple([x for x in val if x]) |
329 | 477 | elif cast is float: |
330 | 478 | # clean string |
331 | float_str = re.sub(r'[^\d,\.]', '', value) | |
332 | # split for avoid thousand separator and different locale comma/dot symbol | |
333 | parts = re.split(r'[,\.]', float_str) | |
479 | float_str = re.sub(r'[^\d,.-]', '', value) | |
480 | # split for avoid thousand separator and different | |
481 | # locale comma/dot symbol | |
482 | parts = re.split(r'[,.]', float_str) | |
334 | 483 | if len(parts) == 1: |
335 | 484 | float_str = parts[0] |
336 | 485 | else: |
337 | float_str = "{0}.{1}".format(''.join(parts[0:-1]), parts[-1]) | |
486 | float_str = "{}.{}".format(''.join(parts[0:-1]), parts[-1]) | |
338 | 487 | value = float(float_str) |
339 | 488 | else: |
340 | 489 | value = cast(value) |
342 | 491 | |
343 | 492 | @classmethod |
344 | 493 | def db_url_config(cls, url, engine=None): |
345 | """Pulled from DJ-Database-URL, parse an arbitrary Database URL. | |
346 | Support currently exists for PostgreSQL, PostGIS, MySQL, Oracle and SQLite. | |
347 | ||
348 | SQLite connects to file based databases. The same URL format is used, omitting the hostname, | |
349 | and using the "file" portion as the filename of the database. | |
350 | This has the effect of four slashes being present for an absolute file path: | |
351 | ||
352 | >>> from environ import Env | |
353 | >>> Env.db_url_config('sqlite:////full/path/to/your/file.sqlite') | |
354 | {'ENGINE': 'django.db.backends.sqlite3', 'HOST': '', 'NAME': '/full/path/to/your/file.sqlite', 'PASSWORD': '', 'PORT': '', 'USER': ''} | |
355 | >>> Env.db_url_config('postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn') | |
356 | {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'HOST': 'ec2-107-21-253-135.compute-1.amazonaws.com', 'NAME': 'd8r82722r2kuvn', 'PASSWORD': 'wegauwhgeuioweg', 'PORT': 5431, 'USER': 'uf07k1i6d8ia0v'} | |
357 | ||
494 | """Parse an arbitrary database URL. | |
495 | ||
496 | Supports the following URL schemas: | |
497 | ||
498 | * PostgreSQL: ``postgres[ql]?://`` or ``p[g]?sql://`` | |
499 | * PostGIS: ``postgis://`` | |
500 | * MySQL: ``mysql://`` or ``mysql2://`` | |
501 | * MySQL (GIS): ``mysqlgis://`` | |
502 | * MySQL Connector Python from Oracle: ``mysql-connector://`` | |
503 | * SQLite: ``sqlite://`` | |
504 | * SQLite with SpatiaLite for GeoDjango: ``spatialite://`` | |
505 | * Oracle: ``oracle://`` | |
506 | * Microsoft SQL Server: ``mssql://`` | |
507 | * PyODBC: ``pyodbc://`` | |
508 | * Amazon Redshift: ``redshift://`` | |
509 | * LDAP: ``ldap://`` | |
510 | ||
511 | :param urllib.parse.ParseResult or str url: | |
512 | Database URL to parse. | |
513 | :param str or None engine: | |
514 | If None, the database engine is evaluates from the ``url``. | |
515 | :return: Parsed database URL. | |
516 | :rtype: dict | |
358 | 517 | """ |
359 | 518 | if not isinstance(url, cls.URL_CLASS): |
360 | 519 | if url == 'sqlite://:memory:': |
366 | 525 | 'NAME': ':memory:' |
367 | 526 | } |
368 | 527 | # note: no other settings are required for sqlite |
369 | url = urllib.parse.urlparse(url) | |
528 | url = urlparse(url) | |
370 | 529 | |
371 | 530 | config = {} |
372 | 531 | |
373 | 532 | # Remove query strings. |
374 | 533 | path = url.path[1:] |
375 | path = urllib.parse.unquote_plus(path.split('?', 2)[0]) | |
376 | ||
377 | # if we are using sqlite and we have no path, then assume we | |
378 | # want an in-memory database (this is the behaviour of sqlalchemy) | |
379 | if url.scheme == 'sqlite' and path == '': | |
380 | path = ':memory:' | |
534 | path = unquote_plus(path.split('?', 2)[0]) | |
535 | ||
536 | if url.scheme == 'sqlite': | |
537 | if path == '': | |
538 | # if we are using sqlite and we have no path, then assume we | |
539 | # want an in-memory database (this is the behaviour of | |
540 | # sqlalchemy) | |
541 | path = ':memory:' | |
542 | if url.netloc: | |
543 | warnings.warn('SQLite URL contains host component %r, ' | |
544 | 'it will be ignored' % url.netloc, stacklevel=3) | |
381 | 545 | if url.scheme == 'ldap': |
382 | path = '{scheme}://{hostname}'.format(scheme=url.scheme, hostname=url.hostname) | |
546 | path = '{scheme}://{hostname}'.format( | |
547 | scheme=url.scheme, | |
548 | hostname=url.hostname, | |
549 | ) | |
383 | 550 | if url.port: |
384 | 551 | path += ':{port}'.format(port=url.port) |
552 | ||
553 | user_host = url.netloc.rsplit('@', 1) | |
554 | if url.scheme in cls.POSTGRES_FAMILY and ',' in user_host[-1]: | |
555 | # Parsing postgres cluster dsn | |
556 | hinfo = list( | |
557 | itertools.zip_longest( | |
558 | *( | |
559 | host.rsplit(':', 1) | |
560 | for host in user_host[-1].split(',') | |
561 | ) | |
562 | ) | |
563 | ) | |
564 | hostname = ','.join(hinfo[0]) | |
565 | port = ','.join(filter(None, hinfo[1])) if len(hinfo) == 2 else '' | |
566 | else: | |
567 | hostname = url.hostname | |
568 | port = url.port | |
385 | 569 | |
386 | 570 | # Update with environment configuration. |
387 | 571 | config.update({ |
388 | 572 | 'NAME': path or '', |
389 | 573 | 'USER': _cast_urlstr(url.username) or '', |
390 | 574 | 'PASSWORD': _cast_urlstr(url.password) or '', |
391 | 'HOST': url.hostname or '', | |
392 | 'PORT': _cast_int(url.port) or '', | |
575 | 'HOST': hostname or '', | |
576 | 'PORT': _cast_int(port) or '', | |
393 | 577 | }) |
578 | ||
579 | if ( | |
580 | url.scheme in cls.POSTGRES_FAMILY and path.startswith('/') | |
581 | or cls.CLOUDSQL in path and path.startswith('/') | |
582 | ): | |
583 | config['HOST'], config['NAME'] = path.rsplit('/', 1) | |
394 | 584 | |
395 | 585 | if url.scheme == 'oracle' and path == '': |
396 | 586 | config['NAME'] = config['HOST'] |
399 | 589 | if url.scheme == 'oracle': |
400 | 590 | # Django oracle/base.py strips port and fails on non-string value |
401 | 591 | if not config['PORT']: |
402 | del(config['PORT']) | |
592 | del (config['PORT']) | |
403 | 593 | else: |
404 | 594 | config['PORT'] = str(config['PORT']) |
405 | 595 | |
406 | 596 | if url.query: |
407 | 597 | config_options = {} |
408 | for k, v in urllib.parse.parse_qs(url.query).items(): | |
598 | for k, v in parse_qs(url.query).items(): | |
409 | 599 | if k.upper() in cls._DB_BASE_OPTIONS: |
410 | config.update({k.upper(): _cast_int(v[0])}) | |
600 | config.update({k.upper(): _cast(v[0])}) | |
411 | 601 | else: |
412 | 602 | config_options.update({k: _cast_int(v[0])}) |
413 | 603 | config['OPTIONS'] = config_options |
414 | 604 | |
415 | 605 | if engine: |
416 | 606 | config['ENGINE'] = engine |
417 | if url.scheme in Env.DB_SCHEMES: | |
418 | config['ENGINE'] = Env.DB_SCHEMES[url.scheme] | |
607 | else: | |
608 | config['ENGINE'] = url.scheme | |
609 | ||
610 | if config['ENGINE'] in Env.DB_SCHEMES: | |
611 | config['ENGINE'] = Env.DB_SCHEMES[config['ENGINE']] | |
419 | 612 | |
420 | 613 | if not config.get('ENGINE', False): |
421 | warnings.warn("Engine not recognized from url: {0}".format(config)) | |
614 | warnings.warn("Engine not recognized from url: {}".format(config)) | |
422 | 615 | return {} |
423 | 616 | |
424 | 617 | return config |
425 | 618 | |
426 | 619 | @classmethod |
427 | 620 | def cache_url_config(cls, url, backend=None): |
428 | """Pulled from DJ-Cache-URL, parse an arbitrary Cache URL. | |
429 | ||
430 | :param url: | |
431 | :param backend: | |
432 | :return: | |
433 | """ | |
434 | url = urllib.parse.urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
621 | """Parse an arbitrary cache URL. | |
622 | ||
623 | :param urllib.parse.ParseResult or str url: | |
624 | Cache URL to parse. | |
625 | :param str or None backend: | |
626 | If None, the backend is evaluates from the ``url``. | |
627 | :return: Parsed cache URL. | |
628 | :rtype: dict | |
629 | """ | |
630 | if not isinstance(url, cls.URL_CLASS): | |
631 | if not url: | |
632 | return {} | |
633 | else: | |
634 | url = urlparse(url) | |
635 | ||
636 | if url.scheme not in cls.CACHE_SCHEMES: | |
637 | raise ImproperlyConfigured( | |
638 | 'Invalid cache schema {}'.format(url.scheme) | |
639 | ) | |
435 | 640 | |
436 | 641 | location = url.netloc.split(',') |
437 | 642 | if len(location) == 1: |
448 | 653 | 'LOCATION': url.netloc + url.path, |
449 | 654 | }) |
450 | 655 | |
451 | if url.path and url.scheme in ['memcache', 'pymemcache']: | |
656 | # urlparse('pymemcache://127.0.0.1:11211') | |
657 | # => netloc='127.0.0.1:11211', path='' | |
658 | # | |
659 | # urlparse('pymemcache://memcached:11211/?key_prefix=ci') | |
660 | # => netloc='memcached:11211', path='/' | |
661 | # | |
662 | # urlparse('memcache:///tmp/memcached.sock') | |
663 | # => netloc='', path='/tmp/memcached.sock' | |
664 | if not url.netloc and url.scheme in ['memcache', 'pymemcache']: | |
452 | 665 | config.update({ |
453 | 666 | 'LOCATION': 'unix:' + url.path, |
454 | 667 | }) |
457 | 670 | scheme = url.scheme.replace('cache', '') |
458 | 671 | else: |
459 | 672 | scheme = 'unix' |
460 | locations = [scheme + '://' + loc + url.path for loc in url.netloc.split(',')] | |
461 | config['LOCATION'] = locations[0] if len(locations) == 1 else locations | |
673 | locations = [scheme + '://' + loc + url.path | |
674 | for loc in url.netloc.split(',')] | |
675 | if len(locations) == 1: | |
676 | config['LOCATION'] = locations[0] | |
677 | else: | |
678 | config['LOCATION'] = locations | |
462 | 679 | |
463 | 680 | if url.query: |
464 | 681 | config_options = {} |
465 | for k, v in urllib.parse.parse_qs(url.query).items(): | |
466 | opt = {k.upper(): _cast_int(v[0])} | |
682 | for k, v in parse_qs(url.query).items(): | |
683 | opt = {k.upper(): _cast(v[0])} | |
467 | 684 | if k.upper() in cls._CACHE_BASE_OPTIONS: |
468 | 685 | config.update(opt) |
469 | 686 | else: |
477 | 694 | |
478 | 695 | @classmethod |
479 | 696 | def email_url_config(cls, url, backend=None): |
480 | """Parses an email URL.""" | |
697 | """Parse an arbitrary email URL. | |
698 | ||
699 | :param urllib.parse.ParseResult or str url: | |
700 | Email URL to parse. | |
701 | :param str or None backend: | |
702 | If None, the backend is evaluates from the ``url``. | |
703 | :return: Parsed email URL. | |
704 | :rtype: dict | |
705 | """ | |
481 | 706 | |
482 | 707 | config = {} |
483 | 708 | |
484 | url = urllib.parse.urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
709 | url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
485 | 710 | |
486 | 711 | # Remove query strings |
487 | 712 | path = url.path[1:] |
488 | path = urllib.parse.unquote_plus(path.split('?', 2)[0]) | |
713 | path = unquote_plus(path.split('?', 2)[0]) | |
489 | 714 | |
490 | 715 | # Update with environment configuration |
491 | 716 | config.update({ |
510 | 735 | |
511 | 736 | if url.query: |
512 | 737 | config_options = {} |
513 | for k, v in urllib.parse.parse_qs(url.query).items(): | |
738 | for k, v in parse_qs(url.query).items(): | |
514 | 739 | opt = {k.upper(): _cast_int(v[0])} |
515 | 740 | if k.upper() in cls._EMAIL_BASE_OPTIONS: |
516 | 741 | config.update(opt) |
522 | 747 | |
523 | 748 | @classmethod |
524 | 749 | def search_url_config(cls, url, engine=None): |
750 | """Parse an arbitrary search URL. | |
751 | ||
752 | :param urllib.parse.ParseResult or str url: | |
753 | Search URL to parse. | |
754 | :param str or None engine: | |
755 | If None, the engine is evaluates from the ``url``. | |
756 | :return: Parsed search URL. | |
757 | :rtype: dict | |
758 | """ | |
759 | ||
525 | 760 | config = {} |
526 | 761 | |
527 | url = urllib.parse.urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
762 | url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
528 | 763 | |
529 | 764 | # Remove query strings. |
530 | 765 | path = url.path[1:] |
531 | path = urllib.parse.unquote_plus(path.split('?', 2)[0]) | |
766 | path = unquote_plus(path.split('?', 2)[0]) | |
532 | 767 | |
533 | 768 | if url.scheme not in cls.SEARCH_SCHEMES: |
534 | raise ImproperlyConfigured('Invalid search schema %s' % url.scheme) | |
769 | raise ImproperlyConfigured( | |
770 | 'Invalid search schema %s' % url.scheme | |
771 | ) | |
535 | 772 | config["ENGINE"] = cls.SEARCH_SCHEMES[url.scheme] |
536 | 773 | |
537 | 774 | # check commons params |
538 | params = {} | |
775 | params = {} # type: dict | |
539 | 776 | if url.query: |
540 | params = urllib.parse.parse_qs(url.query) | |
777 | params = parse_qs(url.query) | |
541 | 778 | if 'EXCLUDED_INDEXES' in params.keys(): |
542 | config['EXCLUDED_INDEXES'] = params['EXCLUDED_INDEXES'][0].split(',') | |
779 | config['EXCLUDED_INDEXES'] \ | |
780 | = params['EXCLUDED_INDEXES'][0].split(',') | |
543 | 781 | if 'INCLUDE_SPELLING' in params.keys(): |
544 | config['INCLUDE_SPELLING'] = cls.parse_value(params['INCLUDE_SPELLING'][0], bool) | |
782 | config['INCLUDE_SPELLING'] = cls.parse_value( | |
783 | params['INCLUDE_SPELLING'][0], | |
784 | bool | |
785 | ) | |
545 | 786 | if 'BATCH_SIZE' in params.keys(): |
546 | config['BATCH_SIZE'] = cls.parse_value(params['BATCH_SIZE'][0], int) | |
787 | config['BATCH_SIZE'] = cls.parse_value( | |
788 | params['BATCH_SIZE'][0], | |
789 | int | |
790 | ) | |
547 | 791 | |
548 | 792 | if url.scheme == 'simple': |
549 | 793 | return config |
550 | elif url.scheme in ['solr', 'elasticsearch', 'elasticsearch2']: | |
794 | elif url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY: | |
551 | 795 | if 'KWARGS' in params.keys(): |
552 | 796 | config['KWARGS'] = params['KWARGS'][0] |
553 | 797 | |
556 | 800 | path = path[:-1] |
557 | 801 | |
558 | 802 | if url.scheme == 'solr': |
559 | config['URL'] = urllib.parse.urlunparse(('http',) + url[1:2] + (path,) + ('', '', '')) | |
803 | config['URL'] = urlunparse( | |
804 | ('http',) + url[1:2] + (path,) + ('', '', '') | |
805 | ) | |
560 | 806 | if 'TIMEOUT' in params.keys(): |
561 | 807 | config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int) |
562 | 808 | return config |
563 | 809 | |
564 | if url.scheme in ['elasticsearch', 'elasticsearch2']: | |
565 | ||
810 | if url.scheme in cls.ELASTICSEARCH_FAMILY: | |
566 | 811 | split = path.rsplit("/", 1) |
567 | 812 | |
568 | 813 | if len(split) > 1: |
572 | 817 | path = "" |
573 | 818 | index = split[0] |
574 | 819 | |
575 | config['URL'] = urllib.parse.urlunparse(('http',) + url[1:2] + (path,) + ('', '', '')) | |
820 | config['URL'] = urlunparse( | |
821 | ('http',) + url[1:2] + (path,) + ('', '', '') | |
822 | ) | |
576 | 823 | if 'TIMEOUT' in params.keys(): |
577 | 824 | config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int) |
578 | 825 | config['INDEX_NAME'] = index |
584 | 831 | if 'STORAGE' in params.keys(): |
585 | 832 | config['STORAGE'] = params['STORAGE'][0] |
586 | 833 | if 'POST_LIMIT' in params.keys(): |
587 | config['POST_LIMIT'] = cls.parse_value(params['POST_LIMIT'][0], int) | |
834 | config['POST_LIMIT'] = cls.parse_value( | |
835 | params['POST_LIMIT'][0], | |
836 | int | |
837 | ) | |
588 | 838 | elif url.scheme == 'xapian': |
589 | 839 | if 'FLAGS' in params.keys(): |
590 | 840 | config['FLAGS'] = params['FLAGS'][0] |
595 | 845 | return config |
596 | 846 | |
597 | 847 | @classmethod |
598 | def read_env(cls, env_file=None, **overrides): | |
599 | """Read a .env file into os.environ. | |
600 | ||
601 | If not given a path to a dotenv path, does filthy magic stack backtracking | |
602 | to find manage.py and then find the dotenv. | |
603 | ||
604 | http://www.wellfireinteractive.com/blog/easier-12-factor-django/ | |
605 | ||
606 | https://gist.github.com/bennylope/2999704 | |
848 | def read_env(cls, env_file=None, overwrite=False, **overrides): | |
849 | r"""Read a .env file into os.environ. | |
850 | ||
851 | If not given a path to a dotenv path, does filthy magic stack | |
852 | backtracking to find the dotenv in the same directory as the file that | |
853 | called ``read_env``. | |
854 | ||
855 | Existing environment variables take precedent and are NOT overwritten | |
856 | by the file content. ``overwrite=True`` will force an overwrite of | |
857 | existing environment variables. | |
858 | ||
859 | Refs: | |
860 | ||
861 | * https://wellfire.co/learn/easier-12-factor-django | |
862 | ||
863 | :param env_file: The path to the ``.env`` file your application should | |
864 | use. If a path is not provided, `read_env` will attempt to import | |
865 | the Django settings module from the Django project root. | |
866 | :param overwrite: ``overwrite=True`` will force an overwrite of | |
867 | existing environment variables. | |
868 | :param \**overrides: Any additional keyword arguments provided directly | |
869 | to read_env will be added to the environment. If the key matches an | |
870 | existing environment variable, the value will be overridden. | |
607 | 871 | """ |
608 | 872 | if env_file is None: |
609 | 873 | frame = sys._getframe() |
610 | env_file = os.path.join(os.path.dirname(frame.f_back.f_code.co_filename), '.env') | |
874 | env_file = os.path.join( | |
875 | os.path.dirname(frame.f_back.f_code.co_filename), | |
876 | '.env' | |
877 | ) | |
611 | 878 | if not os.path.exists(env_file): |
612 | warnings.warn( | |
879 | logger.info( | |
613 | 880 | "%s doesn't exist - if you're not configuring your " |
614 | 881 | "environment separately, create one." % env_file) |
615 | 882 | return |
616 | 883 | |
617 | 884 | try: |
618 | with open(env_file) if isinstance(env_file, string_types) else env_file as f: | |
619 | content = f.read() | |
620 | except IOError: | |
621 | warnings.warn( | |
622 | "Error reading %s - if you're not configuring your " | |
885 | if isinstance(env_file, Openable): | |
886 | # Python 3.5 support (wrap path with str). | |
887 | with open(str(env_file)) as f: | |
888 | content = f.read() | |
889 | else: | |
890 | with env_file as f: | |
891 | content = f.read() | |
892 | except OSError: | |
893 | logger.info( | |
894 | "%s not found - if you're not configuring your " | |
623 | 895 | "environment separately, check this." % env_file) |
624 | 896 | return |
625 | 897 | |
626 | logger.debug('Read environment variables from: {0}'.format(env_file)) | |
898 | logger.debug('Read environment variables from: {}'.format(env_file)) | |
899 | ||
900 | def _keep_escaped_format_characters(match): | |
901 | """Keep escaped newline/tabs in quoted strings""" | |
902 | escaped_char = match.group(1) | |
903 | if escaped_char in 'rnt': | |
904 | return '\\' + escaped_char | |
905 | return escaped_char | |
627 | 906 | |
628 | 907 | for line in content.splitlines(): |
629 | m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line) | |
908 | m1 = re.match(r'\A(?:export )?([A-Za-z_0-9]+)=(.*)\Z', line) | |
630 | 909 | if m1: |
631 | 910 | key, val = m1.group(1), m1.group(2) |
632 | 911 | m2 = re.match(r"\A'(.*)'\Z", val) |
634 | 913 | val = m2.group(1) |
635 | 914 | m3 = re.match(r'\A"(.*)"\Z', val) |
636 | 915 | if m3: |
637 | val = re.sub(r'\\(.)', r'\1', m3.group(1)) | |
638 | cls.ENVIRON.setdefault(key, str(val)) | |
639 | ||
640 | # set defaults | |
916 | val = re.sub(r'\\(.)', _keep_escaped_format_characters, | |
917 | m3.group(1)) | |
918 | overrides[key] = str(val) | |
919 | elif not line or line.startswith('#'): | |
920 | # ignore warnings for empty line-breaks or comments | |
921 | pass | |
922 | else: | |
923 | logger.warning('Invalid line: %s', line) | |
924 | ||
925 | def set_environ(envval): | |
926 | """Return lambda to set environ. | |
927 | ||
928 | Use setdefault unless overwrite is specified. | |
929 | """ | |
930 | if overwrite: | |
931 | return lambda k, v: envval.update({k: str(v)}) | |
932 | return lambda k, v: envval.setdefault(k, str(v)) | |
933 | ||
934 | setenv = set_environ(cls.ENVIRON) | |
935 | ||
641 | 936 | for key, value in overrides.items(): |
642 | cls.ENVIRON.setdefault(key, value) | |
643 | ||
644 | ||
645 | class Path(object): | |
646 | ||
647 | """Inspired to Django Two-scoops, handling File Paths in Settings. | |
648 | ||
649 | >>> from environ import Path | |
650 | >>> root = Path('/home') | |
651 | >>> root, root(), root('dev') | |
652 | (<Path:/home>, '/home', '/home/dev') | |
653 | >>> root == Path('/home') | |
654 | True | |
655 | >>> root in Path('/'), root not in Path('/other/path') | |
656 | (True, True) | |
657 | >>> root('dev', 'not_existing_dir', required=True) | |
658 | Traceback (most recent call last): | |
659 | environ.environ.ImproperlyConfigured: Create required path: /home/not_existing_dir | |
660 | >>> public = root.path('public') | |
661 | >>> public, public.root, public('styles') | |
662 | (<Path:/home/public>, '/home/public', '/home/public/styles') | |
663 | >>> assets, scripts = public.path('assets'), public.path('assets', 'scripts') | |
664 | >>> assets.root, scripts.root | |
665 | ('/home/public/assets', '/home/public/assets/scripts') | |
666 | >>> assets + 'styles', str(assets + 'styles'), ~assets | |
667 | (<Path:/home/public/assets/styles>, '/home/public/assets/styles', <Path:/home/public>) | |
668 | ||
937 | setenv(key, value) | |
938 | ||
939 | ||
940 | class FileAwareEnv(Env): | |
669 | 941 | """ |
942 | First look for environment variables with ``_FILE`` appended. If found, | |
943 | their contents will be read from the file system and used instead. | |
944 | ||
945 | Use as a drop-in replacement for the standard ``environ.Env``: | |
946 | ||
947 | .. code-block:: python | |
948 | ||
949 | python env = environ.FileAwareEnv() | |
950 | ||
951 | For example, if a ``SECRET_KEY_FILE`` environment variable was set, | |
952 | ``env("SECRET_KEY")`` would find the related variable, returning the file | |
953 | contents rather than ever looking up a ``SECRET_KEY`` environment variable. | |
954 | """ | |
955 | ENVIRON = FileAwareMapping() | |
956 | ||
957 | ||
958 | class Path: | |
959 | """Inspired to Django Two-scoops, handling File Paths in Settings.""" | |
670 | 960 | |
671 | 961 | def path(self, *paths, **kwargs): |
672 | 962 | """Create new Path based on self.root and provided paths. |
678 | 968 | return self.__class__(self.__root__, *paths, **kwargs) |
679 | 969 | |
680 | 970 | def file(self, name, *args, **kwargs): |
681 | """Open a file. | |
682 | ||
683 | :param name: Filename appended to self.root | |
684 | :param args: passed to open() | |
685 | :param kwargs: passed to open() | |
686 | ||
687 | :rtype: file | |
971 | r"""Open a file. | |
972 | ||
973 | :param str name: Filename appended to :py:attr:`~root` | |
974 | :param \*args: ``*args`` passed to :py:func:`open` | |
975 | :param \**kwargs: ``**kwargs`` passed to :py:func:`open` | |
976 | :rtype: typing.IO[typing.Any] | |
688 | 977 | """ |
689 | 978 | return open(self(name), *args, **kwargs) |
690 | 979 | |
695 | 984 | |
696 | 985 | def __init__(self, start='', *paths, **kwargs): |
697 | 986 | |
698 | super(Path, self).__init__() | |
987 | super().__init__() | |
699 | 988 | |
700 | 989 | if kwargs.get('is_file', False): |
701 | 990 | start = os.path.dirname(start) |
711 | 1000 | return self._absolute_join(self.__root__, *paths, **kwargs) |
712 | 1001 | |
713 | 1002 | def __eq__(self, other): |
714 | return self.__root__ == other.__root__ | |
1003 | if isinstance(other, Path): | |
1004 | return self.__root__ == other.__root__ | |
1005 | return self.__root__ == other | |
715 | 1006 | |
716 | 1007 | def __ne__(self, other): |
717 | 1008 | return not self.__eq__(other) |
718 | 1009 | |
719 | 1010 | def __add__(self, other): |
720 | return Path(self.__root__, other if not isinstance(other, Path) else other.__root__) | |
1011 | if not isinstance(other, Path): | |
1012 | return Path(self.__root__, other) | |
1013 | return Path(self.__root__, other.__root__) | |
721 | 1014 | |
722 | 1015 | def __sub__(self, other): |
723 | 1016 | if isinstance(other, int): |
724 | 1017 | return self.path('../' * other) |
725 | elif isinstance(other, string_types): | |
1018 | elif isinstance(other, str): | |
726 | 1019 | if self.__root__.endswith(other): |
727 | 1020 | return Path(self.__root__.rstrip(other)) |
728 | 1021 | raise TypeError( |
742 | 1035 | return item.__root__.startswith(base_path) |
743 | 1036 | |
744 | 1037 | def __repr__(self): |
745 | return "<Path:{0}>".format(self.__root__) | |
1038 | return "<Path:{}>".format(self.__root__) | |
746 | 1039 | |
747 | 1040 | def __str__(self): |
748 | 1041 | return self.__root__ |
752 | 1045 | |
753 | 1046 | def __getitem__(self, *args, **kwargs): |
754 | 1047 | return self.__str__().__getitem__(*args, **kwargs) |
1048 | ||
1049 | def __fspath__(self): | |
1050 | return self.__str__() | |
755 | 1051 | |
756 | 1052 | def rfind(self, *args, **kwargs): |
757 | 1053 | return self.__str__().rfind(*args, **kwargs) |
764 | 1060 | absolute_path = os.path.abspath(os.path.join(base, *paths)) |
765 | 1061 | if kwargs.get('required', False) and not os.path.exists(absolute_path): |
766 | 1062 | raise ImproperlyConfigured( |
767 | "Create required path: {0}".format(absolute_path)) | |
1063 | "Create required path: {}".format(absolute_path)) | |
768 | 1064 | return absolute_path |
769 | 1065 | |
770 | 1066 | |
771 | 1067 | def register_scheme(scheme): |
772 | for method in dir(urllib.parse): | |
1068 | for method in dir(urlparselib): | |
773 | 1069 | if method.startswith('uses_'): |
774 | getattr(urllib.parse, method).append(scheme) | |
1070 | getattr(urlparselib, method).append(scheme) | |
775 | 1071 | |
776 | 1072 | |
777 | 1073 | def register_schemes(schemes): |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | """Docker-style file variable support module.""" | |
9 | ||
10 | import os | |
11 | from collections.abc import MutableMapping | |
12 | ||
13 | ||
14 | class FileAwareMapping(MutableMapping): | |
15 | """ | |
16 | A mapping that wraps os.environ, first checking for the existence of a key | |
17 | appended with ``_FILE`` whenever reading a value. If a matching file key is | |
18 | found then the value is instead read from the file system at this location. | |
19 | ||
20 | By default, values read from the file system are cached so future lookups | |
21 | do not hit the disk again. | |
22 | ||
23 | A ``_FILE`` key has higher precedence than a value is set directly in the | |
24 | environment, and an exception is raised if the file can not be found. | |
25 | """ | |
26 | ||
27 | def __init__(self, env=None, cache=True): | |
28 | """ | |
29 | Initialize the mapping. | |
30 | ||
31 | :param env: | |
32 | where to read environment variables from (defaults to | |
33 | ``os.environ``) | |
34 | :param cache: | |
35 | cache environment variables read from the file system (defaults to | |
36 | ``True``) | |
37 | """ | |
38 | self.env = env if env is not None else os.environ | |
39 | self.cache = cache | |
40 | self.files_cache = {} | |
41 | ||
42 | def __getitem__(self, key): | |
43 | if self.cache and key in self.files_cache: | |
44 | return self.files_cache[key] | |
45 | key_file = self.env.get(key + "_FILE") | |
46 | if key_file: | |
47 | with open(key_file) as f: | |
48 | value = f.read() | |
49 | if self.cache: | |
50 | self.files_cache[key] = value | |
51 | return value | |
52 | return self.env[key] | |
53 | ||
54 | def __iter__(self): | |
55 | """ | |
56 | Iterate all keys, also always including the shortened key if ``_FILE`` | |
57 | keys are found. | |
58 | """ | |
59 | for key in self.env: | |
60 | yield key | |
61 | if key.endswith("_FILE"): | |
62 | no_file_key = key[:-5] | |
63 | if no_file_key and no_file_key not in self.env: | |
64 | yield no_file_key | |
65 | ||
66 | def __len__(self): | |
67 | """ | |
68 | Return the length of the file, also always counting shortened keys for | |
69 | any ``_FILE`` key found. | |
70 | """ | |
71 | return len(tuple(iter(self))) | |
72 | ||
73 | def __setitem__(self, key, value): | |
74 | self.env[key] = value | |
75 | if self.cache and key.endswith("_FILE"): | |
76 | no_file_key = key[:-5] | |
77 | if no_file_key and no_file_key in self.files_cache: | |
78 | del self.files_cache[no_file_key] | |
79 | ||
80 | def __delitem__(self, key): | |
81 | file_key = key + "_FILE" | |
82 | if file_key in self.env: | |
83 | del self[file_key] | |
84 | if key in self.env: | |
85 | del self.env[key] | |
86 | return | |
87 | if self.cache and key.endswith("_FILE"): | |
88 | no_file_key = key[:-5] | |
89 | if no_file_key and no_file_key in self.files_cache: | |
90 | del self.files_cache[no_file_key] | |
91 | del self.env[key] |
0 | from __future__ import print_function | |
1 | import json | |
2 | import os | |
3 | import sys | |
4 | import unittest | |
5 | ||
6 | from django.core.exceptions import ImproperlyConfigured | |
7 | ||
8 | from environ import Env, Path, REDIS_DRIVER | |
9 | ||
10 | ||
11 | class BaseTests(unittest.TestCase): | |
12 | ||
13 | URL = 'http://www.google.com/' | |
14 | POSTGRES = 'postgres://uf07k1:wegauwhg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722' | |
15 | MYSQL = 'mysql://bea6eb0:69772142@us-cdbr-east.cleardb.com/heroku_97681?reconnect=true' | |
16 | MYSQLGIS = 'mysqlgis://user:password@127.0.0.1/some_database' | |
17 | SQLITE = 'sqlite:////full/path/to/your/database/file.sqlite' | |
18 | ORACLE_TNS = 'oracle://user:password@sid/' | |
19 | ORACLE = 'oracle://user:password@host:1521/sid' | |
20 | REDSHIFT = 'redshift://user:password@examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev' | |
21 | MEMCACHE = 'memcache://127.0.0.1:11211' | |
22 | REDIS = 'rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=secret' | |
23 | EMAIL = 'smtps://user@domain.com:password@smtp.example.com:587' | |
24 | JSON = dict(one='bar', two=2, three=33.44) | |
25 | DICT = dict(foo='bar', test='on') | |
26 | PATH = '/home/dev' | |
27 | ||
28 | @classmethod | |
29 | def generateData(cls): | |
30 | return dict(STR_VAR='bar', | |
31 | INT_VAR='42', | |
32 | FLOAT_VAR='33.3', | |
33 | FLOAT_COMMA_VAR='33,3', | |
34 | FLOAT_STRANGE_VAR1='123,420,333.3', | |
35 | FLOAT_STRANGE_VAR2='123.420.333,3', | |
36 | BOOL_TRUE_VAR='1', | |
37 | BOOL_TRUE_VAR2='True', | |
38 | BOOL_FALSE_VAR='0', | |
39 | BOOL_FALSE_VAR2='False', | |
40 | PROXIED_VAR='$STR_VAR', | |
41 | INT_LIST='42,33', | |
42 | INT_TUPLE='(42,33)', | |
43 | STR_LIST_WITH_SPACES=' foo, bar', | |
44 | EMPTY_LIST='', | |
45 | DICT_VAR='foo=bar,test=on', | |
46 | DATABASE_URL=cls.POSTGRES, | |
47 | DATABASE_MYSQL_URL=cls.MYSQL, | |
48 | DATABASE_MYSQL_GIS_URL=cls.MYSQLGIS, | |
49 | DATABASE_SQLITE_URL=cls.SQLITE, | |
50 | DATABASE_ORACLE_URL=cls.ORACLE, | |
51 | DATABASE_ORACLE_TNS_URL=cls.ORACLE_TNS, | |
52 | DATABASE_REDSHIFT_URL=cls.REDSHIFT, | |
53 | CACHE_URL=cls.MEMCACHE, | |
54 | CACHE_REDIS=cls.REDIS, | |
55 | EMAIL_URL=cls.EMAIL, | |
56 | URL_VAR=cls.URL, | |
57 | JSON_VAR=json.dumps(cls.JSON), | |
58 | PATH_VAR=cls.PATH) | |
59 | ||
60 | def setUp(self): | |
61 | self._old_environ = os.environ | |
62 | os.environ = Env.ENVIRON = self.generateData() | |
63 | self.env = Env() | |
64 | ||
65 | def tearDown(self): | |
66 | os.environ = self._old_environ | |
67 | ||
68 | def assertTypeAndValue(self, type_, expected, actual): | |
69 | self.assertEqual(type_, type(actual)) | |
70 | self.assertEqual(expected, actual) | |
71 | ||
72 | ||
73 | class EnvTests(BaseTests): | |
74 | ||
75 | def test_not_present_with_default(self): | |
76 | self.assertEqual(3, self.env('not_present', default=3)) | |
77 | ||
78 | def test_not_present_without_default(self): | |
79 | self.assertRaises(ImproperlyConfigured, self.env, 'not_present') | |
80 | ||
81 | def test_contains(self): | |
82 | self.assertTrue('STR_VAR' in self.env) | |
83 | self.assertTrue('EMPTY_LIST' in self.env) | |
84 | self.assertFalse('I_AM_NOT_A_VAR' in self.env) | |
85 | ||
86 | def test_str(self): | |
87 | self.assertTypeAndValue(str, 'bar', self.env('STR_VAR')) | |
88 | self.assertTypeAndValue(str, 'bar', self.env.str('STR_VAR')) | |
89 | ||
90 | def test_int(self): | |
91 | self.assertTypeAndValue(int, 42, self.env('INT_VAR', cast=int)) | |
92 | self.assertTypeAndValue(int, 42, self.env.int('INT_VAR')) | |
93 | ||
94 | def test_int_with_none_default(self): | |
95 | self.assertTrue(self.env('NOT_PRESENT_VAR', cast=int, default=None) is None) | |
96 | ||
97 | def test_float(self): | |
98 | self.assertTypeAndValue(float, 33.3, self.env('FLOAT_VAR', cast=float)) | |
99 | self.assertTypeAndValue(float, 33.3, self.env.float('FLOAT_VAR')) | |
100 | ||
101 | self.assertTypeAndValue(float, 33.3, self.env('FLOAT_COMMA_VAR', cast=float)) | |
102 | self.assertTypeAndValue(float, 123420333.3, self.env('FLOAT_STRANGE_VAR1', cast=float)) | |
103 | self.assertTypeAndValue(float, 123420333.3, self.env('FLOAT_STRANGE_VAR2', cast=float)) | |
104 | ||
105 | def test_bool_true(self): | |
106 | self.assertTypeAndValue(bool, True, self.env('BOOL_TRUE_VAR', cast=bool)) | |
107 | self.assertTypeAndValue(bool, True, self.env('BOOL_TRUE_VAR2', cast=bool)) | |
108 | self.assertTypeAndValue(bool, True, self.env.bool('BOOL_TRUE_VAR')) | |
109 | ||
110 | def test_bool_false(self): | |
111 | self.assertTypeAndValue(bool, False, self.env('BOOL_FALSE_VAR', cast=bool)) | |
112 | self.assertTypeAndValue(bool, False, self.env('BOOL_FALSE_VAR2', cast=bool)) | |
113 | self.assertTypeAndValue(bool, False, self.env.bool('BOOL_FALSE_VAR')) | |
114 | ||
115 | def test_proxied_value(self): | |
116 | self.assertTypeAndValue(str, 'bar', self.env('PROXIED_VAR')) | |
117 | ||
118 | def test_int_list(self): | |
119 | self.assertTypeAndValue(list, [42, 33], self.env('INT_LIST', cast=[int])) | |
120 | self.assertTypeAndValue(list, [42, 33], self.env.list('INT_LIST', int)) | |
121 | ||
122 | def test_int_tuple(self): | |
123 | self.assertTypeAndValue(tuple, (42, 33), self.env('INT_LIST', cast=(int,))) | |
124 | self.assertTypeAndValue(tuple, (42, 33), self.env.tuple('INT_LIST', int)) | |
125 | self.assertTypeAndValue(tuple, ('42', '33'), self.env.tuple('INT_LIST')) | |
126 | ||
127 | def test_str_list_with_spaces(self): | |
128 | self.assertTypeAndValue(list, [' foo', ' bar'], | |
129 | self.env('STR_LIST_WITH_SPACES', cast=[str])) | |
130 | self.assertTypeAndValue(list, [' foo', ' bar'], | |
131 | self.env.list('STR_LIST_WITH_SPACES')) | |
132 | ||
133 | def test_empty_list(self): | |
134 | self.assertTypeAndValue(list, [], self.env('EMPTY_LIST', cast=[int])) | |
135 | ||
136 | def test_dict_value(self): | |
137 | self.assertTypeAndValue(dict, self.DICT, self.env.dict('DICT_VAR')) | |
138 | ||
139 | def test_dict_parsing(self): | |
140 | ||
141 | self.assertEqual({'a': '1'}, self.env.parse_value('a=1', dict)) | |
142 | self.assertEqual({'a': 1}, self.env.parse_value('a=1', dict(value=int))) | |
143 | self.assertEqual({'a': ['1', '2', '3']}, self.env.parse_value('a=1,2,3', dict(value=[str]))) | |
144 | self.assertEqual({'a': [1, 2, 3]}, self.env.parse_value('a=1,2,3', dict(value=[int]))) | |
145 | self.assertEqual({'a': 1, 'b': [1.1, 2.2], 'c': 3}, | |
146 | self.env.parse_value('a=1;b=1.1,2.2;c=3', dict(value=int, cast=dict(b=[float])))) | |
147 | ||
148 | self.assertEqual({'a': "uname", 'c': "http://www.google.com", 'b': True}, | |
149 | self.env.parse_value('a=uname;c=http://www.google.com;b=True', dict(value=str, cast=dict(b=bool)))) | |
150 | ||
151 | def test_url_value(self): | |
152 | url = self.env.url('URL_VAR') | |
153 | self.assertEqual(url.__class__, self.env.URL_CLASS) | |
154 | self.assertEqual(url.geturl(), self.URL) | |
155 | self.assertEqual(None, self.env.url('OTHER_URL', default=None)) | |
156 | ||
157 | def test_url_encoded_parts(self): | |
158 | from six.moves import urllib | |
159 | password_with_unquoted_characters = "#password" | |
160 | encoded_url = "mysql://user:%s@127.0.0.1:3306/dbname" % urllib.parse.quote(password_with_unquoted_characters) | |
161 | parsed_url = self.env.db_url_config(encoded_url) | |
162 | self.assertEqual(parsed_url['PASSWORD'], password_with_unquoted_characters) | |
163 | ||
164 | def test_db_url_value(self): | |
165 | pg_config = self.env.db() | |
166 | self.assertEqual(pg_config['ENGINE'], 'django.db.backends.postgresql_psycopg2') | |
167 | self.assertEqual(pg_config['NAME'], 'd8r82722') | |
168 | self.assertEqual(pg_config['HOST'], 'ec2-107-21-253-135.compute-1.amazonaws.com') | |
169 | self.assertEqual(pg_config['USER'], 'uf07k1') | |
170 | self.assertEqual(pg_config['PASSWORD'], 'wegauwhg') | |
171 | self.assertEqual(pg_config['PORT'], 5431) | |
172 | ||
173 | mysql_config = self.env.db('DATABASE_MYSQL_URL') | |
174 | self.assertEqual(mysql_config['ENGINE'], 'django.db.backends.mysql') | |
175 | self.assertEqual(mysql_config['NAME'], 'heroku_97681') | |
176 | self.assertEqual(mysql_config['HOST'], 'us-cdbr-east.cleardb.com') | |
177 | self.assertEqual(mysql_config['USER'], 'bea6eb0') | |
178 | self.assertEqual(mysql_config['PASSWORD'], '69772142') | |
179 | self.assertEqual(mysql_config['PORT'], '') | |
180 | ||
181 | mysql_gis_config = self.env.db('DATABASE_MYSQL_GIS_URL') | |
182 | self.assertEqual(mysql_gis_config['ENGINE'], 'django.contrib.gis.db.backends.mysql') | |
183 | self.assertEqual(mysql_gis_config['NAME'], 'some_database') | |
184 | self.assertEqual(mysql_gis_config['HOST'], '127.0.0.1') | |
185 | self.assertEqual(mysql_gis_config['USER'], 'user') | |
186 | self.assertEqual(mysql_gis_config['PASSWORD'], 'password') | |
187 | self.assertEqual(mysql_gis_config['PORT'], '') | |
188 | ||
189 | oracle_config = self.env.db('DATABASE_ORACLE_TNS_URL') | |
190 | self.assertEqual(oracle_config['ENGINE'], 'django.db.backends.oracle') | |
191 | self.assertEqual(oracle_config['NAME'], 'sid') | |
192 | self.assertEqual(oracle_config['HOST'], '') | |
193 | self.assertEqual(oracle_config['USER'], 'user') | |
194 | self.assertEqual(oracle_config['PASSWORD'], 'password') | |
195 | self.assertFalse('PORT' in oracle_config) | |
196 | ||
197 | oracle_config = self.env.db('DATABASE_ORACLE_URL') | |
198 | self.assertEqual(oracle_config['ENGINE'], 'django.db.backends.oracle') | |
199 | self.assertEqual(oracle_config['NAME'], 'sid') | |
200 | self.assertEqual(oracle_config['HOST'], 'host') | |
201 | self.assertEqual(oracle_config['USER'], 'user') | |
202 | self.assertEqual(oracle_config['PASSWORD'], 'password') | |
203 | self.assertEqual(oracle_config['PORT'], '1521') | |
204 | ||
205 | redshift_config = self.env.db('DATABASE_REDSHIFT_URL') | |
206 | self.assertEqual(redshift_config['ENGINE'], 'django_redshift_backend') | |
207 | self.assertEqual(redshift_config['NAME'], 'dev') | |
208 | self.assertEqual(redshift_config['HOST'], 'examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com') | |
209 | self.assertEqual(redshift_config['USER'], 'user') | |
210 | self.assertEqual(redshift_config['PASSWORD'], 'password') | |
211 | self.assertEqual(redshift_config['PORT'], 5439) | |
212 | ||
213 | sqlite_config = self.env.db('DATABASE_SQLITE_URL') | |
214 | self.assertEqual(sqlite_config['ENGINE'], 'django.db.backends.sqlite3') | |
215 | self.assertEqual(sqlite_config['NAME'], '/full/path/to/your/database/file.sqlite') | |
216 | ||
217 | def test_cache_url_value(self): | |
218 | ||
219 | cache_config = self.env.cache_url() | |
220 | self.assertEqual(cache_config['BACKEND'], 'django.core.cache.backends.memcached.MemcachedCache') | |
221 | self.assertEqual(cache_config['LOCATION'], '127.0.0.1:11211') | |
222 | ||
223 | redis_config = self.env.cache_url('CACHE_REDIS') | |
224 | self.assertEqual(redis_config['BACKEND'], 'django_redis.cache.RedisCache') | |
225 | self.assertEqual(redis_config['LOCATION'], 'redis://127.0.0.1:6379/1') | |
226 | self.assertEqual(redis_config['OPTIONS'], { | |
227 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
228 | 'PASSWORD': 'secret', | |
229 | }) | |
230 | ||
231 | def test_email_url_value(self): | |
232 | ||
233 | email_config = self.env.email_url() | |
234 | self.assertEqual(email_config['EMAIL_BACKEND'], 'django.core.mail.backends.smtp.EmailBackend') | |
235 | self.assertEqual(email_config['EMAIL_HOST'], 'smtp.example.com') | |
236 | self.assertEqual(email_config['EMAIL_HOST_PASSWORD'], 'password') | |
237 | self.assertEqual(email_config['EMAIL_HOST_USER'], 'user@domain.com') | |
238 | self.assertEqual(email_config['EMAIL_PORT'], 587) | |
239 | self.assertEqual(email_config['EMAIL_USE_TLS'], True) | |
240 | ||
241 | def test_json_value(self): | |
242 | self.assertEqual(self.JSON, self.env.json('JSON_VAR')) | |
243 | ||
244 | def test_path(self): | |
245 | root = self.env.path('PATH_VAR') | |
246 | self.assertTypeAndValue(Path, Path(self.PATH), root) | |
247 | ||
248 | ||
249 | class FileEnvTests(EnvTests): | |
250 | ||
251 | def setUp(self): | |
252 | super(FileEnvTests, self).setUp() | |
253 | Env.ENVIRON = {} | |
254 | self.env = Env() | |
255 | file_path = Path(__file__, is_file=True)('test_env.txt') | |
256 | self.env.read_env(file_path, PATH_VAR=Path(__file__, is_file=True).__root__) | |
257 | ||
258 | class SubClassTests(EnvTests): | |
259 | ||
260 | def setUp(self): | |
261 | super(SubClassTests, self).setUp() | |
262 | self.CONFIG = self.generateData() | |
263 | class MyEnv(Env): | |
264 | ENVIRON = self.CONFIG | |
265 | self.env = MyEnv() | |
266 | ||
267 | def test_singleton_environ(self): | |
268 | self.assertTrue(self.CONFIG is self.env.ENVIRON) | |
269 | ||
270 | ||
271 | class SchemaEnvTests(BaseTests): | |
272 | ||
273 | def test_schema(self): | |
274 | env = Env(INT_VAR=int, NOT_PRESENT_VAR=(float, 33.3), STR_VAR=str, | |
275 | INT_LIST=[int], DEFAULT_LIST=([int], [2])) | |
276 | ||
277 | self.assertTypeAndValue(int, 42, env('INT_VAR')) | |
278 | self.assertTypeAndValue(float, 33.3, env('NOT_PRESENT_VAR')) | |
279 | ||
280 | self.assertTypeAndValue(str, 'bar', env('STR_VAR')) | |
281 | self.assertTypeAndValue(str, 'foo', env('NOT_PRESENT2', default='foo')) | |
282 | ||
283 | self.assertTypeAndValue(list, [42, 33], env('INT_LIST')) | |
284 | self.assertTypeAndValue(list, [2], env('DEFAULT_LIST')) | |
285 | ||
286 | # Override schema in this one case | |
287 | self.assertTypeAndValue(str, '42', env('INT_VAR', cast=str)) | |
288 | ||
289 | ||
290 | class DatabaseTestSuite(unittest.TestCase): | |
291 | ||
292 | def test_postgres_parsing(self): | |
293 | url = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' | |
294 | url = Env.db_url_config(url) | |
295 | ||
296 | self.assertEqual(url['ENGINE'], 'django.db.backends.postgresql_psycopg2') | |
297 | self.assertEqual(url['NAME'], 'd8r82722r2kuvn') | |
298 | self.assertEqual(url['HOST'], 'ec2-107-21-253-135.compute-1.amazonaws.com') | |
299 | self.assertEqual(url['USER'], 'uf07k1i6d8ia0v') | |
300 | self.assertEqual(url['PASSWORD'], 'wegauwhgeuioweg') | |
301 | self.assertEqual(url['PORT'], 5431) | |
302 | ||
303 | def test_postgis_parsing(self): | |
304 | url = 'postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' | |
305 | url = Env.db_url_config(url) | |
306 | ||
307 | self.assertEqual(url['ENGINE'], 'django.contrib.gis.db.backends.postgis') | |
308 | self.assertEqual(url['NAME'], 'd8r82722r2kuvn') | |
309 | self.assertEqual(url['HOST'], 'ec2-107-21-253-135.compute-1.amazonaws.com') | |
310 | self.assertEqual(url['USER'], 'uf07k1i6d8ia0v') | |
311 | self.assertEqual(url['PASSWORD'], 'wegauwhgeuioweg') | |
312 | self.assertEqual(url['PORT'], 5431) | |
313 | ||
314 | def test_mysql_gis_parsing(self): | |
315 | url = 'mysqlgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' | |
316 | url = Env.db_url_config(url) | |
317 | ||
318 | self.assertEqual(url['ENGINE'], 'django.contrib.gis.db.backends.mysql') | |
319 | self.assertEqual(url['NAME'], 'd8r82722r2kuvn') | |
320 | self.assertEqual(url['HOST'], 'ec2-107-21-253-135.compute-1.amazonaws.com') | |
321 | self.assertEqual(url['USER'], 'uf07k1i6d8ia0v') | |
322 | self.assertEqual(url['PASSWORD'], 'wegauwhgeuioweg') | |
323 | self.assertEqual(url['PORT'], 5431) | |
324 | ||
325 | def test_cleardb_parsing(self): | |
326 | url = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' | |
327 | url = Env.db_url_config(url) | |
328 | ||
329 | self.assertEqual(url['ENGINE'], 'django.db.backends.mysql') | |
330 | self.assertEqual(url['NAME'], 'heroku_97681db3eff7580') | |
331 | self.assertEqual(url['HOST'], 'us-cdbr-east.cleardb.com') | |
332 | self.assertEqual(url['USER'], 'bea6eb025ca0d8') | |
333 | self.assertEqual(url['PASSWORD'], '69772142') | |
334 | self.assertEqual(url['PORT'], '') | |
335 | ||
336 | def test_mysql_no_password(self): | |
337 | url = 'mysql://travis@localhost/test_db' | |
338 | url = Env.db_url_config(url) | |
339 | ||
340 | self.assertEqual(url['ENGINE'], 'django.db.backends.mysql') | |
341 | self.assertEqual(url['NAME'], 'test_db') | |
342 | self.assertEqual(url['HOST'], 'localhost') | |
343 | self.assertEqual(url['USER'], 'travis') | |
344 | self.assertEqual(url['PASSWORD'], '') | |
345 | self.assertEqual(url['PORT'], '') | |
346 | ||
347 | def test_empty_sqlite_url(self): | |
348 | url = 'sqlite://' | |
349 | url = Env.db_url_config(url) | |
350 | ||
351 | self.assertEqual(url['ENGINE'], 'django.db.backends.sqlite3') | |
352 | self.assertEqual(url['NAME'], ':memory:') | |
353 | ||
354 | def test_memory_sqlite_url(self): | |
355 | url = 'sqlite://:memory:' | |
356 | url = Env.db_url_config(url) | |
357 | ||
358 | self.assertEqual(url['ENGINE'], 'django.db.backends.sqlite3') | |
359 | self.assertEqual(url['NAME'], ':memory:') | |
360 | ||
361 | def test_database_options_parsing(self): | |
362 | url = 'postgres://user:pass@host:1234/dbname?conn_max_age=600' | |
363 | url = Env.db_url_config(url) | |
364 | self.assertEqual(url['CONN_MAX_AGE'], 600) | |
365 | ||
366 | url = 'mysql://user:pass@host:1234/dbname?init_command=SET storage_engine=INNODB' | |
367 | url = Env.db_url_config(url) | |
368 | self.assertEqual(url['OPTIONS'], { | |
369 | 'init_command': 'SET storage_engine=INNODB', | |
370 | }) | |
371 | ||
372 | def test_database_ldap_url(self): | |
373 | url = 'ldap://cn=admin,dc=nodomain,dc=org:some_secret_password@ldap.nodomain.org/' | |
374 | url = Env.db_url_config(url) | |
375 | ||
376 | self.assertEqual(url['ENGINE'], 'ldapdb.backends.ldap') | |
377 | self.assertEqual(url['HOST'], 'ldap.nodomain.org') | |
378 | self.assertEqual(url['PORT'], '') | |
379 | self.assertEqual(url['NAME'], 'ldap://ldap.nodomain.org') | |
380 | self.assertEqual(url['USER'], 'cn=admin,dc=nodomain,dc=org') | |
381 | self.assertEqual(url['PASSWORD'], 'some_secret_password') | |
382 | ||
383 | ||
384 | class CacheTestSuite(unittest.TestCase): | |
385 | ||
386 | def test_memcache_parsing(self): | |
387 | url = 'memcache://127.0.0.1:11211' | |
388 | url = Env.cache_url_config(url) | |
389 | ||
390 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.memcached.MemcachedCache') | |
391 | self.assertEqual(url['LOCATION'], '127.0.0.1:11211') | |
392 | ||
393 | def test_memcache_pylib_parsing(self): | |
394 | url = 'pymemcache://127.0.0.1:11211' | |
395 | url = Env.cache_url_config(url) | |
396 | ||
397 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.memcached.PyLibMCCache') | |
398 | self.assertEqual(url['LOCATION'], '127.0.0.1:11211') | |
399 | ||
400 | def test_memcache_multiple_parsing(self): | |
401 | url = 'memcache://172.19.26.240:11211,172.19.26.242:11212' | |
402 | url = Env.cache_url_config(url) | |
403 | ||
404 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.memcached.MemcachedCache') | |
405 | self.assertEqual(url['LOCATION'], ['172.19.26.240:11211', '172.19.26.242:11212']) | |
406 | ||
407 | def test_memcache_socket_parsing(self): | |
408 | url = 'memcache:///tmp/memcached.sock' | |
409 | url = Env.cache_url_config(url) | |
410 | ||
411 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.memcached.MemcachedCache') | |
412 | self.assertEqual(url['LOCATION'], 'unix:/tmp/memcached.sock') | |
413 | ||
414 | def test_dbcache_parsing(self): | |
415 | url = 'dbcache://my_cache_table' | |
416 | url = Env.cache_url_config(url) | |
417 | ||
418 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.db.DatabaseCache') | |
419 | self.assertEqual(url['LOCATION'], 'my_cache_table') | |
420 | ||
421 | def test_filecache_parsing(self): | |
422 | url = 'filecache:///var/tmp/django_cache' | |
423 | url = Env.cache_url_config(url) | |
424 | ||
425 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.filebased.FileBasedCache') | |
426 | self.assertEqual(url['LOCATION'], '/var/tmp/django_cache') | |
427 | ||
428 | def test_filecache_windows_parsing(self): | |
429 | url = 'filecache://C:/foo/bar' | |
430 | url = Env.cache_url_config(url) | |
431 | ||
432 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.filebased.FileBasedCache') | |
433 | self.assertEqual(url['LOCATION'], 'C:/foo/bar') | |
434 | ||
435 | def test_locmem_parsing(self): | |
436 | url = 'locmemcache://' | |
437 | url = Env.cache_url_config(url) | |
438 | ||
439 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.locmem.LocMemCache') | |
440 | self.assertEqual(url['LOCATION'], '') | |
441 | ||
442 | def test_locmem_named_parsing(self): | |
443 | url = 'locmemcache://unique-snowflake' | |
444 | url = Env.cache_url_config(url) | |
445 | ||
446 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.locmem.LocMemCache') | |
447 | self.assertEqual(url['LOCATION'], 'unique-snowflake') | |
448 | ||
449 | def test_dummycache_parsing(self): | |
450 | url = 'dummycache://' | |
451 | url = Env.cache_url_config(url) | |
452 | ||
453 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.dummy.DummyCache') | |
454 | self.assertEqual(url['LOCATION'], '') | |
455 | ||
456 | def test_redis_parsing(self): | |
457 | url = 'rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=secret' | |
458 | url = Env.cache_url_config(url) | |
459 | ||
460 | self.assertEqual(url['BACKEND'], REDIS_DRIVER) | |
461 | self.assertEqual(url['LOCATION'], 'redis://127.0.0.1:6379/1') | |
462 | self.assertEqual(url['OPTIONS'], { | |
463 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
464 | 'PASSWORD': 'secret', | |
465 | }) | |
466 | ||
467 | def test_redis_socket_parsing(self): | |
468 | url = 'rediscache:///path/to/socket:1' | |
469 | url = Env.cache_url_config(url) | |
470 | self.assertEqual(url['BACKEND'], 'django_redis.cache.RedisCache') | |
471 | self.assertEqual(url['LOCATION'], 'unix:///path/to/socket:1') | |
472 | ||
473 | def test_redis_with_password_parsing(self): | |
474 | url = 'rediscache://:redispass@127.0.0.1:6379/0' | |
475 | url = Env.cache_url_config(url) | |
476 | self.assertEqual(REDIS_DRIVER, url['BACKEND']) | |
477 | self.assertEqual(url['LOCATION'], 'redis://:redispass@127.0.0.1:6379/0') | |
478 | ||
479 | def test_redis_multi_location_parsing(self): | |
480 | url = 'rediscache://host1:6379,host2:6379,host3:9999/1' | |
481 | url = Env.cache_url_config(url) | |
482 | ||
483 | self.assertEqual(url['BACKEND'], REDIS_DRIVER) | |
484 | self.assertEqual(url['LOCATION'], [ | |
485 | 'redis://host1:6379/1', | |
486 | 'redis://host2:6379/1', | |
487 | 'redis://host3:9999/1', | |
488 | ]) | |
489 | ||
490 | def test_redis_socket_url(self): | |
491 | url = 'redis://:redispass@/path/to/socket.sock?db=0' | |
492 | url = Env.cache_url_config(url) | |
493 | self.assertEqual(REDIS_DRIVER, url['BACKEND']) | |
494 | self.assertEqual(url['LOCATION'], 'unix://:redispass@/path/to/socket.sock') | |
495 | self.assertEqual(url['OPTIONS'], { | |
496 | 'DB': 0 | |
497 | }) | |
498 | ||
499 | def test_options_parsing(self): | |
500 | url = 'filecache:///var/tmp/django_cache?timeout=60&max_entries=1000&cull_frequency=0' | |
501 | url = Env.cache_url_config(url) | |
502 | ||
503 | self.assertEqual(url['BACKEND'], 'django.core.cache.backends.filebased.FileBasedCache') | |
504 | self.assertEqual(url['LOCATION'], '/var/tmp/django_cache') | |
505 | self.assertEqual(url['TIMEOUT'], 60) | |
506 | self.assertEqual(url['OPTIONS'], { | |
507 | 'MAX_ENTRIES': 1000, | |
508 | 'CULL_FREQUENCY': 0, | |
509 | }) | |
510 | ||
511 | def test_custom_backend(self): | |
512 | url = 'memcache://127.0.0.1:5400?foo=option&bars=9001' | |
513 | backend = 'django_redis.cache.RedisCache' | |
514 | url = Env.cache_url_config(url, backend) | |
515 | ||
516 | self.assertEqual(url['BACKEND'], backend) | |
517 | self.assertEqual(url['LOCATION'], '127.0.0.1:5400') | |
518 | self.assertEqual(url['OPTIONS'], { | |
519 | 'FOO': 'option', | |
520 | 'BARS': 9001, | |
521 | }) | |
522 | ||
523 | ||
524 | class SearchTestSuite(unittest.TestCase): | |
525 | ||
526 | solr_url = 'solr://127.0.0.1:8983/solr' | |
527 | elasticsearch_url = 'elasticsearch://127.0.0.1:9200/index' | |
528 | whoosh_url = 'whoosh:///home/search/whoosh_index' | |
529 | xapian_url = 'xapian:///home/search/xapian_index' | |
530 | simple_url = 'simple:///' | |
531 | ||
532 | def test_solr_parsing(self): | |
533 | url = Env.search_url_config(self.solr_url) | |
534 | ||
535 | self.assertEqual(url['ENGINE'], 'haystack.backends.solr_backend.SolrEngine') | |
536 | self.assertEqual(url['URL'], 'http://127.0.0.1:8983/solr') | |
537 | ||
538 | def test_solr_multicore_parsing(self): | |
539 | timeout = 360 | |
540 | index = 'solr_index' | |
541 | url = '%s/%s?TIMEOUT=%s' % (self.solr_url, index, timeout) | |
542 | url = Env.search_url_config(url) | |
543 | ||
544 | self.assertEqual(url['ENGINE'], 'haystack.backends.solr_backend.SolrEngine') | |
545 | self.assertEqual(url['URL'], 'http://127.0.0.1:8983/solr/solr_index') | |
546 | self.assertEqual(url['TIMEOUT'], timeout) | |
547 | self.assertTrue('INDEX_NAME' not in url) | |
548 | self.assertTrue('PATH' not in url) | |
549 | ||
550 | def test_elasticsearch_parsing(self): | |
551 | timeout = 360 | |
552 | url = '%s?TIMEOUT=%s' % (self.elasticsearch_url, timeout) | |
553 | url = Env.search_url_config(url) | |
554 | ||
555 | self.assertEqual(url['ENGINE'], 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine') | |
556 | self.assertTrue('INDEX_NAME' in url.keys()) | |
557 | self.assertEqual(url['INDEX_NAME'], 'index') | |
558 | self.assertTrue('TIMEOUT' in url.keys()) | |
559 | self.assertEqual(url['TIMEOUT'], timeout) | |
560 | self.assertTrue('PATH' not in url) | |
561 | ||
562 | def test_whoosh_parsing(self): | |
563 | storage = 'file' # or ram | |
564 | post_limit = 128 * 1024 * 1024 | |
565 | url = '%s?STORAGE=%s&POST_LIMIT=%s' % (self.whoosh_url, storage, post_limit) | |
566 | url = Env.search_url_config(url) | |
567 | ||
568 | self.assertEqual(url['ENGINE'], 'haystack.backends.whoosh_backend.WhooshEngine') | |
569 | self.assertTrue('PATH' in url.keys()) | |
570 | self.assertEqual(url['PATH'], '/home/search/whoosh_index') | |
571 | self.assertTrue('STORAGE' in url.keys()) | |
572 | self.assertEqual(url['STORAGE'], storage) | |
573 | self.assertTrue('POST_LIMIT' in url.keys()) | |
574 | self.assertEqual(url['POST_LIMIT'], post_limit) | |
575 | self.assertTrue('INDEX_NAME' not in url) | |
576 | ||
577 | def test_xapian_parsing(self): | |
578 | flags = 'myflags' | |
579 | url = '%s?FLAGS=%s' % (self.xapian_url, flags) | |
580 | url = Env.search_url_config(url) | |
581 | ||
582 | self.assertEqual(url['ENGINE'], 'haystack.backends.xapian_backend.XapianEngine') | |
583 | self.assertTrue('PATH' in url.keys()) | |
584 | self.assertEqual(url['PATH'], '/home/search/xapian_index') | |
585 | self.assertTrue('FLAGS' in url.keys()) | |
586 | self.assertEqual(url['FLAGS'], flags) | |
587 | self.assertTrue('INDEX_NAME' not in url) | |
588 | ||
589 | def test_simple_parsing(self): | |
590 | url = Env.search_url_config(self.simple_url) | |
591 | ||
592 | self.assertEqual(url['ENGINE'], 'haystack.backends.simple_backend.SimpleEngine') | |
593 | self.assertTrue('INDEX_NAME' not in url) | |
594 | self.assertTrue('PATH' not in url) | |
595 | ||
596 | def test_common_args_parsing(self): | |
597 | excluded_indexes = 'myapp.indexes.A,myapp.indexes.B' | |
598 | include_spelling = 1 | |
599 | batch_size = 100 | |
600 | params = 'EXCLUDED_INDEXES=%s&INCLUDE_SPELLING=%s&BATCH_SIZE=%s' % ( | |
601 | excluded_indexes, | |
602 | include_spelling, | |
603 | batch_size | |
604 | ) | |
605 | for url in [ | |
606 | self.solr_url, | |
607 | self.elasticsearch_url, | |
608 | self.whoosh_url, | |
609 | self.xapian_url, | |
610 | self.simple_url, | |
611 | ]: | |
612 | url = '?'.join([url, params]) | |
613 | url = Env.search_url_config(url) | |
614 | ||
615 | self.assertTrue('EXCLUDED_INDEXES' in url.keys()) | |
616 | self.assertTrue('myapp.indexes.A' in url['EXCLUDED_INDEXES']) | |
617 | self.assertTrue('myapp.indexes.B' in url['EXCLUDED_INDEXES']) | |
618 | self.assertTrue('INCLUDE_SPELLING'in url.keys()) | |
619 | self.assertTrue(url['INCLUDE_SPELLING']) | |
620 | self.assertTrue('BATCH_SIZE' in url.keys()) | |
621 | self.assertEqual(url['BATCH_SIZE'], 100) | |
622 | ||
623 | ||
624 | class EmailTests(unittest.TestCase): | |
625 | ||
626 | def test_smtp_parsing(self): | |
627 | url = 'smtps://user@domain.com:password@smtp.example.com:587' | |
628 | url = Env.email_url_config(url) | |
629 | ||
630 | self.assertEqual(url['EMAIL_BACKEND'], 'django.core.mail.backends.smtp.EmailBackend') | |
631 | self.assertEqual(url['EMAIL_HOST'], 'smtp.example.com') | |
632 | self.assertEqual(url['EMAIL_HOST_PASSWORD'], 'password') | |
633 | self.assertEqual(url['EMAIL_HOST_USER'], 'user@domain.com') | |
634 | self.assertEqual(url['EMAIL_PORT'], 587) | |
635 | self.assertEqual(url['EMAIL_USE_TLS'], True) | |
636 | ||
637 | ||
638 | class PathTests(unittest.TestCase): | |
639 | ||
640 | def test_path_class(self): | |
641 | ||
642 | root = Path(__file__, '..', is_file=True) | |
643 | root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../')) | |
644 | self.assertEqual(root(), root_path) | |
645 | self.assertEqual(root.__root__, root_path) | |
646 | ||
647 | web = root.path('public') | |
648 | self.assertEqual(web(), os.path.join(root_path, 'public')) | |
649 | self.assertEqual(web('css'), os.path.join(root_path, 'public', 'css')) | |
650 | ||
651 | def test_required_path(self): | |
652 | ||
653 | self.assertRaises(ImproperlyConfigured, Path, '/not/existing/path/', required=True) | |
654 | self.assertRaises(ImproperlyConfigured, Path(__file__), 'not_existing_path', required=True) | |
655 | ||
656 | def test_comparison(self): | |
657 | ||
658 | self.assertTrue(Path('/home') in Path('/')) | |
659 | self.assertTrue(Path('/home') not in Path('/other/dir')) | |
660 | ||
661 | self.assertTrue(Path('/home') == Path('/home')) | |
662 | self.assertTrue(Path('/home') != Path('/home/dev')) | |
663 | ||
664 | self.assertEqual(Path('/home/foo/').rfind('/'), str(Path('/home/foo')).rfind('/')) | |
665 | self.assertEqual(Path('/home/foo/').find('/home'), str(Path('/home/foo/')).find('/home')) | |
666 | self.assertEqual(Path('/home/foo/')[1], str(Path('/home/foo/'))[1]) | |
667 | ||
668 | self.assertEqual(~Path('/home'), Path('/')) | |
669 | self.assertEqual(Path('/') + 'home', Path('/home')) | |
670 | self.assertEqual(Path('/') + '/home/public', Path('/home/public')) | |
671 | self.assertEqual(Path('/home/dev/public') - 2, Path('/home')) | |
672 | self.assertEqual(Path('/home/dev/public') - 'public', Path('/home/dev')) | |
673 | ||
674 | self.assertRaises(TypeError, lambda _: Path('/home/dev/') - 'not int') | |
675 | ||
676 | ||
677 | def load_suite(): | |
678 | ||
679 | test_suite = unittest.TestSuite() | |
680 | cases = [ | |
681 | EnvTests, FileEnvTests, SubClassTests, SchemaEnvTests, PathTests, | |
682 | DatabaseTestSuite, CacheTestSuite, EmailTests, SearchTestSuite | |
683 | ] | |
684 | for case in cases: | |
685 | test_suite.addTest(unittest.makeSuite(case)) | |
686 | return test_suite | |
687 | ||
688 | ||
689 | if __name__ == "__main__": | |
690 | ||
691 | try: | |
692 | if sys.argv[1] == '-o': | |
693 | for key, value in BaseTests.generateData().items(): | |
694 | print("{0}={1}".format(key, value)) | |
695 | sys.exit() | |
696 | except IndexError: | |
697 | pass | |
698 | ||
699 | unittest.TextTestRunner().run(load_suite()) |
0 | DICT_VAR=foo=bar,test=on | |
1 | BOOL_FALSE_VAR2=False | |
2 | DATABASE_MYSQL_URL=mysql://bea6eb0:69772142@us-cdbr-east.cleardb.com/heroku_97681?reconnect=true | |
3 | DATABASE_MYSQL_GIS_URL=mysqlgis://user:password@127.0.0.1/some_database | |
4 | CACHE_URL=memcache://127.0.0.1:11211 | |
5 | CACHE_REDIS=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=secret | |
6 | EMAIL_URL=smtps://user@domain.com:password@smtp.example.com:587 | |
7 | URL_VAR=http://www.google.com/ | |
8 | PATH_VAR=/home/dev | |
9 | BOOL_FALSE_VAR=0 | |
10 | BOOL_TRUE_VAR2=True | |
11 | DATABASE_SQLITE_URL=sqlite:////full/path/to/your/database/file.sqlite | |
12 | JSON_VAR={"three": 33.44, "two": 2, "one": "bar"} | |
13 | BOOL_TRUE_VAR=1 | |
14 | DATABASE_URL=postgres://uf07k1:wegauwhg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722 | |
15 | FLOAT_VAR=33.3 | |
16 | FLOAT_COMMA_VAR=33,3 | |
17 | FLOAT_STRANGE_VAR1=123,420,333.3 | |
18 | FLOAT_STRANGE_VAR2=123.420.333,3 | |
19 | PROXIED_VAR=$STR_VAR | |
20 | EMPTY_LIST= | |
21 | INT_VAR=42 | |
22 | STR_LIST_WITH_SPACES= foo, bar | |
23 | STR_VAR=bar | |
24 | INT_LIST=42,33 | |
25 | CYRILLIC_VAR=фуубар | |
26 | INT_TUPLE=(42,33) | |
27 | DATABASE_ORACLE_TNS_URL=oracle://user:password@sid | |
28 | DATABASE_ORACLE_URL=oracle://user:password@host:1521/sid | |
29 | DATABASE_REDSHIFT_URL=redshift://user:password@examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev |
0 | [bumpversion] | |
1 | current_version = 0.4.4 | |
2 | commit = True | |
3 | tag = True | |
4 | files = setup.py environ/environ.py docs/conf.py | |
5 | ||
6 | 0 | [bdist_wheel] |
7 | 1 | universal = 1 |
8 | 2 |
0 | 0 | #!/usr/bin/env python |
1 | ||
2 | from __future__ import unicode_literals | |
3 | from setuptools import setup, find_packages | |
4 | import io | |
5 | import os | |
6 | ||
7 | here = os.path.abspath(os.path.dirname(__file__)) | |
8 | README = io.open(os.path.join(here, 'README.rst'), encoding="utf8").read() | |
9 | ||
10 | version = '0.4.4' | |
11 | author = 'joke2k' | |
12 | description = "Django-environ allows you to utilize 12factor inspired environment " \ | |
13 | "variables to configure your Django application." | |
14 | install_requires = ['django', 'six'] | |
15 | ||
16 | setup(name='django-environ', | |
17 | version=version, | |
18 | description=description, | |
19 | long_description=README, | |
20 | classifiers=[ | |
21 | # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers | |
22 | 'Development Status :: 3 - Alpha', | |
23 | 'Intended Audience :: Information Technology', | |
24 | 'Framework :: Django', | |
25 | 'Framework :: Django :: 1.8', | |
26 | 'Framework :: Django :: 1.9', | |
27 | 'Framework :: Django :: 1.10', | |
28 | 'Framework :: Django :: 1.11', | |
29 | 'Programming Language :: Python', | |
30 | 'Programming Language :: Python :: 2', | |
31 | 'Programming Language :: Python :: 2.7', | |
32 | 'Programming Language :: Python :: 3', | |
33 | 'Programming Language :: Python :: 3.4', | |
34 | 'Programming Language :: Python :: 3.5', | |
35 | 'Programming Language :: Python :: Implementation :: CPython', | |
36 | 'Programming Language :: Python :: Implementation :: PyPy', | |
37 | 'Topic :: Software Development :: Libraries :: Python Modules', | |
38 | 'Topic :: Utilities', | |
39 | 'License :: OSI Approved :: MIT License', | |
40 | 'Framework :: Django' | |
41 | ], | |
42 | keywords='django environment variables 12factor', | |
43 | author=author, | |
44 | author_email='joke2k@gmail.com', | |
45 | url='https://github.com/joke2k/django-environ', | |
46 | license='MIT License', | |
47 | packages=find_packages(), | |
48 | platforms=["any"], | |
49 | include_package_data=True, | |
50 | test_suite='environ.test.load_suite', | |
51 | zip_safe=False, | |
52 | install_requires=install_requires, | |
53 | ) | |
1 | # | |
2 | # This file is part of the django-environ. | |
3 | # | |
4 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
5 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
6 | # | |
7 | # For the full copyright and license information, please view | |
8 | # the LICENSE.txt file that was distributed with this source code. | |
9 | ||
10 | import codecs | |
11 | import re | |
12 | from os import path | |
13 | ||
14 | from setuptools import find_packages, setup | |
15 | ||
16 | ||
17 | def read_file(filepath): | |
18 | """Read content from a UTF-8 encoded text file.""" | |
19 | with codecs.open(filepath, 'rb', 'utf-8') as file_handle: | |
20 | return file_handle.read() | |
21 | ||
22 | ||
23 | PKG_NAME = 'django-environ' | |
24 | PKG_DIR = path.abspath(path.dirname(__file__)) | |
25 | META_PATH = path.join(PKG_DIR, 'environ', '__init__.py') | |
26 | META_CONTENTS = read_file(META_PATH) | |
27 | ||
28 | ||
29 | def load_long_description(): | |
30 | """Load long description from file README.rst.""" | |
31 | def changes(): | |
32 | changelog = path.join(PKG_DIR, 'CHANGELOG.rst') | |
33 | pattern = ( | |
34 | r'(`(v\d+.\d+.\d+)`_( - \d{1,2}-\w+-\d{4}\r?\n-+\r?\n.*?))' | |
35 | r'\r?\n\r?\n\r?\n`v\d+.\d+.\d+`_' | |
36 | ) | |
37 | result = re.search(pattern, read_file(changelog), re.S) | |
38 | ||
39 | return result.group(2) + result.group(3) if result else '' | |
40 | ||
41 | try: | |
42 | title = PKG_NAME | |
43 | head = '=' * (len(title)) | |
44 | ||
45 | contents = ( | |
46 | head, | |
47 | format(title.strip(' .')), | |
48 | head, | |
49 | read_file(path.join(PKG_DIR, 'README.rst')).split( | |
50 | '.. -teaser-begin-' | |
51 | )[1], | |
52 | '', | |
53 | read_file(path.join(PKG_DIR, 'CONTRIBUTING.rst')), | |
54 | '', | |
55 | 'Release Information', | |
56 | '===================\n', | |
57 | changes(), | |
58 | '', | |
59 | '`Full changelog <{}/en/latest/changelog.html>`_.'.format( | |
60 | find_meta('url') | |
61 | ), | |
62 | '', | |
63 | read_file(path.join(PKG_DIR, 'SECURITY.rst')), | |
64 | '', | |
65 | read_file(path.join(PKG_DIR, 'AUTHORS.rst')), | |
66 | ) | |
67 | ||
68 | return '\n'.join(contents) | |
69 | except (RuntimeError, FileNotFoundError) as read_error: | |
70 | message = 'Long description could not be read from README.rst' | |
71 | raise RuntimeError('%s: %s' % (message, read_error)) from read_error | |
72 | ||
73 | ||
74 | def is_canonical_version(version): | |
75 | """Check if a version string is in the canonical format of PEP 440.""" | |
76 | pattern = ( | |
77 | r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))' | |
78 | r'*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))' | |
79 | r'?(\.dev(0|[1-9][0-9]*))?$') | |
80 | return re.match(pattern, version) is not None | |
81 | ||
82 | ||
83 | def find_meta(meta): | |
84 | """Extract __*meta*__ from META_CONTENTS.""" | |
85 | meta_match = re.search( | |
86 | r"^__{meta}__\s+=\s+['\"]([^'\"]*)['\"]".format(meta=meta), | |
87 | META_CONTENTS, | |
88 | re.M | |
89 | ) | |
90 | ||
91 | if meta_match: | |
92 | return meta_match.group(1) | |
93 | raise RuntimeError( | |
94 | 'Unable to find __%s__ string in package meta file' % meta) | |
95 | ||
96 | ||
97 | def get_version_string(): | |
98 | """Return package version as listed in `__version__` in meta file.""" | |
99 | # Parse version string | |
100 | version_string = find_meta('version') | |
101 | ||
102 | # Check validity | |
103 | if not is_canonical_version(version_string): | |
104 | message = ( | |
105 | 'The detected version string "{}" is not in canonical ' | |
106 | 'format as defined in PEP 440.'.format(version_string)) | |
107 | raise ValueError(message) | |
108 | ||
109 | return version_string | |
110 | ||
111 | ||
112 | # What does this project relate to. | |
113 | KEYWORDS = [ | |
114 | 'environment', | |
115 | 'django', | |
116 | 'variables', | |
117 | '12factor', | |
118 | ] | |
119 | ||
120 | # Classifiers: available ones listed at https://pypi.org/classifiers | |
121 | CLASSIFIERS = [ | |
122 | 'Development Status :: 5 - Production/Stable', | |
123 | ||
124 | 'Framework :: Django', | |
125 | 'Framework :: Django :: 1.11', | |
126 | 'Framework :: Django :: 2.0', | |
127 | 'Framework :: Django :: 2.1', | |
128 | 'Framework :: Django :: 2.2', | |
129 | 'Framework :: Django :: 3.0', | |
130 | 'Framework :: Django :: 3.1', | |
131 | 'Framework :: Django :: 3.2', | |
132 | 'Framework :: Django :: 4.0', | |
133 | ||
134 | 'Operating System :: OS Independent', | |
135 | ||
136 | 'Intended Audience :: Developers', | |
137 | 'Natural Language :: English', | |
138 | ||
139 | 'Programming Language :: Python', | |
140 | 'Programming Language :: Python :: 3', | |
141 | 'Programming Language :: Python :: 3.4', | |
142 | 'Programming Language :: Python :: 3.5', | |
143 | 'Programming Language :: Python :: 3.6', | |
144 | 'Programming Language :: Python :: 3.7', | |
145 | 'Programming Language :: Python :: 3.8', | |
146 | 'Programming Language :: Python :: 3.9', | |
147 | 'Programming Language :: Python :: 3.10', | |
148 | 'Programming Language :: Python :: Implementation :: CPython', | |
149 | 'Programming Language :: Python :: Implementation :: PyPy', | |
150 | ||
151 | 'Topic :: Software Development :: Libraries :: Python Modules', | |
152 | 'Topic :: Utilities', | |
153 | ||
154 | 'License :: OSI Approved :: MIT License', | |
155 | ] | |
156 | ||
157 | # Dependencies that are downloaded by pip on installation and why. | |
158 | INSTALL_REQUIRES = [] | |
159 | ||
160 | DEPENDENCY_LINKS = [] | |
161 | ||
162 | # List additional groups of dependencies here (e.g. testing dependencies). | |
163 | # You can install these using the following syntax, for example: | |
164 | # | |
165 | # $ pip install -e .[testing,docs,develop] | |
166 | # | |
167 | EXTRAS_REQUIRE = { | |
168 | # Dependencies that are required to run tests | |
169 | 'testing': [ | |
170 | 'coverage[toml]>=5.0a4', # Code coverage measurement for Python | |
171 | 'pytest>=4.6.11', # Our tests framework | |
172 | ], | |
173 | # Dependencies that are required to build documentation | |
174 | 'docs': [ | |
175 | 'furo>=2021.8.17b43,==2021.8.*', # Sphinx documentation theme | |
176 | 'sphinx>=3.5.0', # Python documentation generator | |
177 | 'sphinx-notfound-page', # Create a custom 404 page | |
178 | ], | |
179 | } | |
180 | ||
181 | # Dependencies that are required to develop package | |
182 | DEVELOP_REQUIRE = [] | |
183 | ||
184 | # Dependencies that are required to develop package | |
185 | EXTRAS_REQUIRE['develop'] = \ | |
186 | DEVELOP_REQUIRE + EXTRAS_REQUIRE['testing'] + EXTRAS_REQUIRE['docs'] | |
187 | ||
188 | # Project's URLs | |
189 | PROJECT_URLS = { | |
190 | 'Documentation': find_meta('url'), | |
191 | 'Funding': 'https://opencollective.com/django-environ', | |
192 | 'Say Thanks!': 'https://saythanks.io/to/joke2k', | |
193 | 'Changelog': '{}/en/latest/changelog.html'.format(find_meta('url')), | |
194 | 'Bug Tracker': 'https://github.com/joke2k/django-environ/issues', | |
195 | 'Source Code': 'https://github.com/joke2k/django-environ', | |
196 | } | |
197 | ||
198 | ||
199 | if __name__ == '__main__': | |
200 | setup( | |
201 | name=PKG_NAME, | |
202 | version=get_version_string(), | |
203 | author=find_meta('author'), | |
204 | author_email=find_meta('author_email'), | |
205 | maintainer=find_meta('maintainer'), | |
206 | maintainer_email=find_meta('maintainer_email'), | |
207 | license=find_meta('license'), | |
208 | description=find_meta('description'), | |
209 | long_description=load_long_description(), | |
210 | long_description_content_type='text/x-rst', | |
211 | keywords=KEYWORDS, | |
212 | url=find_meta('url'), | |
213 | project_urls=PROJECT_URLS, | |
214 | classifiers=CLASSIFIERS, | |
215 | packages=find_packages(exclude=['tests.*', 'tests']), | |
216 | platforms=['any'], | |
217 | include_package_data=True, | |
218 | zip_safe=False, | |
219 | python_requires='>=3.4,<4', | |
220 | install_requires=INSTALL_REQUIRES, | |
221 | dependency_links=DEPENDENCY_LINKS, | |
222 | extras_require=EXTRAS_REQUIRE, | |
223 | ) |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | def assert_type_and_value(type_, expected, actual): | |
9 | assert isinstance(actual, type_) | |
10 | assert actual == expected |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import os | |
9 | import pathlib | |
10 | import sys | |
11 | ||
12 | import pytest | |
13 | ||
14 | ||
15 | @pytest.fixture | |
16 | def solr_url(): | |
17 | """Return Solr URL.""" | |
18 | return 'solr://127.0.0.1:8983/solr' | |
19 | ||
20 | ||
21 | @pytest.fixture | |
22 | def whoosh_url(): | |
23 | """Return Whoosh URL.""" | |
24 | return 'whoosh:///home/search/whoosh_index' | |
25 | ||
26 | ||
27 | @pytest.fixture | |
28 | def xapian_url(): | |
29 | """Return Xapian URL.""" | |
30 | return 'xapian:///home/search/xapian_index' | |
31 | ||
32 | ||
33 | @pytest.fixture | |
34 | def simple_url(): | |
35 | """Return simple URL.""" | |
36 | return 'simple:///' | |
37 | ||
38 | ||
39 | @pytest.fixture | |
40 | def volume(): | |
41 | """Return volume name is OS is Windows, otherwise None.""" | |
42 | if sys.platform == 'win32': | |
43 | return pathlib.Path(os.getcwd()).parts[0] | |
44 | return None | |
45 | ||
46 | ||
47 | @pytest.fixture(params=[ | |
48 | 'solr://127.0.0.1:8983/solr', | |
49 | 'elasticsearch://127.0.0.1:9200/index', | |
50 | 'whoosh:///home/search/whoosh_index', | |
51 | 'xapian:///home/search/xapian_index', | |
52 | 'simple:///' | |
53 | ]) | |
54 | def search_url(request): | |
55 | """Return Search Engine URL.""" | |
56 | return request.param |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | from environ.compat import json | |
9 | ||
10 | ||
11 | class FakeEnv: | |
12 | URL = 'http://www.google.com/' | |
13 | POSTGRES = 'postgres://uf07k1:wegauwhg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722' | |
14 | MYSQL = 'mysql://bea6eb0:69772142@us-cdbr-east.cleardb.com/heroku_97681?reconnect=true' | |
15 | MYSQL_CLOUDSQL_URL = 'mysql://djuser:hidden-password@//cloudsql/arvore-codelab:us-central1:mysqlinstance/mydatabase' | |
16 | MYSQLGIS = 'mysqlgis://user:password@127.0.0.1/some_database' | |
17 | SQLITE = 'sqlite:////full/path/to/your/database/file.sqlite' | |
18 | ORACLE_TNS = 'oracle://user:password@sid/' | |
19 | ORACLE = 'oracle://user:password@host:1521/sid' | |
20 | CUSTOM_BACKEND = 'custom.backend://user:password@example.com:5430/database' | |
21 | REDSHIFT = 'redshift://user:password@examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev' | |
22 | MEMCACHE = 'memcache://127.0.0.1:11211' | |
23 | REDIS = 'rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=secret' | |
24 | EMAIL = 'smtps://user@domain.com:password@smtp.example.com:587' | |
25 | JSON = dict(one='bar', two=2, three=33.44) | |
26 | DICT = dict(foo='bar', test='on') | |
27 | PATH = '/home/dev' | |
28 | EXPORTED = 'exported var' | |
29 | SAML_ATTRIBUTE_MAPPING = dict( | |
30 | uid=('username',), | |
31 | mail=('email',), | |
32 | cn=('first_name',), | |
33 | sn=('last_name',) | |
34 | ) | |
35 | ||
36 | @classmethod | |
37 | def generate_data(cls): | |
38 | return dict(STR_VAR='bar', | |
39 | MULTILINE_STR_VAR='foo\\nbar', | |
40 | MULTILINE_QUOTED_STR_VAR='---BEGIN---\\r\\n---END---', | |
41 | MULTILINE_ESCAPED_STR_VAR='---BEGIN---\\\\n---END---', | |
42 | INT_VAR='42', | |
43 | FLOAT_VAR='33.3', | |
44 | FLOAT_COMMA_VAR='33,3', | |
45 | FLOAT_STRANGE_VAR1='123,420,333.3', | |
46 | FLOAT_STRANGE_VAR2='123.420.333,3', | |
47 | FLOAT_NEGATIVE_VAR='-1.0', | |
48 | BOOL_TRUE_STRING_LIKE_INT='1', | |
49 | BOOL_TRUE_INT=1, | |
50 | BOOL_TRUE_STRING_LIKE_BOOL='True', | |
51 | BOOL_TRUE_STRING_1='on', | |
52 | BOOL_TRUE_STRING_2='ok', | |
53 | BOOL_TRUE_STRING_3='yes', | |
54 | BOOL_TRUE_STRING_4='y', | |
55 | BOOL_TRUE_STRING_5='true', | |
56 | BOOL_TRUE_BOOL=True, | |
57 | BOOL_FALSE_STRING_LIKE_INT='0', | |
58 | BOOL_FALSE_INT=0, | |
59 | BOOL_FALSE_STRING_LIKE_BOOL='False', | |
60 | BOOL_FALSE_BOOL=False, | |
61 | PROXIED_VAR='$STR_VAR', | |
62 | ESCAPED_VAR=r'\$baz', | |
63 | INT_LIST='42,33', | |
64 | INT_TUPLE='(42,33)', | |
65 | MIX_TUPLE='(42,Test)', | |
66 | STR_LIST_WITH_SPACES=' foo, bar', | |
67 | EMPTY_LIST='', | |
68 | DICT_VAR='foo=bar,test=on', | |
69 | DATABASE_URL=cls.POSTGRES, | |
70 | DATABASE_MYSQL_URL=cls.MYSQL, | |
71 | DATABASE_MYSQL_GIS_URL=cls.MYSQLGIS, | |
72 | DATABASE_SQLITE_URL=cls.SQLITE, | |
73 | DATABASE_ORACLE_URL=cls.ORACLE, | |
74 | DATABASE_ORACLE_TNS_URL=cls.ORACLE_TNS, | |
75 | DATABASE_REDSHIFT_URL=cls.REDSHIFT, | |
76 | DATABASE_CUSTOM_BACKEND_URL=cls.CUSTOM_BACKEND, | |
77 | DATABASE_MYSQL_CLOUDSQL_URL=cls.MYSQL_CLOUDSQL_URL, | |
78 | CACHE_URL=cls.MEMCACHE, | |
79 | CACHE_REDIS=cls.REDIS, | |
80 | EMAIL_URL=cls.EMAIL, | |
81 | URL_VAR=cls.URL, | |
82 | JSON_VAR=json.dumps(cls.JSON), | |
83 | PATH_VAR=cls.PATH, | |
84 | EXPORTED_VAR=cls.EXPORTED, | |
85 | SAML_ATTRIBUTE_MAPPING='uid=username;mail=email;cn=first_name;sn=last_name;', | |
86 | PREFIX_TEST='foo', | |
87 | ) |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | from unittest import mock | |
9 | ||
10 | import pytest | |
11 | ||
12 | import environ.compat | |
13 | from environ import Env | |
14 | from environ.compat import PYMEMCACHE_DRIVER, REDIS_DRIVER, ImproperlyConfigured | |
15 | ||
16 | ||
17 | def test_base_options_parsing(): | |
18 | url = ('memcache://127.0.0.1:11211/?timeout=0&' | |
19 | 'key_prefix=cache_&key_function=foo.get_key&version=1') | |
20 | url = Env.cache_url_config(url) | |
21 | ||
22 | assert url['KEY_PREFIX'] == 'cache_' | |
23 | assert url['KEY_FUNCTION'] == 'foo.get_key' | |
24 | assert url['TIMEOUT'] == 0 | |
25 | assert url['VERSION'] == 1 | |
26 | ||
27 | url = 'redis://127.0.0.1:6379/?timeout=None' | |
28 | url = Env.cache_url_config(url) | |
29 | ||
30 | assert url['TIMEOUT'] is None | |
31 | ||
32 | ||
33 | @pytest.mark.parametrize( | |
34 | 'url,backend,location', | |
35 | [ | |
36 | ('dbcache://my_cache_table', | |
37 | 'django.core.cache.backends.db.DatabaseCache', 'my_cache_table'), | |
38 | ('filecache:///var/tmp/django_cache', | |
39 | 'django.core.cache.backends.filebased.FileBasedCache', | |
40 | '/var/tmp/django_cache'), | |
41 | ('filecache://C:/foo/bar', | |
42 | 'django.core.cache.backends.filebased.FileBasedCache', 'C:/foo/bar'), | |
43 | ('locmemcache://', | |
44 | 'django.core.cache.backends.locmem.LocMemCache', ''), | |
45 | ('locmemcache://unique-snowflake', | |
46 | 'django.core.cache.backends.locmem.LocMemCache', 'unique-snowflake'), | |
47 | ('dummycache://', | |
48 | 'django.core.cache.backends.dummy.DummyCache', ''), | |
49 | ('rediss://127.0.0.1:6379/1', REDIS_DRIVER, | |
50 | 'rediss://127.0.0.1:6379/1'), | |
51 | ('rediscache://:redispass@127.0.0.1:6379/0', REDIS_DRIVER, | |
52 | 'redis://:redispass@127.0.0.1:6379/0'), | |
53 | ('rediscache://host1:6379,host2:6379,host3:9999/1', REDIS_DRIVER, | |
54 | ['redis://host1:6379/1', 'redis://host2:6379/1', | |
55 | 'redis://host3:9999/1']), | |
56 | ('rediscache:///path/to/socket:1', 'django_redis.cache.RedisCache', | |
57 | 'unix:///path/to/socket:1'), | |
58 | ('memcache:///tmp/memcached.sock', | |
59 | 'django.core.cache.backends.memcached.MemcachedCache', | |
60 | 'unix:/tmp/memcached.sock'), | |
61 | ('memcache://172.19.26.240:11211,172.19.26.242:11212', | |
62 | 'django.core.cache.backends.memcached.MemcachedCache', | |
63 | ['172.19.26.240:11211', '172.19.26.242:11212']), | |
64 | ('memcache://127.0.0.1:11211', | |
65 | 'django.core.cache.backends.memcached.MemcachedCache', | |
66 | '127.0.0.1:11211'), | |
67 | ('pymemcache://127.0.0.1:11211', | |
68 | PYMEMCACHE_DRIVER, | |
69 | '127.0.0.1:11211'), | |
70 | ('pymemcache://memcached:11211/?key_prefix=ci', | |
71 | PYMEMCACHE_DRIVER, | |
72 | 'memcached:11211'), | |
73 | ], | |
74 | ids=[ | |
75 | 'dbcache', | |
76 | 'filecache', | |
77 | 'filecache_win', | |
78 | 'locmemcache_empty', | |
79 | 'locmemcache', | |
80 | 'dummycache', | |
81 | 'rediss', | |
82 | 'redis_with_password', | |
83 | 'redis_multiple', | |
84 | 'redis_socket', | |
85 | 'memcached_socket', | |
86 | 'memcached_multiple', | |
87 | 'memcached', | |
88 | 'pylibmccache', | |
89 | 'pylibmccache_trailing_slash', | |
90 | ], | |
91 | ) | |
92 | def test_cache_parsing(url, backend, location): | |
93 | url = Env.cache_url_config(url) | |
94 | ||
95 | assert url['BACKEND'] == backend | |
96 | assert url['LOCATION'] == location | |
97 | ||
98 | ||
99 | @pytest.mark.parametrize('django_version', ((3, 2), (3, 1), None)) | |
100 | @pytest.mark.parametrize('pymemcache_installed', (True, False)) | |
101 | def test_pymemcache_compat(django_version, pymemcache_installed): | |
102 | old = 'django.core.cache.backends.memcached.PyLibMCCache' | |
103 | new = 'django.core.cache.backends.memcached.PyMemcacheCache' | |
104 | with mock.patch.object(environ.compat, 'DJANGO_VERSION', django_version): | |
105 | with mock.patch('environ.compat.find_loader') as mock_find_loader: | |
106 | mock_find_loader.return_value = pymemcache_installed | |
107 | driver = environ.compat.choose_pymemcache_driver() | |
108 | if django_version and django_version < (3, 2): | |
109 | assert driver == old | |
110 | else: | |
111 | assert driver == new if pymemcache_installed else old | |
112 | ||
113 | ||
114 | def test_redis_parsing(): | |
115 | url = ('rediscache://127.0.0.1:6379/1?client_class=' | |
116 | 'django_redis.client.DefaultClient&password=secret') | |
117 | url = Env.cache_url_config(url) | |
118 | ||
119 | assert url['BACKEND'] == REDIS_DRIVER | |
120 | assert url['LOCATION'] == 'redis://127.0.0.1:6379/1' | |
121 | assert url['OPTIONS'] == { | |
122 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
123 | 'PASSWORD': 'secret', | |
124 | } | |
125 | ||
126 | ||
127 | def test_redis_socket_url(): | |
128 | url = 'redis://:redispass@/path/to/socket.sock?db=0' | |
129 | url = Env.cache_url_config(url) | |
130 | assert REDIS_DRIVER == url['BACKEND'] | |
131 | assert url['LOCATION'] == 'unix://:redispass@/path/to/socket.sock' | |
132 | assert url['OPTIONS'] == { | |
133 | 'DB': 0 | |
134 | } | |
135 | ||
136 | ||
137 | def test_options_parsing(): | |
138 | url = 'filecache:///var/tmp/django_cache?timeout=60&max_entries=1000&cull_frequency=0' | |
139 | url = Env.cache_url_config(url) | |
140 | ||
141 | assert url['BACKEND'] == 'django.core.cache.backends.filebased.FileBasedCache' | |
142 | assert url['LOCATION'] == '/var/tmp/django_cache' | |
143 | assert url['TIMEOUT'] == 60 | |
144 | assert url['OPTIONS'] == { | |
145 | 'MAX_ENTRIES': 1000, | |
146 | 'CULL_FREQUENCY': 0, | |
147 | } | |
148 | ||
149 | ||
150 | def test_custom_backend(): | |
151 | url = 'memcache://127.0.0.1:5400?foo=option&bars=9001' | |
152 | backend = 'django_redis.cache.RedisCache' | |
153 | url = Env.cache_url_config(url, backend) | |
154 | ||
155 | assert url['BACKEND'] == backend | |
156 | assert url['LOCATION'] == '127.0.0.1:5400' | |
157 | assert url['OPTIONS'] == { | |
158 | 'FOO': 'option', | |
159 | 'BARS': 9001, | |
160 | } | |
161 | ||
162 | ||
163 | def test_unknown_backend(): | |
164 | url = 'unknown-scheme://127.0.0.1:1000' | |
165 | with pytest.raises(ImproperlyConfigured) as excinfo: | |
166 | Env.cache_url_config(url) | |
167 | assert str(excinfo.value) == 'Invalid cache schema unknown-scheme' | |
168 | ||
169 | ||
170 | def test_empty_url_is_mapped_to_empty_config(): | |
171 | assert Env.cache_url_config('') == {} | |
172 | assert Env.cache_url_config(None) == {} | |
173 | ||
174 | ||
175 | @pytest.mark.parametrize( | |
176 | 'chars', | |
177 | ['!', '$', '&', "'", '(', ')', '*', '+', ';', '=', '-', '.', '-v1.2'] | |
178 | ) | |
179 | def test_cache_url_password_using_sub_delims(monkeypatch, chars): | |
180 | """Ensure CACHE_URL passwords may contains some unsafe characters. | |
181 | ||
182 | See: https://github.com/joke2k/django-environ/issues/200 for details.""" | |
183 | url = 'rediss://enigma:secret{}@ondigitalocean.com:25061/2'.format(chars) | |
184 | monkeypatch.setenv('CACHE_URL', url) | |
185 | env = Env() | |
186 | ||
187 | result = env.cache() | |
188 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
189 | assert result['LOCATION'] == url | |
190 | ||
191 | result = env.cache_url_config(url) | |
192 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
193 | assert result['LOCATION'] == url | |
194 | ||
195 | url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars) | |
196 | monkeypatch.setenv('CACHE_URL', url) | |
197 | env = Env() | |
198 | ||
199 | result = env.cache() | |
200 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
201 | assert result['LOCATION'] == url | |
202 | ||
203 | result = env.cache_url_config(url) | |
204 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
205 | assert result['LOCATION'] == url | |
206 | ||
207 | url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars) | |
208 | monkeypatch.setenv('CACHE_URL', url) | |
209 | env = Env() | |
210 | ||
211 | result = env.cache() | |
212 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
213 | assert result['LOCATION'] == url | |
214 | ||
215 | result = env.cache_url_config(url) | |
216 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
217 | assert result['LOCATION'] == url | |
218 | ||
219 | ||
220 | @pytest.mark.parametrize( | |
221 | 'chars', ['%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%2C'] | |
222 | ) | |
223 | def test_cache_url_password_using_gen_delims(monkeypatch, chars): | |
224 | """Ensure CACHE_URL passwords may contains %-encoded characters. | |
225 | ||
226 | See: https://github.com/joke2k/django-environ/issues/200 for details.""" | |
227 | url = 'rediss://enigma:secret{}@ondigitalocean.com:25061/2'.format(chars) | |
228 | monkeypatch.setenv('CACHE_URL', url) | |
229 | env = Env() | |
230 | ||
231 | result = env.cache() | |
232 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
233 | assert result['LOCATION'] == url | |
234 | ||
235 | url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars) | |
236 | monkeypatch.setenv('CACHE_URL', url) | |
237 | env = Env() | |
238 | ||
239 | result = env.cache() | |
240 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
241 | assert result['LOCATION'] == url | |
242 | ||
243 | url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars) | |
244 | monkeypatch.setenv('CACHE_URL', url) | |
245 | env = Env() | |
246 | ||
247 | result = env.cache() | |
248 | assert result['BACKEND'] == 'django_redis.cache.RedisCache' | |
249 | assert result['LOCATION'] == url | |
250 | ||
251 | ||
252 | def test_cache_url_env_using_default(): | |
253 | env = Env(CACHE_URL=(str, "locmemcache://")) | |
254 | result = env.cache() | |
255 | ||
256 | assert result["BACKEND"] == "django.core.cache.backends.locmem.LocMemCache" | |
257 | assert result["LOCATION"] == "" |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import warnings | |
9 | ||
10 | import pytest | |
11 | ||
12 | from environ import Env | |
13 | from environ.compat import DJANGO_POSTGRES | |
14 | ||
15 | ||
16 | @pytest.mark.parametrize( | |
17 | 'url,engine,name,host,user,passwd,port', | |
18 | [ | |
19 | # postgres://user:password@host:port/dbname | |
20 | ('postgres://enigma:secret@example.com:5431/dbname', | |
21 | DJANGO_POSTGRES, | |
22 | 'dbname', | |
23 | 'example.com', | |
24 | 'enigma', | |
25 | 'secret', | |
26 | 5431), | |
27 | # postgres://path/dbname | |
28 | ('postgres:////var/run/postgresql/dbname', | |
29 | DJANGO_POSTGRES, | |
30 | 'dbname', | |
31 | '/var/run/postgresql', | |
32 | '', | |
33 | '', | |
34 | ''), | |
35 | # postgis://user:password@host:port/dbname | |
36 | ('postgis://enigma:secret@example.com:5431/dbname', | |
37 | 'django.contrib.gis.db.backends.postgis', | |
38 | 'dbname', | |
39 | 'example.com', | |
40 | 'enigma', | |
41 | 'secret', | |
42 | 5431), | |
43 | # postgres://user:password@host:port,host:port,host:port/dbname | |
44 | ('postgres://username:p@ss:12,wor:34d@host1:111,22.55.44.88:222,[2001:db8::1234]:333/db', | |
45 | DJANGO_POSTGRES, | |
46 | 'db', | |
47 | 'host1,22.55.44.88,[2001:db8::1234]', | |
48 | 'username', | |
49 | 'p@ss:12,wor:34d', | |
50 | '111,222,333' | |
51 | ), | |
52 | # postgres://host,host,host/dbname | |
53 | ('postgres://node1,node2,node3/db', | |
54 | DJANGO_POSTGRES, | |
55 | 'db', | |
56 | 'node1,node2,node3', | |
57 | '', | |
58 | '', | |
59 | '' | |
60 | ), | |
61 | # mysqlgis://user:password@host:port/dbname | |
62 | ('mysqlgis://enigma:secret@example.com:5431/dbname', | |
63 | 'django.contrib.gis.db.backends.mysql', | |
64 | 'dbname', | |
65 | 'example.com', | |
66 | 'enigma', | |
67 | 'secret', | |
68 | 5431), | |
69 | # mysql://user:password@host/dbname?options | |
70 | ('mysql://enigma:secret@reconnect.com/dbname?reconnect=true', | |
71 | 'django.db.backends.mysql', | |
72 | 'dbname', | |
73 | 'reconnect.com', | |
74 | 'enigma', | |
75 | 'secret', | |
76 | ''), | |
77 | # mysql://user@host/dbname | |
78 | ('mysql://enigma@localhost/dbname', | |
79 | 'django.db.backends.mysql', | |
80 | 'dbname', | |
81 | 'localhost', | |
82 | 'enigma', | |
83 | '', | |
84 | ''), | |
85 | # sqlite:// | |
86 | ('sqlite://', | |
87 | 'django.db.backends.sqlite3', | |
88 | ':memory:', | |
89 | '', | |
90 | '', | |
91 | '', | |
92 | ''), | |
93 | # sqlite:////absolute/path/to/db/file | |
94 | ('sqlite:////full/path/to/your/file.sqlite', | |
95 | 'django.db.backends.sqlite3', | |
96 | '/full/path/to/your/file.sqlite', | |
97 | '', | |
98 | '', | |
99 | '', | |
100 | ''), | |
101 | # sqlite://:memory: | |
102 | ('sqlite://:memory:', | |
103 | 'django.db.backends.sqlite3', | |
104 | ':memory:', | |
105 | '', | |
106 | '', | |
107 | '', | |
108 | ''), | |
109 | # ldap://user:password@host | |
110 | ('ldap://cn=admin,dc=nodomain,dc=org:secret@example.com', | |
111 | 'ldapdb.backends.ldap', | |
112 | 'ldap://example.com', | |
113 | 'example.com', | |
114 | 'cn=admin,dc=nodomain,dc=org', | |
115 | 'secret', | |
116 | ''), | |
117 | # mysql://user:password@host/dbname | |
118 | ('mssql://enigma:secret@example.com/dbname' | |
119 | '?driver=ODBC Driver 13 for SQL Server', | |
120 | 'sql_server.pyodbc', | |
121 | 'dbname', | |
122 | 'example.com', | |
123 | 'enigma', | |
124 | 'secret', | |
125 | ''), | |
126 | # mysql://user:password@host:port/dbname | |
127 | ('mssql://enigma:secret@amazonaws.com\\insnsnss:12345/dbname' | |
128 | '?driver=ODBC Driver 13 for SQL Server', | |
129 | 'sql_server.pyodbc', | |
130 | 'dbname', | |
131 | 'amazonaws.com\\insnsnss', | |
132 | 'enigma', | |
133 | 'secret', | |
134 | 12345), | |
135 | ], | |
136 | ids=[ | |
137 | 'postgres', | |
138 | 'postgres_unix_domain', | |
139 | 'postgis', | |
140 | 'postgres_cluster', | |
141 | 'postgres_no_ports', | |
142 | 'mysqlgis', | |
143 | 'cleardb', | |
144 | 'mysql_no_password', | |
145 | 'sqlite_empty', | |
146 | 'sqlite_file', | |
147 | 'sqlite_memory', | |
148 | 'ldap', | |
149 | 'mssql', | |
150 | 'mssql_port', | |
151 | ], | |
152 | ) | |
153 | def test_db_parsing(url, engine, name, host, user, passwd, port): | |
154 | config = Env.db_url_config(url) | |
155 | ||
156 | assert config['ENGINE'] == engine | |
157 | assert config['NAME'] == name | |
158 | ||
159 | if url != 'sqlite://:memory:': | |
160 | assert config['PORT'] == port | |
161 | assert config['PASSWORD'] == passwd | |
162 | assert config['USER'] == user | |
163 | assert config['HOST'] == host | |
164 | ||
165 | if engine == 'sql_server.pyodbc': | |
166 | assert config['OPTIONS'] == {'driver': 'ODBC Driver 13 for SQL Server'} | |
167 | ||
168 | if host == 'reconnect.com': | |
169 | assert config['OPTIONS'] == {'reconnect': 'true'} | |
170 | ||
171 | ||
172 | ||
173 | def test_postgres_complex_db_name_parsing(): | |
174 | """Make sure we can use complex postgres host.""" | |
175 | env_url = ( | |
176 | 'postgres://user:password@//cloudsql/' | |
177 | 'project-1234:us-central1:instance/dbname' | |
178 | ) | |
179 | ||
180 | url = Env.db_url_config(env_url) | |
181 | ||
182 | assert url['ENGINE'] == DJANGO_POSTGRES | |
183 | assert url['HOST'] == '/cloudsql/project-1234:us-central1:instance' | |
184 | assert url['NAME'] == 'dbname' | |
185 | assert url['USER'] == 'user' | |
186 | assert url['PASSWORD'] == 'password' | |
187 | assert url['PORT'] == '' | |
188 | ||
189 | ||
190 | @pytest.mark.parametrize( | |
191 | 'scheme', | |
192 | ['postgres', 'postgresql', 'psql', 'pgsql', 'postgis'], | |
193 | ) | |
194 | def test_postgres_like_scheme_parsing(scheme): | |
195 | """Verify all the postgres-like schemes parsed the same as postgres.""" | |
196 | env_url1 = ( | |
197 | 'postgres://user:password@//cloudsql/' | |
198 | 'project-1234:us-central1:instance/dbname' | |
199 | ) | |
200 | env_url2 = ( | |
201 | '{}://user:password@//cloudsql/' | |
202 | 'project-1234:us-central1:instance/dbname' | |
203 | ).format(scheme) | |
204 | ||
205 | url1 = Env.db_url_config(env_url1) | |
206 | url2 = Env.db_url_config(env_url2) | |
207 | ||
208 | assert url2['NAME'] == url1['NAME'] | |
209 | assert url2['PORT'] == url1['PORT'] | |
210 | assert url2['PASSWORD'] == url1['PASSWORD'] | |
211 | assert url2['USER'] == url1['USER'] | |
212 | assert url2['HOST'] == url1['HOST'] | |
213 | ||
214 | if scheme == 'postgis': | |
215 | assert url2['ENGINE'] == 'django.contrib.gis.db.backends.postgis' | |
216 | else: | |
217 | assert url2['ENGINE'] == url1['ENGINE'] | |
218 | ||
219 | ||
220 | def test_memory_sqlite_url_warns_about_netloc(recwarn): | |
221 | warnings.simplefilter("always") | |
222 | ||
223 | url = 'sqlite://missing-slash-path' | |
224 | url = Env.db_url_config(url) | |
225 | ||
226 | assert len(recwarn) == 1 | |
227 | assert recwarn.pop(UserWarning) | |
228 | ||
229 | assert url['ENGINE'] == 'django.db.backends.sqlite3' | |
230 | assert url['NAME'] == ':memory:' | |
231 | ||
232 | ||
233 | def test_database_options_parsing(): | |
234 | url = 'postgres://user:pass@host:1234/dbname?conn_max_age=600' | |
235 | url = Env.db_url_config(url) | |
236 | assert url['CONN_MAX_AGE'] == 600 | |
237 | ||
238 | url = ('postgres://user:pass@host:1234/dbname?' | |
239 | 'conn_max_age=None&autocommit=True&atomic_requests=False') | |
240 | url = Env.db_url_config(url) | |
241 | assert url['CONN_MAX_AGE'] is None | |
242 | assert url['AUTOCOMMIT'] is True | |
243 | assert url['ATOMIC_REQUESTS'] is False | |
244 | ||
245 | url = ('mysql://user:pass@host:1234/dbname?init_command=SET ' | |
246 | 'storage_engine=INNODB') | |
247 | url = Env.db_url_config(url) | |
248 | assert url['OPTIONS'] == { | |
249 | 'init_command': 'SET storage_engine=INNODB', | |
250 | } |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | from environ import Env | |
9 | ||
10 | ||
11 | def test_smtp_parsing(): | |
12 | url = 'smtps://user@domain.com:password@smtp.example.com:587' | |
13 | url = Env.email_url_config(url) | |
14 | ||
15 | assert len(url) == 7 | |
16 | ||
17 | assert url['EMAIL_BACKEND'] == 'django.core.mail.backends.smtp.EmailBackend' | |
18 | assert url['EMAIL_HOST'] == 'smtp.example.com' | |
19 | assert url['EMAIL_HOST_PASSWORD'] == 'password' | |
20 | assert url['EMAIL_HOST_USER'] == 'user@domain.com' | |
21 | assert url['EMAIL_PORT'] == 587 | |
22 | assert url['EMAIL_USE_TLS'] is True | |
23 | assert url['EMAIL_FILE_PATH'] == '' |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import os | |
9 | from urllib.parse import quote | |
10 | from warnings import catch_warnings | |
11 | ||
12 | import pytest | |
13 | ||
14 | from environ import Env, Path | |
15 | from environ.compat import ImproperlyConfigured, DJANGO_POSTGRES | |
16 | from .asserts import assert_type_and_value | |
17 | from .fixtures import FakeEnv | |
18 | ||
19 | ||
20 | class TestEnv: | |
21 | def setup_method(self, method): | |
22 | """ | |
23 | Setup environment variables. | |
24 | ||
25 | Setup any state tied to the execution of the given method in a | |
26 | class. setup_method is invoked for every test method of a class. | |
27 | """ | |
28 | self.old_environ = os.environ | |
29 | os.environ = Env.ENVIRON = FakeEnv.generate_data() | |
30 | self.env = Env() | |
31 | ||
32 | def teardown_method(self, method): | |
33 | """ | |
34 | Rollback environment variables. | |
35 | ||
36 | Teardown any state that was previously setup with a setup_method call. | |
37 | """ | |
38 | assert self.old_environ is not None | |
39 | os.environ = self.old_environ | |
40 | ||
41 | def test_not_present_with_default(self): | |
42 | assert self.env('not_present', default=3) == 3 | |
43 | ||
44 | def test_not_present_without_default(self): | |
45 | with pytest.raises(ImproperlyConfigured) as excinfo: | |
46 | self.env('not_present') | |
47 | assert str(excinfo.value) == 'Set the not_present environment variable' | |
48 | assert excinfo.value.__cause__ is not None | |
49 | ||
50 | def test_contains(self): | |
51 | assert 'STR_VAR' in self.env | |
52 | assert 'EMPTY_LIST' in self.env | |
53 | assert 'I_AM_NOT_A_VAR' not in self.env | |
54 | ||
55 | @pytest.mark.parametrize( | |
56 | 'var,val,multiline', | |
57 | [ | |
58 | ('STR_VAR', 'bar', False), | |
59 | ('MULTILINE_STR_VAR', 'foo\\nbar', False), | |
60 | ('MULTILINE_STR_VAR', 'foo\nbar', True), | |
61 | ('MULTILINE_QUOTED_STR_VAR', '---BEGIN---\\r\\n---END---', False), | |
62 | ('MULTILINE_QUOTED_STR_VAR', '---BEGIN---\n---END---', True), | |
63 | ('MULTILINE_ESCAPED_STR_VAR', '---BEGIN---\\\\n---END---', False), | |
64 | ('MULTILINE_ESCAPED_STR_VAR', '---BEGIN---\\\n---END---', True), | |
65 | ], | |
66 | ) | |
67 | def test_str(self, var, val, multiline): | |
68 | assert isinstance(self.env(var), str) | |
69 | if not multiline: | |
70 | assert self.env(var) == val | |
71 | assert self.env.str(var, multiline=multiline) == val | |
72 | ||
73 | def test_unicode(self, recwarn): | |
74 | actual = self.env.unicode('CYRILLIC_VAR', default='фуубар') | |
75 | expected = self.env.str('CYRILLIC_VAR', default='фуубар') | |
76 | ||
77 | assert actual == expected | |
78 | assert len(recwarn) == 1 | |
79 | w = recwarn.pop(DeprecationWarning) | |
80 | assert issubclass(w.category, DeprecationWarning) | |
81 | assert str(w.message) == '`%s.unicode` is deprecated, use `%s.str` instead' %( | |
82 | self.env.__class__.__name__, | |
83 | self.env.__class__.__name__, | |
84 | ) | |
85 | assert w.filename | |
86 | assert w.lineno | |
87 | ||
88 | @pytest.mark.parametrize( | |
89 | 'var,val,default', | |
90 | [ | |
91 | ('STR_VAR', b'bar', Env.NOTSET), | |
92 | ('NON_EXISTENT_BYTES_VAR', b'some-default', b'some-default'), | |
93 | ('NON_EXISTENT_STR_VAR', b'some-default', 'some-default'), | |
94 | ] | |
95 | ) | |
96 | def test_bytes(self, var, val, default): | |
97 | assert_type_and_value(bytes, val, self.env.bytes(var, default=default)) | |
98 | ||
99 | def test_int(self): | |
100 | assert_type_and_value(int, 42, self.env('INT_VAR', cast=int)) | |
101 | assert_type_and_value(int, 42, self.env.int('INT_VAR')) | |
102 | ||
103 | def test_int_with_none_default(self): | |
104 | assert self.env('NOT_PRESENT_VAR', cast=int, default=None) is None | |
105 | assert self.env('EMPTY_INT_VAR', cast=int, default=None) is None | |
106 | ||
107 | @pytest.mark.parametrize( | |
108 | 'value,variable', | |
109 | [ | |
110 | (33.3, 'FLOAT_VAR'), | |
111 | (33.3, 'FLOAT_COMMA_VAR'), | |
112 | (123420333.3, 'FLOAT_STRANGE_VAR1'), | |
113 | (123420333.3, 'FLOAT_STRANGE_VAR2'), | |
114 | (-1.0, 'FLOAT_NEGATIVE_VAR'), | |
115 | ] | |
116 | ) | |
117 | def test_float(self, value, variable): | |
118 | assert_type_and_value(float, value, self.env.float(variable)) | |
119 | assert_type_and_value(float, value, self.env(variable, cast=float)) | |
120 | ||
121 | @pytest.mark.parametrize( | |
122 | 'value,variable', | |
123 | [ | |
124 | (True, 'BOOL_TRUE_STRING_LIKE_INT'), | |
125 | (True, 'BOOL_TRUE_STRING_LIKE_BOOL'), | |
126 | (True, 'BOOL_TRUE_INT'), | |
127 | (True, 'BOOL_TRUE_BOOL'), | |
128 | (True, 'BOOL_TRUE_STRING_1'), | |
129 | (True, 'BOOL_TRUE_STRING_2'), | |
130 | (True, 'BOOL_TRUE_STRING_3'), | |
131 | (True, 'BOOL_TRUE_STRING_4'), | |
132 | (True, 'BOOL_TRUE_STRING_5'), | |
133 | (False, 'BOOL_FALSE_STRING_LIKE_INT'), | |
134 | (False, 'BOOL_FALSE_INT'), | |
135 | (False, 'BOOL_FALSE_STRING_LIKE_BOOL'), | |
136 | (False, 'BOOL_FALSE_BOOL'), | |
137 | ] | |
138 | ) | |
139 | def test_bool_true(self, value, variable): | |
140 | assert_type_and_value(bool, value, self.env.bool(variable)) | |
141 | assert_type_and_value(bool, value, self.env(variable, cast=bool)) | |
142 | ||
143 | def test_proxied_value(self): | |
144 | assert self.env('PROXIED_VAR') == 'bar' | |
145 | ||
146 | def test_escaped_dollar_sign(self): | |
147 | self.env.escape_proxy = True | |
148 | assert self.env('ESCAPED_VAR') == '$baz' | |
149 | ||
150 | def test_escaped_dollar_sign_disabled(self): | |
151 | self.env.escape_proxy = False | |
152 | assert self.env('ESCAPED_VAR') == r'\$baz' | |
153 | ||
154 | def test_int_list(self): | |
155 | assert_type_and_value(list, [42, 33], self.env('INT_LIST', cast=[int])) | |
156 | assert_type_and_value(list, [42, 33], self.env.list('INT_LIST', int)) | |
157 | ||
158 | def test_int_list_cast_tuple(self): | |
159 | assert_type_and_value(tuple, (42, 33), self.env('INT_LIST', cast=(int,))) | |
160 | assert_type_and_value(tuple, (42, 33), self.env.tuple('INT_LIST', int)) | |
161 | assert_type_and_value(tuple, ('42', '33'), self.env.tuple('INT_LIST')) | |
162 | ||
163 | def test_int_tuple(self): | |
164 | assert_type_and_value(tuple, (42, 33), self.env('INT_TUPLE', cast=(int,))) | |
165 | assert_type_and_value(tuple, (42, 33), self.env.tuple('INT_TUPLE', int)) | |
166 | assert_type_and_value(tuple, ('42', '33'), self.env.tuple('INT_TUPLE')) | |
167 | ||
168 | def test_mix_tuple_issue_387(self): | |
169 | """Cast a tuple of mixed types. | |
170 | ||
171 | Casts a string like "(42,Test)" to a tuple like (42, 'Test'). | |
172 | See: https://github.com/joke2k/django-environ/issues/387 for details.""" | |
173 | caster = lambda v: int(v) if v.isdigit() else v.strip() | |
174 | cast = lambda t: tuple(map(caster, [c for c in t.strip('()').split(',')])) | |
175 | assert_type_and_value(tuple, (42, 'Test'), self.env( 'MIX_TUPLE', default=(0, ''), cast=cast)) | |
176 | ||
177 | def test_str_list_with_spaces(self): | |
178 | assert_type_and_value(list, [' foo', ' bar'], | |
179 | self.env('STR_LIST_WITH_SPACES', cast=[str])) | |
180 | assert_type_and_value(list, [' foo', ' bar'], | |
181 | self.env.list('STR_LIST_WITH_SPACES')) | |
182 | ||
183 | def test_empty_list(self): | |
184 | assert_type_and_value(list, [], self.env('EMPTY_LIST', cast=[int])) | |
185 | ||
186 | def test_dict_value(self): | |
187 | assert_type_and_value(dict, FakeEnv.DICT, self.env.dict('DICT_VAR')) | |
188 | ||
189 | def test_complex_dict_value(self): | |
190 | assert_type_and_value( | |
191 | dict, | |
192 | FakeEnv.SAML_ATTRIBUTE_MAPPING, | |
193 | self.env.dict('SAML_ATTRIBUTE_MAPPING', cast={'value': tuple}) | |
194 | ) | |
195 | ||
196 | @pytest.mark.parametrize( | |
197 | 'value,cast,expected', | |
198 | [ | |
199 | ('a=1', dict, {'a': '1'}), | |
200 | ('a=1', dict(value=int), {'a': 1}), | |
201 | ('a=1', dict(value=float), {'a': 1.0}), | |
202 | ('a=1,2,3', dict(value=[str]), {'a': ['1', '2', '3']}), | |
203 | ('a=1,2,3', dict(value=[int]), {'a': [1, 2, 3]}), | |
204 | ('a=1;b=1.1,2.2;c=3', dict(value=int, cast=dict(b=[float])), | |
205 | {'a': 1, 'b': [1.1, 2.2], 'c': 3}), | |
206 | ('a=uname;c=http://www.google.com;b=True', | |
207 | dict(value=str, cast=dict(b=bool)), | |
208 | {'a': "uname", 'c': "http://www.google.com", 'b': True}), | |
209 | ], | |
210 | ids=[ | |
211 | 'dict', | |
212 | 'dict_int', | |
213 | 'dict_float', | |
214 | 'dict_str_list', | |
215 | 'dict_int_list', | |
216 | 'dict_int_cast', | |
217 | 'dict_str_cast', | |
218 | ], | |
219 | ) | |
220 | def test_dict_parsing(self, value, cast, expected): | |
221 | assert self.env.parse_value(value, cast) == expected | |
222 | ||
223 | def test_url_value(self): | |
224 | url = self.env.url('URL_VAR') | |
225 | assert url.__class__ == self.env.URL_CLASS | |
226 | assert url.geturl() == FakeEnv.URL | |
227 | assert self.env.url('OTHER_URL', default=None) is None | |
228 | ||
229 | def test_url_encoded_parts(self): | |
230 | password_with_unquoted_characters = "#password" | |
231 | encoded_url = "mysql://user:%s@127.0.0.1:3306/dbname" % quote( | |
232 | password_with_unquoted_characters | |
233 | ) | |
234 | parsed_url = self.env.db_url_config(encoded_url) | |
235 | assert parsed_url['PASSWORD'] == password_with_unquoted_characters | |
236 | ||
237 | @pytest.mark.parametrize( | |
238 | 'var,engine,name,host,user,passwd,port', | |
239 | [ | |
240 | (Env.DEFAULT_DATABASE_ENV, DJANGO_POSTGRES, 'd8r82722', | |
241 | 'ec2-107-21-253-135.compute-1.amazonaws.com', 'uf07k1', | |
242 | 'wegauwhg', 5431), | |
243 | ('DATABASE_MYSQL_URL', 'django.db.backends.mysql', 'heroku_97681', | |
244 | 'us-cdbr-east.cleardb.com', 'bea6eb0', '69772142', ''), | |
245 | ('DATABASE_MYSQL_GIS_URL', 'django.contrib.gis.db.backends.mysql', | |
246 | 'some_database', '127.0.0.1', 'user', 'password', ''), | |
247 | ('DATABASE_ORACLE_TNS_URL', 'django.db.backends.oracle', 'sid', '', | |
248 | 'user', 'password', None), | |
249 | ('DATABASE_ORACLE_URL', 'django.db.backends.oracle', 'sid', 'host', | |
250 | 'user', 'password', '1521'), | |
251 | ('DATABASE_REDSHIFT_URL', 'django_redshift_backend', 'dev', | |
252 | 'examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com', | |
253 | 'user', 'password', 5439), | |
254 | ('DATABASE_SQLITE_URL', 'django.db.backends.sqlite3', | |
255 | '/full/path/to/your/database/file.sqlite', '', '', '', ''), | |
256 | ('DATABASE_CUSTOM_BACKEND_URL', 'custom.backend', 'database', | |
257 | 'example.com', 'user', 'password', 5430), | |
258 | ('DATABASE_MYSQL_CLOUDSQL_URL', 'django.db.backends.mysql', 'mydatabase', | |
259 | '/cloudsql/arvore-codelab:us-central1:mysqlinstance', 'djuser', 'hidden-password', ''), | |
260 | ], | |
261 | ids=[ | |
262 | 'postgres', | |
263 | 'mysql', | |
264 | 'mysql_gis', | |
265 | 'oracle_tns', | |
266 | 'oracle', | |
267 | 'redshift', | |
268 | 'sqlite', | |
269 | 'custom', | |
270 | 'cloudsql', | |
271 | ], | |
272 | ) | |
273 | def test_db_url_value(self, var, engine, name, host, user, passwd, port): | |
274 | config = self.env.db(var) | |
275 | ||
276 | assert config['ENGINE'] == engine | |
277 | assert config['NAME'] == name | |
278 | assert config['HOST'] == host | |
279 | assert config['USER'] == user | |
280 | assert config['PASSWORD'] == passwd | |
281 | ||
282 | if port is None: | |
283 | assert 'PORT' not in config | |
284 | else: | |
285 | assert config['PORT'] == port | |
286 | ||
287 | @pytest.mark.parametrize( | |
288 | 'var,backend,location,options', | |
289 | [ | |
290 | (Env.DEFAULT_CACHE_ENV, | |
291 | 'django.core.cache.backends.memcached.MemcachedCache', | |
292 | '127.0.0.1:11211', None), | |
293 | ('CACHE_REDIS', 'django_redis.cache.RedisCache', | |
294 | 'redis://127.0.0.1:6379/1', | |
295 | {'CLIENT_CLASS': 'django_redis.client.DefaultClient', | |
296 | 'PASSWORD': 'secret'}), | |
297 | ], | |
298 | ids=[ | |
299 | 'memcached', | |
300 | 'redis', | |
301 | ], | |
302 | ) | |
303 | def test_cache_url_value(self, var, backend, location, options): | |
304 | config = self.env.cache_url(var) | |
305 | ||
306 | assert config['BACKEND'] == backend | |
307 | assert config['LOCATION'] == location | |
308 | ||
309 | if options is None: | |
310 | assert 'OPTIONS' not in config | |
311 | else: | |
312 | assert config['OPTIONS'] == options | |
313 | ||
314 | def test_email_url_value(self): | |
315 | email_config = self.env.email_url() | |
316 | assert email_config['EMAIL_BACKEND'] == ( | |
317 | 'django.core.mail.backends.smtp.EmailBackend' | |
318 | ) | |
319 | assert email_config['EMAIL_HOST'] == 'smtp.example.com' | |
320 | assert email_config['EMAIL_HOST_PASSWORD'] == 'password' | |
321 | assert email_config['EMAIL_HOST_USER'] == 'user@domain.com' | |
322 | assert email_config['EMAIL_PORT'] == 587 | |
323 | assert email_config['EMAIL_USE_TLS'] | |
324 | ||
325 | def test_json_value(self): | |
326 | assert self.env.json('JSON_VAR') == FakeEnv.JSON | |
327 | ||
328 | def test_path(self): | |
329 | root = self.env.path('PATH_VAR') | |
330 | assert_type_and_value(Path, Path(FakeEnv.PATH), root) | |
331 | ||
332 | def test_smart_cast(self): | |
333 | assert self.env.get_value('STR_VAR', default='string') == 'bar' | |
334 | assert self.env.get_value('BOOL_TRUE_STRING_LIKE_INT', default=True) | |
335 | assert not self.env.get_value( | |
336 | 'BOOL_FALSE_STRING_LIKE_INT', | |
337 | default=True) | |
338 | assert self.env.get_value('INT_VAR', default=1) == 42 | |
339 | assert self.env.get_value('FLOAT_VAR', default=1.2) == 33.3 | |
340 | ||
341 | def test_exported(self): | |
342 | assert self.env('EXPORTED_VAR') == FakeEnv.EXPORTED | |
343 | ||
344 | def test_prefix(self): | |
345 | self.env.prefix = 'PREFIX_' | |
346 | assert self.env('TEST') == 'foo' | |
347 | ||
348 | ||
349 | class TestFileEnv(TestEnv): | |
350 | def setup_method(self, method): | |
351 | """ | |
352 | Setup environment variables. | |
353 | ||
354 | Setup any state tied to the execution of the given method in a | |
355 | class. setup_method is invoked for every test method of a class. | |
356 | """ | |
357 | super().setup_method(method) | |
358 | ||
359 | Env.ENVIRON = {} | |
360 | self.env.read_env( | |
361 | Path(__file__, is_file=True)('test_env.txt'), | |
362 | PATH_VAR=Path(__file__, is_file=True).__root__ | |
363 | ) | |
364 | ||
365 | def create_temp_env_file(self, name): | |
366 | import pathlib | |
367 | import tempfile | |
368 | ||
369 | env_file_path = (pathlib.Path(tempfile.gettempdir()) / name) | |
370 | try: | |
371 | env_file_path.unlink() | |
372 | except FileNotFoundError: | |
373 | pass | |
374 | ||
375 | assert not env_file_path.exists() | |
376 | return env_file_path | |
377 | ||
378 | def test_read_env_path_like(self): | |
379 | env_file_path = self.create_temp_env_file('test_pathlib.env') | |
380 | ||
381 | env_key = 'SECRET' | |
382 | env_val = 'enigma' | |
383 | env_str = env_key + '=' + env_val | |
384 | ||
385 | # open() doesn't take path-like on Python < 3.6 | |
386 | with open(str(env_file_path), 'w', encoding='utf-8') as f: | |
387 | f.write(env_str + '\n') | |
388 | ||
389 | self.env.read_env(env_file_path) | |
390 | assert env_key in self.env.ENVIRON | |
391 | assert self.env.ENVIRON[env_key] == env_val | |
392 | ||
393 | @pytest.mark.parametrize("overwrite", [True, False]) | |
394 | def test_existing_overwrite(self, overwrite): | |
395 | env_file_path = self.create_temp_env_file('test_existing.env') | |
396 | with open(str(env_file_path), 'w') as f: | |
397 | f.write("EXISTING=b") | |
398 | self.env.ENVIRON['EXISTING'] = "a" | |
399 | self.env.read_env(env_file_path, overwrite=overwrite) | |
400 | assert self.env.ENVIRON["EXISTING"] == ("b" if overwrite else "a") | |
401 | ||
402 | ||
403 | class TestSubClass(TestEnv): | |
404 | def setup_method(self, method): | |
405 | """ | |
406 | Setup environment variables. | |
407 | ||
408 | Setup any state tied to the execution of the given method in a | |
409 | class. setup_method is invoked for every test method of a class. | |
410 | """ | |
411 | super().setup_method(method) | |
412 | ||
413 | self.CONFIG = FakeEnv.generate_data() | |
414 | ||
415 | class MyEnv(Env): | |
416 | ENVIRON = self.CONFIG | |
417 | ||
418 | self.env = MyEnv() | |
419 | ||
420 | def test_singleton_environ(self): | |
421 | assert self.CONFIG is self.env.ENVIRON |
0 | DICT_VAR=foo=bar,test=on | |
1 | ||
2 | # Database variables | |
3 | DATABASE_MYSQL_URL=mysql://bea6eb0:69772142@us-cdbr-east.cleardb.com/heroku_97681?reconnect=true | |
4 | DATABASE_MYSQL_CLOUDSQL_URL=mysql://djuser:hidden-password@//cloudsql/arvore-codelab:us-central1:mysqlinstance/mydatabase | |
5 | DATABASE_MYSQL_GIS_URL=mysqlgis://user:password@127.0.0.1/some_database | |
6 | ||
7 | # Cache variables | |
8 | CACHE_URL=memcache://127.0.0.1:11211 | |
9 | CACHE_REDIS=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=secret | |
10 | ||
11 | # Email variables | |
12 | EMAIL_URL=smtps://user@domain.com:password@smtp.example.com:587 | |
13 | ||
14 | # Others | |
15 | URL_VAR=http://www.google.com/ | |
16 | PATH_VAR=/home/dev | |
17 | BOOL_TRUE_STRING_LIKE_INT='1' | |
18 | BOOL_TRUE_INT=1 | |
19 | BOOL_TRUE_STRING_LIKE_BOOL='True' | |
20 | BOOL_TRUE_STRING_1='on' | |
21 | BOOL_TRUE_STRING_2='ok' | |
22 | BOOL_TRUE_STRING_3='yes' | |
23 | BOOL_TRUE_STRING_4='y' | |
24 | BOOL_TRUE_STRING_5='true' | |
25 | BOOL_TRUE_BOOL=True | |
26 | BOOL_FALSE_STRING_LIKE_INT='0' | |
27 | BOOL_FALSE_INT=0 | |
28 | BOOL_FALSE_STRING_LIKE_BOOL='False' | |
29 | BOOL_FALSE_BOOL=False | |
30 | DATABASE_SQLITE_URL=sqlite:////full/path/to/your/database/file.sqlite | |
31 | JSON_VAR={"three": 33.44, "two": 2, "one": "bar"} | |
32 | DATABASE_URL=postgres://uf07k1:wegauwhg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722 | |
33 | FLOAT_VAR=33.3 | |
34 | FLOAT_COMMA_VAR=33,3 | |
35 | FLOAT_STRANGE_VAR1=123,420,333.3 | |
36 | FLOAT_STRANGE_VAR2=123.420.333,3 | |
37 | FLOAT_NEGATIVE_VAR=-1.0 | |
38 | PROXIED_VAR=$STR_VAR | |
39 | ESCAPED_VAR=\$baz | |
40 | EMPTY_LIST= | |
41 | EMPTY_INT_VAR= | |
42 | INT_VAR=42 | |
43 | STR_LIST_WITH_SPACES= foo, bar | |
44 | STR_VAR=bar | |
45 | MULTILINE_STR_VAR=foo\nbar | |
46 | MULTILINE_QUOTED_STR_VAR="---BEGIN---\r\n---END---" | |
47 | MULTILINE_ESCAPED_STR_VAR=---BEGIN---\\n---END--- | |
48 | INT_LIST=42,33 | |
49 | CYRILLIC_VAR=фуубар | |
50 | INT_TUPLE=(42,33) | |
51 | MIX_TUPLE=(42,Test) | |
52 | DATABASE_ORACLE_TNS_URL=oracle://user:password@sid | |
53 | DATABASE_ORACLE_URL=oracle://user:password@host:1521/sid | |
54 | DATABASE_REDSHIFT_URL=redshift://user:password@examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev | |
55 | DATABASE_CUSTOM_BACKEND_URL=custom.backend://user:password@example.com:5430/database | |
56 | ||
57 | # Djangosaml2's SAML_ATTRIBUTE_MAPPING | |
58 | SAML_ATTRIBUTE_MAPPING="uid=username;mail=email;cn=first_name;sn=last_name;" | |
59 | ||
60 | # Exports | |
61 | export EXPORTED_VAR="exported var" | |
62 | ||
63 | # Prefixed | |
64 | PREFIX_TEST='foo' |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import os | |
9 | import tempfile | |
10 | from contextlib import contextmanager | |
11 | ||
12 | import environ | |
13 | import pytest | |
14 | ||
15 | ||
16 | @contextmanager | |
17 | def make_temp_file(text): | |
18 | with tempfile.NamedTemporaryFile("w", delete=False) as f: | |
19 | f.write(text) | |
20 | f.close() | |
21 | try: | |
22 | yield f.name | |
23 | finally: | |
24 | if os.path.exists(f.name): | |
25 | os.unlink(f.name) | |
26 | ||
27 | ||
28 | @pytest.fixture | |
29 | def tmp_f(): | |
30 | with make_temp_file(text="fish") as f_name: | |
31 | yield f_name | |
32 | ||
33 | ||
34 | def test_mapping(tmp_f): | |
35 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | |
36 | assert env["ANIMAL"] == "fish" | |
37 | ||
38 | ||
39 | def test_precidence(tmp_f): | |
40 | env = environ.FileAwareMapping( | |
41 | env={ | |
42 | "ANIMAL_FILE": tmp_f, | |
43 | "ANIMAL": "cat", | |
44 | } | |
45 | ) | |
46 | assert env["ANIMAL"] == "fish" | |
47 | ||
48 | ||
49 | def test_missing_file_raises_exception(): | |
50 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": "non-existant-file"}) | |
51 | with pytest.raises(FileNotFoundError): | |
52 | env["ANIMAL"] | |
53 | ||
54 | ||
55 | def test_iter(): | |
56 | env = environ.FileAwareMapping( | |
57 | env={ | |
58 | "ANIMAL_FILE": "some-file", | |
59 | "VEGETABLE": "leek", | |
60 | "VEGETABLE_FILE": "some-vegetable-file", | |
61 | } | |
62 | ) | |
63 | keys = set(env) | |
64 | assert keys == {"ANIMAL_FILE", "ANIMAL", "VEGETABLE", "VEGETABLE_FILE"} | |
65 | assert "ANIMAL" in keys | |
66 | ||
67 | ||
68 | def test_len(): | |
69 | env = environ.FileAwareMapping( | |
70 | env={ | |
71 | "ANIMAL_FILE": "some-file", | |
72 | "VEGETABLE": "leek", | |
73 | "VEGETABLE_FILE": "some-vegetable-file", | |
74 | } | |
75 | ) | |
76 | assert len(env) == 4 | |
77 | ||
78 | ||
79 | def test_cache(tmp_f): | |
80 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | |
81 | assert env["ANIMAL"] == "fish" | |
82 | ||
83 | with open(tmp_f, "w") as f: | |
84 | f.write("cat") | |
85 | assert env["ANIMAL"] == "fish" | |
86 | ||
87 | os.unlink(tmp_f) | |
88 | assert not os.path.exists(env["ANIMAL_FILE"]) | |
89 | assert env["ANIMAL"] == "fish" | |
90 | ||
91 | ||
92 | def test_no_cache(tmp_f): | |
93 | env = environ.FileAwareMapping( | |
94 | cache=False, | |
95 | env={"ANIMAL_FILE": tmp_f}, | |
96 | ) | |
97 | assert env["ANIMAL"] == "fish" | |
98 | ||
99 | with open(tmp_f, "w") as f: | |
100 | f.write("cat") | |
101 | assert env["ANIMAL"] == "cat" | |
102 | ||
103 | os.unlink(tmp_f) | |
104 | assert not os.path.exists(env["ANIMAL_FILE"]) | |
105 | with pytest.raises(FileNotFoundError): | |
106 | assert env["ANIMAL"] | |
107 | ||
108 | ||
109 | def test_setdefault(tmp_f): | |
110 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | |
111 | assert env.setdefault("FRUIT", "apple") == "apple" | |
112 | assert env.setdefault("ANIMAL", "cat") == "fish" | |
113 | assert env.env == {"ANIMAL_FILE": tmp_f, "FRUIT": "apple"} | |
114 | ||
115 | ||
116 | class TestDelItem: | |
117 | def test_del_key(self): | |
118 | env = environ.FileAwareMapping(env={"FRUIT": "apple"}) | |
119 | del env["FRUIT"] | |
120 | with pytest.raises(KeyError): | |
121 | env["FRUIT"] | |
122 | ||
123 | def test_del_key_with_file_key(self): | |
124 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": "some-file"}) | |
125 | del env["ANIMAL"] | |
126 | with pytest.raises(KeyError): | |
127 | env["ANIMAL"] | |
128 | ||
129 | def test_del_shadowed_key_with_file_key(self): | |
130 | env = environ.FileAwareMapping( | |
131 | env={"ANIMAL_FILE": "some-file", "ANIMAL": "cat"} | |
132 | ) | |
133 | del env["ANIMAL"] | |
134 | with pytest.raises(KeyError): | |
135 | env["ANIMAL"] | |
136 | ||
137 | def test_del_file_key(self): | |
138 | env = environ.FileAwareMapping( | |
139 | env={ | |
140 | "ANIMAL_FILE": "some-file", | |
141 | "ANIMAL": "fish", | |
142 | } | |
143 | ) | |
144 | del env["ANIMAL_FILE"] | |
145 | assert env["ANIMAL"] == "fish" | |
146 | ||
147 | def test_del_file_key_clears_cache(self, tmp_f): | |
148 | env = environ.FileAwareMapping( | |
149 | env={ | |
150 | "ANIMAL_FILE": tmp_f, | |
151 | "ANIMAL": "cat", | |
152 | } | |
153 | ) | |
154 | assert env["ANIMAL"] == "fish" | |
155 | del env["ANIMAL_FILE"] | |
156 | assert env["ANIMAL"] == "cat" | |
157 | ||
158 | ||
159 | class TestSetItem: | |
160 | def test_set_key(self): | |
161 | env = environ.FileAwareMapping(env={"FRUIT": "apple"}) | |
162 | env["FRUIT"] = "banana" | |
163 | assert env["FRUIT"] == "banana" | |
164 | ||
165 | def test_cant_override_key_with_file_key(self, tmp_f): | |
166 | env = environ.FileAwareMapping( | |
167 | env={ | |
168 | "FRUIT": "apple", | |
169 | "FRUIT_FILE": tmp_f, | |
170 | } | |
171 | ) | |
172 | with open(tmp_f, "w") as f: | |
173 | f.write("banana") | |
174 | env["FRUIT"] = "cucumber" | |
175 | assert env["FRUIT"] == "banana" | |
176 | ||
177 | def test_set_file_key(self, tmp_f): | |
178 | env = environ.FileAwareMapping(env={"ANIMAL": "cat"}) | |
179 | env["ANIMAL_FILE"] = tmp_f | |
180 | assert env["ANIMAL"] == "fish" | |
181 | ||
182 | def test_change_file_key_clears_cache(self, tmp_f): | |
183 | env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | |
184 | assert env["ANIMAL"] == "fish" | |
185 | with make_temp_file(text="cat") as new_tmp_f: | |
186 | env["ANIMAL_FILE"] = new_tmp_f | |
187 | assert env["ANIMAL"] == "cat" |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import os | |
9 | import pytest | |
10 | import sys | |
11 | ||
12 | from environ import Path | |
13 | from environ.compat import ImproperlyConfigured | |
14 | ||
15 | ||
16 | def test_str(volume): | |
17 | root = Path('/home') | |
18 | ||
19 | if sys.platform == 'win32': | |
20 | assert str(root) == '{}home'.format(volume) | |
21 | assert str(root()) == '{}home'.format(volume) | |
22 | assert str(root('dev')) == '{}home\\dev'.format(volume) | |
23 | else: | |
24 | assert str(root) == '/home' | |
25 | assert str(root()) == '/home' | |
26 | assert str(root('dev')) == '/home/dev' | |
27 | ||
28 | ||
29 | def test_path_class(): | |
30 | root = Path(__file__, '..', is_file=True) | |
31 | root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../')) | |
32 | ||
33 | assert root() == root_path | |
34 | assert root.__root__ == root_path | |
35 | ||
36 | web = root.path('public') | |
37 | assert web() == os.path.join(root_path, 'public') | |
38 | assert web('css') == os.path.join(root_path, 'public', 'css') | |
39 | ||
40 | ||
41 | def test_repr(volume): | |
42 | root = Path('/home') | |
43 | if sys.platform == 'win32': | |
44 | assert root.__repr__() == '<Path:{}home>'.format(volume) | |
45 | else: | |
46 | assert root.__repr__() == '<Path:/home>' | |
47 | ||
48 | ||
49 | def test_comparison(volume): | |
50 | root = Path('/home') | |
51 | assert root.__eq__(Path('/home')) | |
52 | assert root in Path('/') | |
53 | assert root not in Path('/other/path') | |
54 | ||
55 | assert Path('/home') == Path('/home') | |
56 | assert Path('/home') != Path('/home/dev') | |
57 | ||
58 | assert Path('/home/foo/').rfind('/') == str(Path('/home/foo')).rfind('/') | |
59 | assert Path('/home/foo/').find('/home') == str(Path('/home/foo/')).find('/home') | |
60 | assert Path('/home/foo/')[1] == str(Path('/home/foo/'))[1] | |
61 | assert Path('/home/foo/').__fspath__() == str(Path('/home/foo/')) | |
62 | assert ~Path('/home') == Path('/') | |
63 | ||
64 | if sys.platform == 'win32': | |
65 | assert Path('/home') == '{}home'.format(volume) | |
66 | assert '{}home'.format(volume) == Path('/home') | |
67 | else: | |
68 | assert Path('/home') == '/home' | |
69 | assert '/home' == Path('/home') | |
70 | ||
71 | assert Path('/home') != '/usr' | |
72 | ||
73 | def test_sum(): | |
74 | """Make sure Path correct handle __add__.""" | |
75 | assert Path('/') + 'home' == Path('/home') | |
76 | assert Path('/') + '/home/public' == Path('/home/public') | |
77 | ||
78 | ||
79 | def test_subtraction(): | |
80 | """Make sure Path correct handle __sub__.""" | |
81 | assert Path('/home/dev/public') - 2 == Path('/home') | |
82 | assert Path('/home/dev/public') - 'public' == Path('/home/dev') | |
83 | ||
84 | ||
85 | def test_subtraction_not_int(): | |
86 | """Subtraction with an invalid type should raise TypeError.""" | |
87 | with pytest.raises(TypeError) as excinfo: | |
88 | Path('/home/dev/') - 'not int' | |
89 | assert str(excinfo.value) == ( | |
90 | "unsupported operand type(s) for -: '<class 'environ.environ.Path'>' " | |
91 | "and '<class 'str'>' unless value of <class 'environ.environ.Path'> " | |
92 | "ends with value of <class 'str'>" | |
93 | ) | |
94 | ||
95 | ||
96 | def test_required_path(): | |
97 | root = Path('/home') | |
98 | with pytest.raises(ImproperlyConfigured) as excinfo: | |
99 | root('dev', 'not_existing_dir', required=True) | |
100 | assert "Create required path:" in str(excinfo.value) | |
101 | ||
102 | with pytest.raises(ImproperlyConfigured) as excinfo: | |
103 | Path('/not/existing/path/', required=True) | |
104 | assert "Create required path:" in str(excinfo.value) | |
105 | ||
106 | ||
107 | def test_complex_manipulation(volume): | |
108 | root = Path('/home') | |
109 | public = root.path('public') | |
110 | assets, scripts = public.path('assets'), public.path('assets', 'scripts') | |
111 | ||
112 | if sys.platform == 'win32': | |
113 | assert public.__repr__() == '<Path:{}home\\public>'.format(volume) | |
114 | assert str(public.root) == '{}home\\public'.format(volume) | |
115 | assert str(public('styles')) == '{}home\\public\\styles'.format(volume) | |
116 | assert str(assets.root) == '{}home\\public\\assets'.format(volume) | |
117 | assert str(scripts.root) == '{}home\\public\\assets\\scripts'.format( | |
118 | volume | |
119 | ) | |
120 | ||
121 | assert (~assets).__repr__() == '<Path:{}home\\public>'.format( | |
122 | volume | |
123 | ) | |
124 | assert str(assets + 'styles') == ( | |
125 | '{}home\\public\\assets\\styles'.format(volume) | |
126 | ) | |
127 | assert (assets + 'styles').__repr__() == ( | |
128 | '<Path:{}home\\public\\assets\\styles>'.format(volume) | |
129 | ) | |
130 | else: | |
131 | assert public.__repr__() == '<Path:/home/public>' | |
132 | assert str(public.root) == '/home/public' | |
133 | assert str(public('styles')) == '/home/public/styles' | |
134 | assert str(assets.root) == '/home/public/assets' | |
135 | assert str(scripts.root) == '/home/public/assets/scripts' | |
136 | ||
137 | assert str(assets + 'styles') == '/home/public/assets/styles' | |
138 | assert (~assets).__repr__() == '<Path:/home/public>' | |
139 | assert (assets + 'styles').__repr__() == ( | |
140 | '<Path:/home/public/assets/styles>' | |
141 | ) |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import os | |
9 | ||
10 | from environ import Env | |
11 | from .fixtures import FakeEnv | |
12 | ||
13 | _old_environ = None | |
14 | ||
15 | ||
16 | def setup_module(): | |
17 | """Setup environment variables to the execution for the current module.""" | |
18 | global _old_environ | |
19 | ||
20 | _old_environ = os.environ | |
21 | os.environ = Env.ENVIRON = FakeEnv.generate_data() | |
22 | ||
23 | ||
24 | def teardown_module(): | |
25 | """Restore environment variables was previously setup in setup_module.""" | |
26 | global _old_environ | |
27 | ||
28 | assert _old_environ is not None | |
29 | os.environ = _old_environ | |
30 | ||
31 | ||
32 | def test_schema(): | |
33 | env = Env(INT_VAR=int, NOT_PRESENT_VAR=(float, 33.3), STR_VAR=str, | |
34 | INT_LIST=[int], DEFAULT_LIST=([int], [2])) | |
35 | ||
36 | assert isinstance(env('INT_VAR'), int) | |
37 | assert env('INT_VAR') == 42 | |
38 | ||
39 | assert isinstance(env('NOT_PRESENT_VAR'), float) | |
40 | assert env('NOT_PRESENT_VAR') == 33.3 | |
41 | ||
42 | assert 'bar' == env('STR_VAR') | |
43 | assert 'foo' == env('NOT_PRESENT2', default='foo') | |
44 | ||
45 | assert isinstance(env('INT_LIST'), list) | |
46 | assert env('INT_LIST') == [42, 33] | |
47 | ||
48 | assert isinstance(env('DEFAULT_LIST'), list) | |
49 | assert env('DEFAULT_LIST') == [2] | |
50 | ||
51 | # Override schema in this one case | |
52 | assert isinstance(env('INT_VAR', cast=str), str) | |
53 | assert env('INT_VAR', cast=str) == '42' |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import pytest | |
9 | ||
10 | from environ import Env | |
11 | ||
12 | ||
13 | def test_solr_parsing(solr_url): | |
14 | url = Env.search_url_config(solr_url) | |
15 | ||
16 | assert len(url) == 2 | |
17 | assert url['ENGINE'] == 'haystack.backends.solr_backend.SolrEngine' | |
18 | assert url['URL'] == 'http://127.0.0.1:8983/solr' | |
19 | ||
20 | ||
21 | def test_solr_multicore_parsing(solr_url): | |
22 | timeout = 360 | |
23 | index = 'solr_index' | |
24 | url = '{}/{}?TIMEOUT={}'.format(solr_url, index, timeout) | |
25 | url = Env.search_url_config(url) | |
26 | ||
27 | assert url['ENGINE'] == 'haystack.backends.solr_backend.SolrEngine' | |
28 | assert url['URL'] == 'http://127.0.0.1:8983/solr/solr_index' | |
29 | assert url['TIMEOUT'] == timeout | |
30 | assert 'INDEX_NAME' not in url | |
31 | assert 'PATH' not in url | |
32 | ||
33 | ||
34 | @pytest.mark.parametrize( | |
35 | 'url,engine', | |
36 | [ | |
37 | ('elasticsearch://127.0.0.1:9200/index', | |
38 | 'elasticsearch_backend.ElasticsearchSearchEngine'), | |
39 | ('elasticsearch2://127.0.0.1:9200/index', | |
40 | 'elasticsearch2_backend.Elasticsearch2SearchEngine'), | |
41 | ('elasticsearch5://127.0.0.1:9200/index', | |
42 | 'elasticsearch5_backend.Elasticsearch5SearchEngine'), | |
43 | ('elasticsearch7://127.0.0.1:9200/index', | |
44 | 'elasticsearch7_backend.Elasticsearch7SearchEngine'), | |
45 | ], | |
46 | ids=[ | |
47 | 'elasticsearch', | |
48 | 'elasticsearch2', | |
49 | 'elasticsearch5', | |
50 | 'elasticsearch7', | |
51 | ] | |
52 | ) | |
53 | def test_elasticsearch_parsing(url, engine): | |
54 | """Ensure all supported Elasticsearch engines are recognized.""" | |
55 | timeout = 360 | |
56 | url = '{}?TIMEOUT={}'.format(url, timeout) | |
57 | url = Env.search_url_config(url) | |
58 | ||
59 | assert url['ENGINE'] == 'haystack.backends.{}'.format(engine) | |
60 | assert 'INDEX_NAME' in url.keys() | |
61 | assert url['INDEX_NAME'] == 'index' | |
62 | assert 'TIMEOUT' in url.keys() | |
63 | assert url['TIMEOUT'] == timeout | |
64 | assert 'PATH' not in url | |
65 | ||
66 | ||
67 | @pytest.mark.parametrize('storage', ['file', 'ram']) | |
68 | def test_whoosh_parsing(whoosh_url, storage): | |
69 | post_limit = 128 * 1024 * 1024 | |
70 | url = '{}?STORAGE={}&POST_LIMIT={}'.format(whoosh_url, storage, post_limit) | |
71 | url = Env.search_url_config(url) | |
72 | ||
73 | assert url['ENGINE'] == 'haystack.backends.whoosh_backend.WhooshEngine' | |
74 | assert 'PATH' in url.keys() | |
75 | assert url['PATH'] == '/home/search/whoosh_index' | |
76 | assert 'STORAGE' in url.keys() | |
77 | assert url['STORAGE'] == storage | |
78 | assert 'POST_LIMIT' in url.keys() | |
79 | assert url['POST_LIMIT'] == post_limit | |
80 | assert 'INDEX_NAME' not in url | |
81 | ||
82 | ||
83 | @pytest.mark.parametrize('flags', ['myflags']) | |
84 | def test_xapian_parsing(xapian_url, flags): | |
85 | url = '{}?FLAGS={}'.format(xapian_url, flags) | |
86 | url = Env.search_url_config(url) | |
87 | ||
88 | assert url['ENGINE'] == 'haystack.backends.xapian_backend.XapianEngine' | |
89 | assert 'PATH' in url.keys() | |
90 | assert url['PATH'] == '/home/search/xapian_index' | |
91 | assert 'FLAGS' in url.keys() | |
92 | assert url['FLAGS'] == flags | |
93 | assert 'INDEX_NAME' not in url | |
94 | ||
95 | ||
96 | def test_simple_parsing(simple_url): | |
97 | url = Env.search_url_config(simple_url) | |
98 | ||
99 | assert url['ENGINE'] == 'haystack.backends.simple_backend.SimpleEngine' | |
100 | assert 'INDEX_NAME' not in url | |
101 | assert 'PATH' not in url | |
102 | ||
103 | ||
104 | def test_common_args_parsing(search_url): | |
105 | excluded_indexes = 'myapp.indexes.A,myapp.indexes.B' | |
106 | include_spelling = 1 | |
107 | batch_size = 100 | |
108 | params = 'EXCLUDED_INDEXES={}&INCLUDE_SPELLING={}&BATCH_SIZE={}'.format( | |
109 | excluded_indexes, | |
110 | include_spelling, | |
111 | batch_size | |
112 | ) | |
113 | ||
114 | url = '?'.join([search_url, params]) | |
115 | url = Env.search_url_config(url) | |
116 | ||
117 | assert 'EXCLUDED_INDEXES' in url.keys() | |
118 | assert 'myapp.indexes.A' in url['EXCLUDED_INDEXES'] | |
119 | assert 'myapp.indexes.B' in url['EXCLUDED_INDEXES'] | |
120 | assert 'INCLUDE_SPELLING' in url.keys() | |
121 | assert url['INCLUDE_SPELLING'] | |
122 | assert 'BATCH_SIZE' in url.keys() | |
123 | assert url['BATCH_SIZE'] == 100 |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | import pytest | |
9 | from environ.environ import _cast, _cast_urlstr | |
10 | ||
11 | ||
12 | @pytest.mark.parametrize( | |
13 | 'literal', | |
14 | ['anything-', 'anything*', '*anything', 'anything.', | |
15 | 'anything.1', '(anything', 'anything-v1.2', 'anything-1.2', 'anything='] | |
16 | ) | |
17 | def test_cast(literal): | |
18 | """Safely evaluate a string containing an invalid Python literal. | |
19 | ||
20 | See https://github.com/joke2k/django-environ/issues/200 for details.""" | |
21 | assert _cast(literal) == literal | |
22 | ||
23 | @pytest.mark.parametrize( | |
24 | "quoted_url_str,expected_unquoted_str", | |
25 | [ | |
26 | ("Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+", "Le-{FsIaYnaQw}a2B/[V8bS+"), | |
27 | ("my_test-string+", "my_test-string+"), | |
28 | ("my%20test%20string+", "my test string+") | |
29 | ] | |
30 | ) | |
31 | def test_cast_urlstr(quoted_url_str, expected_unquoted_str): | |
32 | """Make sure that a url str that contains plus sign literals does not get unquoted incorrectly | |
33 | Plus signs should not be converted to spaces, since spaces are encoded with %20 in URIs | |
34 | ||
35 | see https://github.com/joke2k/django-environ/issues/357 for details. | |
36 | related to https://github.com/joke2k/django-environ/pull/69""" | |
37 | ||
38 | assert _cast_urlstr(quoted_url_str) == expected_unquoted_str |
0 | # This file is part of the django-environ. | |
1 | # | |
2 | # Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch> | |
3 | # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com> | |
4 | # | |
5 | # For the full copyright and license information, please view | |
6 | # the LICENSE.txt file that was distributed with this source code. | |
7 | ||
8 | # Tox (https://tox.readthedocs.io) - run tests in multiple virtualenvs. | |
9 | # Also contains configuration settings for all tools executed by Tox. | |
10 | ||
11 | [tox] | |
12 | minversion = 3.22 | |
13 | envlist = | |
14 | build | |
15 | coverage-report | |
16 | linkcheck | |
17 | docs | |
18 | lint | |
19 | manifest | |
20 | py{35,36,37,38,39,310}-django{111,22} | |
21 | py{36,37,38,39,310}-django{30,31,32} | |
22 | py{38,39,310}-django{40} | |
23 | pypy-django{111,22,30,31,32} | |
24 | ||
25 | [gh-actions] | |
26 | python = | |
27 | 3.5: py35 | |
28 | 3.6: py36 | |
29 | 3.7: py37 | |
30 | 3.8: py38 | |
31 | 3.9: py39 | |
32 | 3.10: py310 | |
33 | pypy-3.7: pypy | |
34 | ||
35 | [testenv] | |
36 | description = Unit tests | |
37 | extras = testing | |
38 | deps = | |
39 | django111: Django>=1.11,<2 | |
40 | django22: Django>=2.2,<3.0 | |
41 | django30: Django>=3.0,<3.1 | |
42 | django31: Django>=3.1,<3.2 | |
43 | django32: Django>=3.2,<3.3 | |
44 | django40: Django>=4.0,<4.1 | |
45 | commands_pre = | |
46 | python -m pip install --upgrade pip | |
47 | python -m pip install . | |
48 | commands = | |
49 | coverage erase | |
50 | coverage run -m pytest {posargs} | |
51 | ||
52 | [testenv:coverage-report] | |
53 | description = Combine coverage reports | |
54 | skip_install = true | |
55 | deps = coverage[toml]>=5.4 | |
56 | commands = | |
57 | coverage combine | |
58 | coverage report | |
59 | coverage html | |
60 | coverage xml | |
61 | ||
62 | [testenv:lint] | |
63 | description = Static code analysis and code style check | |
64 | skip_install = true | |
65 | deps = | |
66 | flake8 | |
67 | flake8-blind-except | |
68 | flake8-import-order | |
69 | commands_pre = | |
70 | python -m pip install --upgrade pip | |
71 | python -m pip install . | |
72 | commands = flake8 environ setup.py | |
73 | ||
74 | [testenv:linkcheck] | |
75 | description = Check external links in the package documentation | |
76 | # Keep basepython in sync with .readthedocs.yml and docs.yml | |
77 | # (GitHub Action Workflow). | |
78 | basepython = python3.10 | |
79 | extras = docs | |
80 | commands = | |
81 | {envpython} -m sphinx \ | |
82 | -j auto \ | |
83 | -b linkcheck \ | |
84 | {tty:--color} \ | |
85 | -n -W -a \ | |
86 | --keep-going \ | |
87 | -d {envtmpdir}/doctrees \ | |
88 | docs \ | |
89 | docs/_build/linkcheck | |
90 | isolated_build = true | |
91 | ||
92 | [testenv:docs] | |
93 | description = Build package documentation (HTML) | |
94 | # Keep basepython in sync with .readthedocs.yml and docs.yml | |
95 | # (GitHub Action Workflow). | |
96 | basepython = python3.10 | |
97 | extras = docs | |
98 | commands = | |
99 | {envpython} -m sphinx \ | |
100 | -j auto \ | |
101 | -b html \ | |
102 | {tty:--color} \ | |
103 | -n -T -W \ | |
104 | -d {envtmpdir}/doctrees \ | |
105 | docs \ | |
106 | docs/_build/html | |
107 | ||
108 | {envpython} -m sphinx \ | |
109 | -j auto \ | |
110 | -b doctest \ | |
111 | {tty:--color} \ | |
112 | -n -T -W \ | |
113 | -d {envtmpdir}/doctrees \ | |
114 | docs \ | |
115 | docs/_build/doctest | |
116 | ||
117 | {envpython} -m doctest \ | |
118 | AUTHORS.rst \ | |
119 | CHANGELOG.rst \ | |
120 | CONTRIBUTING.rst \ | |
121 | README.rst \ | |
122 | SECURITY.rst | |
123 | ||
124 | [testenv:manifest] | |
125 | description = Check MANIFEST.in in a source package for completeness | |
126 | deps = check-manifest | |
127 | skip_install = true | |
128 | commands = check-manifest -v | |
129 | ||
130 | [testenv:build] | |
131 | description = Build and test package distribution | |
132 | skip_install = true | |
133 | deps = | |
134 | twine | |
135 | check-wheel-contents | |
136 | commands_pre = | |
137 | python -m pip install -U pip setuptools wheel | |
138 | python setup.py bdist_wheel -d {envtmpdir}/build | |
139 | python setup.py sdist -d {envtmpdir}/build | |
140 | commands = | |
141 | twine check {envtmpdir}/build/* | |
142 | check-wheel-contents {envtmpdir}/build | |
143 | ||
144 | [pytest] | |
145 | testpaths = tests | |
146 | addopts = | |
147 | --verbose | |
148 | --ignore=.tox | |
149 | ||
150 | [coverage:run] | |
151 | branch = True | |
152 | parallel = True | |
153 | # A list of file name patterns, the files to leave | |
154 | # out of measurement or reporting. | |
155 | omit = | |
156 | .tox/* | |
157 | tests/* | |
158 | */__pycache__/* | |
159 | ||
160 | [coverage:report] | |
161 | precision = 2 | |
162 | show_missing = True | |
163 | ||
164 | [flake8] | |
165 | # Base flake8 configuration: | |
166 | statistics = True | |
167 | show-source = True | |
168 | # TODO: max-complexity = 10 | |
169 | # Plugins: | |
170 | import-order-style = smarkets | |
171 | # A list of mappings of files and the codes that should be ignored for | |
172 | # the entirety of the file: | |
173 | per-file-ignores = | |
174 | environ/__init__.py:F401,F403 | |
175 | environ/compat.py:F401 | |
176 | # Excluding some directories: | |
177 | extend-exclude = | |
178 | .tox | |
179 | build* | |
180 | dist | |
181 | htmlcov |