Giter Site home page Giter Site logo

Comments (8)

epicserve avatar epicserve commented on June 11, 2024

I should add that I'm willing to contribute and help implement this feature.

from environs.

epicserve avatar epicserve commented on June 11, 2024

I updated the issue to include an idea on how you would invoke writing to the file in a Django app.

from environs.

epicserve avatar epicserve commented on June 11, 2024

I coded up the following POC that could be integrated in to the project.

import base64
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Union

import environs


class EnvWriter:
    var_data: dict
    write_dot_env_file: bool = False

    def __init__(self, read_dot_env_file: bool = True, eager: bool = True, expand_vars: bool = False):
        self.var_data = {}
        self._env = environs.Env(eager=eager, expand_vars=expand_vars)
        self.write_dot_env_file = self._env.bool("WRITE_DOT_ENV_FILE", default=False)
        self._env = environs.Env(eager=eager, expand_vars=expand_vars)
        self.base_dir = environs.Path(__file__).parent
        if read_dot_env_file is True:
            self._env.read_env(str(self.base_dir.joinpath(".env")))

    def _get_var(self, environs_instance, var_type: str, environ_args: tuple = None, environ_kwargs: dict = None):
        help_text = environ_kwargs.pop("help_text", None)
        initial = environ_kwargs.pop("initial", None)

        if self.write_dot_env_file is True:
            self.var_data[environ_args[0]] = {
                "type": var_type,
                "default": environ_kwargs.get("default"),
                "help_text": help_text,
                "initial": initial,
            }

        try:
            return getattr(environs_instance, var_type)(*environ_args, **environ_kwargs)
        except environs.EnvError as e:
            if self.write_dot_env_file is False:
                raise e

    def __call__(self, *args, **kwargs):
        return self._get_var(self._env, var_type="str", environ_args=args, environ_kwargs=kwargs)

    def __getattr__(self, item):
        allowed_methods = [
            "int",
            "bool",
            "str",
            "float",
            "decimal",
            "list",
            "dict",
            "json",
            "datetime",
            "date",
            "time",
            "path",
            "log_level",
            "timedelta",
            "uuid",
            "url",
            "enum",
            "dj_db_url",
            "dj_email_url",
            "dj_cache_url",
        ]
        if item not in allowed_methods:
            return AttributeError(f"'{type(self).__name__}' object has no attribute '{item}'")

        def _get_var(*args, **kwargs):
            return self._get_var(self._env, var_type=item, environ_args=args, environ_kwargs=kwargs)

        return _get_var

    def write_env_file(self, env_file_path: Union[Path, str] = None, overwrite_existing: bool = False):
        if env_file_path is None:
            env_file_path = self.base_dir.joinpath(".env")

        if env_file_path.exists() is True and overwrite_existing is False:
            env_file_path = f"{env_file_path}.{datetime.now().strftime('%Y%m%d%H%M%S')}"

        with open(env_file_path, "w") as f:
            env_str = (
                f"# This is an initial .env file generated on {datetime.now(timezone.utc).isoformat()}. Any environment variable with a default\n"
                "# can be safely removed or commented out. Any variable without a default must be set.\n\n"
            )
            for key, data in self.var_data.items():
                initial = data.get("initial", None)
                val = ""

                if data["help_text"] is not None:
                    env_str += f"# {data['help_text']}\n"
                env_str += f"# type: {data['type']}\n"

                if data["default"] is not None:
                    env_str += f"# default: {data['default']}\n"

                if initial is not None and val == "":
                    val = initial()

                if val == "" and data["default"] is not None:
                    env_str += f"# {key}={val}\n\n"
                else:
                    env_str += f"{key}={val}\n\n"

            f.write(env_str)


os.environ.setdefault("WRITE_DOT_ENV_FILE", "True")
env = EnvWriter(read_dot_env_file=False)

