Преглед изворни кода

add support for .env configuration (#1104)

Elijah Ahianyo пре 1 година
родитељ
комит
d4d25c17d8
9 измењених фајлова са 137 додато и 29 уклоњено
  1. 17 2
      poetry.lock
  2. 3 6
      pynecone/app.py
  3. 45 5
      pynecone/config.py
  4. 39 8
      pynecone/constants.py
  5. 11 2
      pynecone/pc.py
  6. 12 0
      pynecone/utils/build.py
  7. 2 2
      pynecone/utils/exec.py
  8. 1 0
      pyproject.toml
  9. 7 4
      tests/test_config.py

+ 17 - 2
poetry.lock

@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
 [[package]]
 name = "anyio"
@@ -1136,6 +1136,21 @@ files = [
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-dotenv"
+version = "0.13.0"
+description = "Add .env support to your django/flask apps in development and deployments"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "python-dotenv-0.13.0.tar.gz", hash = "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74"},
+    {file = "python_dotenv-0.13.0-py2.py3-none-any.whl", hash = "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
 [[package]]
 name = "python-engineio"
 version = "4.4.1"
@@ -1787,4 +1802,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "693f9d308dcdc9788aba818f480e0cadecf9d5f97e43cc66d1a69da7aa06204f"
+content-hash = "ad76c12812d3aedc9a36727b7e69e9b064077e365ebacc4be904d09824ddac4b"

+ 3 - 6
pynecone/app.py

@@ -88,15 +88,12 @@ class App(Base):
         self.add_cors()
         self.add_default_endpoints()
 
-        # Set up CORS options.
-        cors_allowed_origins = config.cors_allowed_origins
-        if config.cors_allowed_origins == [constants.CORS_ALLOWED_ORIGINS]:
-            cors_allowed_origins = "*"
-
         # Set up the Socket.IO AsyncServer.
         self.sio = AsyncServer(
             async_mode="asgi",
-            cors_allowed_origins=cors_allowed_origins,
+            cors_allowed_origins="*"
+            if config.cors_allowed_origins == constants.CORS_ALLOWED_ORIGINS
+            else config.cors_allowed_origins,
             cors_credentials=config.cors_credentials,
             max_http_buffer_size=config.polling_max_http_buffer_size,
             ping_interval=constants.PING_INTERVAL,

+ 45 - 5
pynecone/config.py

@@ -2,11 +2,14 @@
 
 from __future__ import annotations
 
+import importlib
 import os
 import sys
 import urllib.parse
 from typing import List, Optional
 
+from dotenv import load_dotenv
+
 from pynecone import constants
 from pynecone.base import Base
 
@@ -129,9 +132,9 @@ class Config(Base):
     username: Optional[str] = None
 
     # The frontend port.
-    port: str = constants.FRONTEND_PORT
+    frontend_port: str = constants.FRONTEND_PORT
 
-    # The frontend port.
+    # The backend port.
     backend_port: str = constants.BACKEND_PORT
 
     # The backend host.
@@ -141,7 +144,7 @@ class Config(Base):
     api_url: str = constants.API_URL
 
     # The deploy url.
-    deploy_url: Optional[str] = None
+    deploy_url: Optional[str] = constants.DEPLOY_URL
 
     # The database url.
     db_url: Optional[str] = constants.DB_URL
@@ -150,7 +153,7 @@ class Config(Base):
     db_config: Optional[DBConfig] = None
 
     # The redis url.
-    redis_url: Optional[str] = None
+    redis_url: Optional[str] = constants.REDIS_URL
 
     # Telemetry opt-in.
     telemetry_enabled: bool = True
@@ -176,7 +179,7 @@ class Config(Base):
     ] = constants.Transports.WEBSOCKET_POLLING
 
     # List of origins that are allowed to connect to the backend API.
-    cors_allowed_origins: Optional[list] = [constants.CORS_ALLOWED_ORIGINS]
+    cors_allowed_origins: Optional[list] = constants.CORS_ALLOWED_ORIGINS
 
     # Whether credentials (cookies, authentication) are allowed in requests to the backend API.
     cors_credentials: Optional[bool] = True
@@ -184,6 +187,12 @@ class Config(Base):
     # The maximum size of a message when using the polling backend transport.
     polling_max_http_buffer_size: Optional[int] = constants.POLLING_MAX_HTTP_BUFFER_SIZE
 
+    # Dotenv file path
+    env_path: Optional[str] = constants.DOT_ENV_FILE
+
+    # Whether to override OS environment variables
+    override_os_envs: Optional[bool] = True
+
     def __init__(self, *args, **kwargs):
         """Initialize the config values.
 
@@ -198,6 +207,36 @@ class Config(Base):
 
         super().__init__(*args, **kwargs)
 
+        # set overriden class attribute values as os env variables to avoid losing them
+        for key, value in dict(self).items():
+            key = key.upper()
+            if (
+                key.startswith("_")
+                or key in os.environ
+                or (value is None and key != "DB_URL")
+            ):
+                continue
+            os.environ[key] = str(value)
+
+        # Load env variables from env file
+        load_dotenv(self.env_path, override=self.override_os_envs)  # type: ignore
+        # Recompute constants after loading env variables
+        importlib.reload(constants)
+        # Recompute instance attributes
+        self.recompute_field_values()
+
+    def recompute_field_values(self):
+        """Recompute instance field values to reflect new values after reloading
+        constant values.
+        """
+        for field in self.get_fields():
+            try:
+                if field.startswith("_"):
+                    continue
+                setattr(self, field, getattr(constants, f"{field.upper()}"))
+            except AttributeError:
+                pass
+
 
 def get_config() -> Config:
     """Get the app config.
@@ -210,5 +249,6 @@ def get_config() -> Config:
     sys.path.append(os.getcwd())
     try:
         return __import__(constants.CONFIG_MODULE).config
+
     except ImportError:
         return Config(app_name="")  # type: ignore

+ 39 - 8
pynecone/constants.py

@@ -4,9 +4,36 @@ import os
 import re
 from enum import Enum
 from types import SimpleNamespace
+from typing import Any, Type
 
 import pkg_resources
 
+
+def get_value(key: str, default: Any = None, type_: Type = str) -> Type:
+    """Get the value for the constant.
+    Obtain os env value and cast non-string types into
+    their original types.
+
+    Args:
+        key: constant name.
+        default: default value if key doesn't exist.
+        type_: the type of the constant.
+
+    Returns:
+        the value of the constant in its designated type
+    """
+    value = os.getenv(key, default)
+    try:
+        if value and type_ != str:
+            value = eval(value)
+    except Exception:
+        pass
+    finally:
+        # Special case for db_url expects None to be a valid input when
+        # user explicitly overrides db_url as None
+        return value if value != "None" else None  # noqa B012
+
+
 # App names and versions.
 # The name of the Pynecone package.
 MODULE_NAME = "pynecone"
@@ -61,12 +88,15 @@ PCVERSION_APP_FILE = os.path.join(WEB_DIR, "pynecone.json")
 ENV_JSON = os.path.join(WEB_DIR, "env.json")
 
 # Commands to run the app.
+DOT_ENV_FILE = ".env"
 # The frontend default port.
-FRONTEND_PORT = "3000"
+FRONTEND_PORT = get_value("FRONTEND_PORT", "3000")
 # The backend default port.
-BACKEND_PORT = "8000"
+BACKEND_PORT = get_value("BACKEND_PORT", "8000")
 # The backend api url.
-API_URL = "http://localhost:8000"
+API_URL = get_value("API_URL", "http://localhost:8000")
+# The deploy url
+DEPLOY_URL = get_value("DEPLOY_URL")
 # bun root location
 BUN_ROOT_PATH = "$HOME/.bun"
 # The default path where bun is installed.
@@ -74,7 +104,7 @@ BUN_PATH = f"{BUN_ROOT_PATH}/bin/bun"
 # Command to install bun.
 INSTALL_BUN = f"curl -fsSL https://bun.sh/install | bash -s -- bun-v{MAX_BUN_VERSION}"
 # Default host in dev mode.
-BACKEND_HOST = "0.0.0.0"
+BACKEND_HOST = get_value("BACKEND_HOST", "0.0.0.0")
 # The default timeout when launching the gunicorn server.
 TIMEOUT = 120
 # The command to run the backend in production mode.
@@ -122,9 +152,11 @@ FRONTEND_ZIP = "frontend.zip"
 # The name of the backend zip during deployment.
 BACKEND_ZIP = "backend.zip"
 # The name of the sqlite database.
-DB_NAME = "pynecone.db"
+DB_NAME = os.getenv("DB_NAME", "pynecone.db")
 # The sqlite url.
-DB_URL = f"sqlite:///{DB_NAME}"
+DB_URL = get_value("DB_URL", f"sqlite:///{DB_NAME}")
+# The redis url
+REDIS_URL = get_value("REDIS_URL")
 # The default title to show for Pynecone apps.
 DEFAULT_TITLE = "Pynecone App"
 # The default description to show for Pynecone apps.
@@ -134,7 +166,6 @@ DEFAULT_IMAGE = "favicon.ico"
 # The default meta list to show for Pynecone apps.
 DEFAULT_META_LIST = []
 
-
 # The gitignore file.
 GITIGNORE_FILE = ".gitignore"
 # Files to gitignore.
@@ -306,5 +337,5 @@ COLOR_MODE = "colorMode"
 TOGGLE_COLOR_MODE = "toggleColorMode"
 
 # Server socket configuration variables
-CORS_ALLOWED_ORIGINS = "*"
+CORS_ALLOWED_ORIGINS = get_value("CORS_ALLOWED_ORIGINS", ["*"], list)
 POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000

+ 11 - 2
pynecone/pc.py

@@ -72,7 +72,7 @@ def run(
     loglevel: constants.LogLevel = typer.Option(
         constants.LogLevel.ERROR, help="The log level to use."
     ),
-    port: str = typer.Option(None, help="Specify a different frontend port."),
+    frontend_port: str = typer.Option(None, help="Specify a different frontend port."),
     backend_port: str = typer.Option(None, help="Specify a different backend port."),
     backend_host: str = typer.Option(None, help="Specify the backend host."),
 ):
@@ -81,8 +81,17 @@ def run(
         console.print(
             "[yellow][WARNING] We strongly advise you to use Windows Subsystem for Linux (WSL) for optimal performance when using Pynecone. Due to compatibility issues with one of our dependencies, Bun, you may experience slower performance on Windows. By using WSL, you can expect to see a significant speed increase."
         )
+    # Set ports as os env variables to take precedence over config and
+    # .env variables(if override_os_envs flag in config is set to False).
+    build.set_os_env(
+        frontend_port=frontend_port,
+        backend_port=backend_port,
+        backend_host=backend_host,
+    )
 
-    frontend_port = get_config().port if port is None else port
+    frontend_port = (
+        get_config().frontend_port if frontend_port is None else frontend_port
+    )
     backend_port = get_config().backend_port if backend_port is None else backend_port
     backend_host = get_config().backend_host if backend_host is None else backend_host
 

+ 12 - 0
pynecone/utils/build.py

@@ -58,6 +58,18 @@ def set_environment_variables():
     )
 
 
+def set_os_env(**kwargs):
+    """Set os environment variables.
+
+    Args:
+        kwargs: env key word args.
+    """
+    for key, value in kwargs.items():
+        if not value:
+            continue
+        os.environ[key.upper()] = value
+
+
 def generate_sitemap(deploy_url: str):
     """Generate the sitemap config file.
 

+ 2 - 2
pynecone/utils/exec.py

@@ -96,7 +96,7 @@ def run_frontend(
 
     # Run the frontend in development mode.
     console.rule("[bold green]App Running")
-    os.environ["PORT"] = get_config().port if port is None else port
+    os.environ["PORT"] = get_config().frontend_port if port is None else port
     run_process_and_launch_url(
         [prerequisites.get_package_manager(), "run", "dev"], root, loglevel
     )
@@ -123,7 +123,7 @@ def run_frontend_prod(
     export_app(app, loglevel=loglevel)
 
     # Set the port.
-    os.environ["PORT"] = get_config().port if port is None else port
+    os.environ["PORT"] = get_config().frontend_port if port is None else port
 
     # Run the frontend in production mode.
     console.rule("[bold green]App Running")

+ 1 - 0
pyproject.toml

@@ -41,6 +41,7 @@ typer = "0.4.2"
 uvicorn = "^0.20.0"
 watchdog = "^2.3.1"
 websockets = "^10.4"
+python-dotenv = "0.13.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"

+ 7 - 4
tests/test_config.py

@@ -1,3 +1,4 @@
+import os
 from typing import Dict
 
 import pytest
@@ -21,18 +22,19 @@ def config_no_db_url_values(base_config_values) -> Dict:
     return base_config_values
 
 
-@pytest.fixture
-def config_empty_db_url_values(base_config_values) -> Dict:
+@pytest.fixture(autouse=True)
+def config_empty_db_url_values(base_config_values):
     """Create config values with empty db_url.
 
     Args:
         base_config_values: Base config values fixture.
 
-    Returns:
+    Yields:
         Config values
     """
     base_config_values["db_url"] = None
-    return base_config_values
+    yield base_config_values
+    os.environ.pop("DB_URL")
 
 
 def test_config_db_url(base_config_values):
@@ -41,6 +43,7 @@ def test_config_db_url(base_config_values):
     Args:
         base_config_values: base_config_values fixture.
     """
+    os.environ.pop("DB_URL")
     config = pc.Config(**base_config_values)
     assert config.db_url == base_config_values["db_url"]