Codebase list django-environ / fb2d042
Import upstream version 0.9.0 Debian Janitor 1 year, 3 months ago
52 changed file(s) with 5229 addition(s) and 2237 deletion(s). Raw diff Collapse all Expand all
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>
12
23 Permission is hereby granted, free of charge, to any person obtaining a copy
34 of this software and associated documentation files (the "Software"), to deal
1516 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1617 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1718 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
+252
-428
PKG-INFO less more
0 Metadata-Version: 1.1
0 Metadata-Version: 2.1
11 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
42117 Platform: any
422 Classifier: Development Status :: 3 - Alpha
423 Classifier: Intended Audience :: Information Technology
18 Classifier: Development Status :: 5 - Production/Stable
42419 Classifier: Framework :: Django
425 Classifier: Framework :: Django :: 1.8
426 Classifier: Framework :: Django :: 1.9
427 Classifier: Framework :: Django :: 1.10
42820 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
42931 Classifier: Programming Language :: Python
430 Classifier: Programming Language :: Python :: 2
431 Classifier: Programming Language :: Python :: 2.7
43232 Classifier: Programming Language :: Python :: 3
43333 Classifier: Programming Language :: Python :: 3.4
43434 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
43540 Classifier: Programming Language :: Python :: Implementation :: CPython
43641 Classifier: Programming Language :: Python :: Implementation :: PyPy
43742 Classifier: Topic :: Software Development :: Libraries :: Python Modules
43843 Classifier: Topic :: Utilities
43944 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
31
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>
526
6 |pypi| |unix_build| |windows_build| |coverage| |license|
27 .. -teaser-begin-
728
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.
832
9 This module is a merge of:
33 .. -teaser-end-
1034
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:
1737
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-
2539
2640 .. code-block:: python
2741
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
3044
31 DEBUG = True
32 TEMPLATE_DEBUG = DEBUG
45 env = environ.Env(
46 # set casting, default value
47 DEBUG=(bool, False)
48 )
3349
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__)))
4852
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'))
5355
54 SECRET_KEY = '...im incredibly still here...'
56 # False if not in os.environ because of casting above
57 DEBUG = env('DEBUG')
5558
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')
7262
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(),
7471
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 }
7678
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(),
8185
82 SITE_ROOT = root()
86 # read os.environ['REDIS_URL']
87 'redis': env.cache_url('REDIS_URL')
88 }
8389
84 DEBUG = env('DEBUG') # False if not in os.environ
85 TEMPLATE_DEBUG = DEBUG
90 .. -overview-
8691
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.
119102
120103
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.
123108
124 ::
109 **Feature Support**
125110
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``)
127117
118 .. -project-information-
128119
129 How to use
130 ==========
120 Project Information
121 ===================
131122
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/>`_.
133127
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.
135130
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!
144132
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-
149134
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
272136 =======
273137
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
11 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
42117 Platform: any
422 Classifier: Development Status :: 3 - Alpha
423 Classifier: Intended Audience :: Information Technology
18 Classifier: Development Status :: 5 - Production/Stable
42419 Classifier: Framework :: Django
425 Classifier: Framework :: Django :: 1.8
426 Classifier: Framework :: Django :: 1.9
427 Classifier: Framework :: Django :: 1.10
42820 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
42931 Classifier: Programming Language :: Python
430 Classifier: Programming Language :: Python :: 2
431 Classifier: Programming Language :: Python :: 2.7
43232 Classifier: Programming Language :: Python :: 3
43333 Classifier: Programming Language :: Python :: 3.4
43434 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
43540 Classifier: Programming Language :: Python :: Implementation :: CPython
43641 Classifier: Programming Language :: Python :: Implementation :: PyPy
43742 Classifier: Topic :: Software Development :: Libraries :: Python Modules
43843 Classifier: Topic :: Utilities
43944 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
05 LICENSE.txt
16 MANIFEST.in
27 README.rst
8 SECURITY.rst
39 setup.cfg
410 setup.py
11 tox.ini
512 django_environ.egg-info/PKG-INFO
613 django_environ.egg-info/SOURCES.txt
714 django_environ.egg-info/dependency_links.txt
815 django_environ.egg-info/not-zip-safe
916 django_environ.egg-info/requires.txt
1017 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
1134 environ/__init__.py
35 environ/compat.py
1236 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."
(New empty file)
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
017 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
08 """
19 Django-environ allows you to utilize 12factor inspired environment
210 variables to configure your Django application.
311 """
4 import json
12
13 import ast
14 import itertools
515 import logging
616 import os
717 import re
818 import sys
19 import urllib.parse as urlparselib
920 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
1038
1139 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)
1945
2046 logger = logging.getLogger(__name__)
2147
2248
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
2959 def _cast_int(v):
60 """Return int if possible."""
3061 return int(v) if hasattr(v, 'isdigit') and v.isdigit() else v
3162
63
3264 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:
4869
4970 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:
5575 """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.
5777
5878 Usage:::
5979
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')
63102 """
64103
65104 ENVIRON = os.environ
66105 NOTSET = NoValue()
67106 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
69112 DEFAULT_DATABASE_ENV = 'DATABASE_URL'
70113 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,
75118 'postgis': 'django.contrib.gis.db.backends.postgis',
76119 'mysql': 'django.db.backends.mysql',
77120 'mysql2': 'django.db.backends.mysql',
78121 'mysql-connector': 'mysql.connector.django',
79122 'mysqlgis': 'django.contrib.gis.db.backends.mysql',
123 'mssql': 'sql_server.pyodbc',
80124 'oracle': 'django.db.backends.oracle',
81125 'pyodbc': 'sql_server.pyodbc',
82126 'redshift': 'django_redshift_backend',
84128 'sqlite': 'django.db.backends.sqlite3',
85129 'ldap': 'ldapdb.backends.ldap',
86130 }
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 ]
88137
89138 DEFAULT_CACHE_ENV = 'CACHE_URL'
90139 CACHE_SCHEMES = {
93142 'filecache': 'django.core.cache.backends.filebased.FileBasedCache',
94143 'locmemcache': 'django.core.cache.backends.locmem.LocMemCache',
95144 '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',
97147 'rediscache': REDIS_DRIVER,
98148 'redis': REDIS_DRIVER,
149 'rediss': REDIS_DRIVER,
99150 }
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 ]
101158
102159 DEFAULT_EMAIL_ENV = 'EMAIL_URL'
103160 EMAIL_SCHEMES = {
114171
115172 DEFAULT_SEARCH_ENV = 'SEARCH_URL'
116173 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",
119182 "solr": "haystack.backends.solr_backend.SolrEngine",
120183 "whoosh": "haystack.backends.whoosh_backend.WhooshEngine",
121184 "xapian": "haystack.backends.xapian_backend.XapianEngine",
122185 "simple": "haystack.backends.simple_backend.SimpleEngine",
123186 }
187 CLOUDSQL = 'cloudsql'
124188
125189 def __init__(self, **scheme):
190 self.smart_cast = True
191 self.escape_proxy = False
192 self.prefix = ""
126193 self.scheme = scheme
127194
128195 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 )
130202
131203 def __contains__(self, var):
132204 return var in self.ENVIRON
133205
134206 # Shortcuts
135207
136 def str(self, var, default=NOTSET):
208 def str(self, var, default=NOTSET, multiline=False):
137209 """
138210 :rtype: str
139211 """
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
141216
142217 def unicode(self, var, default=NOTSET):
143218 """Helper for python2
144219 :rtype: unicode
145220 """
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
146230 return self.get_value(var, cast=str, default=default)
147231
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
148241 def bool(self, var, default=NOTSET):
149242 """
150243 :rtype: bool
173266 """
174267 :rtype: list
175268 """
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 )
177274
178275 def tuple(self, var, cast=None, default=NOTSET):
179276 """
180277 :rtype: tuple
181278 """
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 )
183284
184285 def dict(self, var, cast=dict, default=NOTSET):
185286 """
189290
190291 def url(self, var, default=NOTSET):
191292 """
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 )
195301
196302 def db_url(self, var=DEFAULT_DATABASE_ENV, default=NOTSET, engine=None):
197303 """Returns a config dictionary, defaulting to DATABASE_URL.
198304
305 The db method is an alias for db_url.
306
199307 :rtype: dict
200308 """
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
202314 db = db_url
203315
204316 def cache_url(self, var=DEFAULT_CACHE_ENV, default=NOTSET, backend=None):
205317 """Returns a config dictionary, defaulting to CACHE_URL.
206318
319 The cache method is an alias for cache_url.
320
207321 :rtype: dict
208322 """
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
210328 cache = cache_url
211329
212330 def email_url(self, var=DEFAULT_EMAIL_ENV, default=NOTSET, backend=None):
213331 """Returns a config dictionary, defaulting to EMAIL_URL.
214332
333 The email method is an alias for email_url.
334
215335 :rtype: dict
216336 """
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
218342 email = email_url
219343
220344 def search_url(self, var=DEFAULT_SEARCH_ENV, default=NOTSET, engine=None):
222346
223347 :rtype: dict
224348 """
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 )
226353
227354 def path(self, var, default=NOTSET, **kwargs):
228355 """
233360 def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
234361 """Return value for given environment variable.
235362
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(
245376 var, cast, default
246377 ))
247378
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]
250382
251383 try:
252384 has_default = len(var_info) == 2
267399 cast = var_info
268400
269401 try:
270 value = self.ENVIRON[var]
271 except KeyError:
402 value = self.ENVIRON[var_name]
403 except KeyError as exc:
272404 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
275407
276408 value = default
277409
278410 # 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)
281415 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
282427
283428 if value != default or (parse_default and value):
284429 value = self.parse_value(value, cast)
315460 value = dict(map(
316461 lambda kv: (
317462 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 )
319467 ),
320468 [val.split('=') for val in value.split(';') if val]
321469 ))
328476 value = tuple([x for x in val if x])
329477 elif cast is float:
330478 # 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)
334483 if len(parts) == 1:
335484 float_str = parts[0]
336485 else:
337 float_str = "{0}.{1}".format(''.join(parts[0:-1]), parts[-1])
486 float_str = "{}.{}".format(''.join(parts[0:-1]), parts[-1])
338487 value = float(float_str)
339488 else:
340489 value = cast(value)
342491
343492 @classmethod
344493 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
358517 """
359518 if not isinstance(url, cls.URL_CLASS):
360519 if url == 'sqlite://:memory:':
366525 'NAME': ':memory:'
367526 }
368527 # note: no other settings are required for sqlite
369 url = urllib.parse.urlparse(url)
528 url = urlparse(url)
370529
371530 config = {}
372531
373532 # Remove query strings.
374533 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)
381545 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 )
383550 if url.port:
384551 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
385569
386570 # Update with environment configuration.
387571 config.update({
388572 'NAME': path or '',
389573 'USER': _cast_urlstr(url.username) or '',
390574 '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 '',
393577 })
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)
394584
395585 if url.scheme == 'oracle' and path == '':
396586 config['NAME'] = config['HOST']
399589 if url.scheme == 'oracle':
400590 # Django oracle/base.py strips port and fails on non-string value
401591 if not config['PORT']:
402 del(config['PORT'])
592 del (config['PORT'])
403593 else:
404594 config['PORT'] = str(config['PORT'])
405595
406596 if url.query:
407597 config_options = {}
408 for k, v in urllib.parse.parse_qs(url.query).items():
598 for k, v in parse_qs(url.query).items():
409599 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])})
411601 else:
412602 config_options.update({k: _cast_int(v[0])})
413603 config['OPTIONS'] = config_options
414604
415605 if engine:
416606 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']]
419612
420613 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))
422615 return {}
423616
424617 return config
425618
426619 @classmethod
427620 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 )
435640
436641 location = url.netloc.split(',')
437642 if len(location) == 1:
448653 'LOCATION': url.netloc + url.path,
449654 })
450655
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']:
452665 config.update({
453666 'LOCATION': 'unix:' + url.path,
454667 })
457670 scheme = url.scheme.replace('cache', '')
458671 else:
459672 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
462679
463680 if url.query:
464681 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])}
467684 if k.upper() in cls._CACHE_BASE_OPTIONS:
468685 config.update(opt)
469686 else:
477694
478695 @classmethod
479696 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 """
481706
482707 config = {}
483708
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
485710
486711 # Remove query strings
487712 path = url.path[1:]
488 path = urllib.parse.unquote_plus(path.split('?', 2)[0])
713 path = unquote_plus(path.split('?', 2)[0])
489714
490715 # Update with environment configuration
491716 config.update({
510735
511736 if url.query:
512737 config_options = {}
513 for k, v in urllib.parse.parse_qs(url.query).items():
738 for k, v in parse_qs(url.query).items():
514739 opt = {k.upper(): _cast_int(v[0])}
515740 if k.upper() in cls._EMAIL_BASE_OPTIONS:
516741 config.update(opt)
522747
523748 @classmethod
524749 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
525760 config = {}
526761
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
528763
529764 # Remove query strings.
530765 path = url.path[1:]
531 path = urllib.parse.unquote_plus(path.split('?', 2)[0])
766 path = unquote_plus(path.split('?', 2)[0])
532767
533768 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 )
535772 config["ENGINE"] = cls.SEARCH_SCHEMES[url.scheme]
536773
537774 # check commons params
538 params = {}
775 params = {} # type: dict
539776 if url.query:
540 params = urllib.parse.parse_qs(url.query)
777 params = parse_qs(url.query)
541778 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(',')
543781 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 )
545786 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 )
547791
548792 if url.scheme == 'simple':
549793 return config
550 elif url.scheme in ['solr', 'elasticsearch', 'elasticsearch2']:
794 elif url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY:
551795 if 'KWARGS' in params.keys():
552796 config['KWARGS'] = params['KWARGS'][0]
553797
556800 path = path[:-1]
557801
558802 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 )
560806 if 'TIMEOUT' in params.keys():
561807 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
562808 return config
563809
564 if url.scheme in ['elasticsearch', 'elasticsearch2']:
565
810 if url.scheme in cls.ELASTICSEARCH_FAMILY:
566811 split = path.rsplit("/", 1)
567812
568813 if len(split) > 1:
572817 path = ""
573818 index = split[0]
574819
575 config['URL'] = urllib.parse.urlunparse(('http',) + url[1:2] + (path,) + ('', '', ''))
820 config['URL'] = urlunparse(
821 ('http',) + url[1:2] + (path,) + ('', '', '')
822 )
576823 if 'TIMEOUT' in params.keys():
577824 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
578825 config['INDEX_NAME'] = index
584831 if 'STORAGE' in params.keys():
585832 config['STORAGE'] = params['STORAGE'][0]
586833 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 )
588838 elif url.scheme == 'xapian':
589839 if 'FLAGS' in params.keys():
590840 config['FLAGS'] = params['FLAGS'][0]
595845 return config
596846
597847 @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.
607871 """
608872 if env_file is None:
609873 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 )
611878 if not os.path.exists(env_file):
612 warnings.warn(
879 logger.info(
613880 "%s doesn't exist - if you're not configuring your "
614881 "environment separately, create one." % env_file)
615882 return
616883
617884 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 "
623895 "environment separately, check this." % env_file)
624896 return
625897
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
627906
628907 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)
630909 if m1:
631910 key, val = m1.group(1), m1.group(2)
632911 m2 = re.match(r"\A'(.*)'\Z", val)
634913 val = m2.group(1)
635914 m3 = re.match(r'\A"(.*)"\Z', val)
636915 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
641936 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):
669941 """
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."""
670960
671961 def path(self, *paths, **kwargs):
672962 """Create new Path based on self.root and provided paths.
678968 return self.__class__(self.__root__, *paths, **kwargs)
679969
680970 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]
688977 """
689978 return open(self(name), *args, **kwargs)
690979
695984
696985 def __init__(self, start='', *paths, **kwargs):
697986
698 super(Path, self).__init__()
987 super().__init__()
699988
700989 if kwargs.get('is_file', False):
701990 start = os.path.dirname(start)
7111000 return self._absolute_join(self.__root__, *paths, **kwargs)
7121001
7131002 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
7151006
7161007 def __ne__(self, other):
7171008 return not self.__eq__(other)
7181009
7191010 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__)
7211014
7221015 def __sub__(self, other):
7231016 if isinstance(other, int):
7241017 return self.path('../' * other)
725 elif isinstance(other, string_types):
1018 elif isinstance(other, str):
7261019 if self.__root__.endswith(other):
7271020 return Path(self.__root__.rstrip(other))
7281021 raise TypeError(
7421035 return item.__root__.startswith(base_path)
7431036
7441037 def __repr__(self):
745 return "<Path:{0}>".format(self.__root__)
1038 return "<Path:{}>".format(self.__root__)
7461039
7471040 def __str__(self):
7481041 return self.__root__
7521045
7531046 def __getitem__(self, *args, **kwargs):
7541047 return self.__str__().__getitem__(*args, **kwargs)
1048
1049 def __fspath__(self):
1050 return self.__str__()
7551051
7561052 def rfind(self, *args, **kwargs):
7571053 return self.__str__().rfind(*args, **kwargs)
7641060 absolute_path = os.path.abspath(os.path.join(base, *paths))
7651061 if kwargs.get('required', False) and not os.path.exists(absolute_path):
7661062 raise ImproperlyConfigured(
767 "Create required path: {0}".format(absolute_path))
1063 "Create required path: {}".format(absolute_path))
7681064 return absolute_path
7691065
7701066
7711067 def register_scheme(scheme):
772 for method in dir(urllib.parse):
1068 for method in dir(urlparselib):
7731069 if method.startswith('uses_'):
774 getattr(urllib.parse, method).append(scheme)
1070 getattr(urlparselib, method).append(scheme)
7751071
7761072
7771073 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
-700
environ/test.py less more
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
-30
environ/test_env.txt less more
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
60 [bdist_wheel]
71 universal = 1
82
00 #!/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