DEBUG = env.bool("DEBUG", default=False, help_text="Set Django Debug mode to on or off")
MAX_CONNECTIONS = env.int("MAX_CONNECTIONS", help_text="Maximum number of connections allowed to the database")
SECRET_KEY = env(
    "SECRET_KEY",
    initial=lambda: base64.b64encode(os.urandom(60)).decode(),
    help_text="Django's SECRET_KEY used to provide cryptographic signing.",
)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[], help_text="List of allowed hosts that this Django site can serve")
INTERNAL_IPS = env.list("INTERNAL_IPS", default=["127.0.0.1"], help_text="IPs allowed to run in debug mode")
REDIS_URL = env("REDIS_URL", default="redis://redis:6379/0", help_text="Redis URL for connecting to redis")
DATABASES = {
    "default": env.dj_db_url(
        "DATABASE_URL",
        default=f"sqlite:///{env.base_dir}/db.sqlite",
        help_text="Database URL for connecting to database",
    )
}
email = env.dj_email_url(
    "EMAIL_URL",
    default="smtp://[email protected]:[email protected]:587/?ssl=True&_default_from_email=President%20Skroob%20%[email protected]%3E",
    help_text="URL used for setting Django's email settings",
)

env.write_env_file(overwrite_existing=True)

from environs.

epicserve avatar epicserve commented on June 11, 2024

The previous POC code generates the following .env file.

# This is an initial .env file generated on 2023-09-01T19:06:59.482643+00:00. Any environment variable with a default
# can be safely removed or commented out. Any variable without a default must be set.

# Set Django Debug mode to on or off
# type: bool
# default: False
# DEBUG=

# Maximum number of connections allowed to the database
# type: int
MAX_CONNECTIONS=

# Django's SECRET_KEY used to provide cryptographic signing.
# type: str
SECRET_KEY=JtMmaa4NOcM6H84tXWjzVQmNobPWvVY5nahPcS17U6zftSkm9G2yO/GDHDUj7Sr0Q0s0lOsS32L/1FY0

# List of allowed hosts that this Django site can serve
# type: list
# default: []
# ALLOWED_HOSTS=

# IPs allowed to run in debug mode
# type: list
# default: ['127.0.0.1']
# INTERNAL_IPS=

# Redis URL for connecting to redis
# type: str
# default: redis://redis:6379/0
# REDIS_URL=

# Database URL for connecting to database
# type: dj_db_url
# default: sqlite:////opt/project/db.sqlite
# DATABASE_URL=

# URL used for setting Django's email settings
# type: dj_email_url
# default: smtp://[email protected]:[email protected]:587/?ssl=True&_default_from_email=President%20Skroob%20%[email protected]%3E
# EMAIL_URL=

from environs.

epicserve avatar epicserve commented on June 11, 2024

@sloria, any thoughts on this? I put a lot of thought and time into this.

I made another POC Pull Request into Django Base Site, if you want to look at that as well. It seems like something like this could be very helpful!

from environs.

sloria avatar sloria commented on June 11, 2024

Sorry for the delay in responding. I'd say that creating boilerplate .env files is out of the scope of environs. This library is unopinionated and is used in a wide variety of use cases (not only web applications), so any template .env file is bound to be overprescriptive. Keeping this feature in app boilerplates like the one you linked to makes a lot of sense to me!

from environs.

epicserve avatar epicserve commented on June 11, 2024

@sloria, How does adding a way to generate an .env file to any Python project, whether it's a web, CLI, or whatever type project make environs more opinionated?

Any python project that you have to manually figure out where each env() call is being made and determine the intent and reason behind the variable, creates a lot of extra wasted time, when all of that information could be be self documenting.

from environs.

sloria avatar sloria commented on June 11, 2024

environs scope is limited to parsing and validating envvars--it has no opinions on what those envvars are and what they're used for.

A command for writing a boilerplate .env file is the domain of project scaffolds.

from environs.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.