add support for .env configuration (#1104)

This commit is contained in:
Elijah Ahianyo 2023-06-01 00:26:03 +00:00 committed by GitHub
parent d4b5c78406
commit d4d25c17d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 29 deletions

19
poetry.lock generated
View File

@ -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]] [[package]]
name = "anyio" name = "anyio"
@ -1136,6 +1136,21 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" 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]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.4.1" version = "4.4.1"
@ -1787,4 +1802,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "693f9d308dcdc9788aba818f480e0cadecf9d5f97e43cc66d1a69da7aa06204f" content-hash = "ad76c12812d3aedc9a36727b7e69e9b064077e365ebacc4be904d09824ddac4b"

View File

@ -88,15 +88,12 @@ class App(Base):
self.add_cors() self.add_cors()
self.add_default_endpoints() 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. # Set up the Socket.IO AsyncServer.
self.sio = AsyncServer( self.sio = AsyncServer(
async_mode="asgi", 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, cors_credentials=config.cors_credentials,
max_http_buffer_size=config.polling_max_http_buffer_size, max_http_buffer_size=config.polling_max_http_buffer_size,
ping_interval=constants.PING_INTERVAL, ping_interval=constants.PING_INTERVAL,

View File

@ -2,11 +2,14 @@
from __future__ import annotations from __future__ import annotations
import importlib
import os import os
import sys import sys
import urllib.parse import urllib.parse
from typing import List, Optional from typing import List, Optional
from dotenv import load_dotenv
from pynecone import constants from pynecone import constants
from pynecone.base import Base from pynecone.base import Base
@ -129,9 +132,9 @@ class Config(Base):
username: Optional[str] = None username: Optional[str] = None
# The frontend port. # 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 backend_port: str = constants.BACKEND_PORT
# The backend host. # The backend host.
@ -141,7 +144,7 @@ class Config(Base):
api_url: str = constants.API_URL api_url: str = constants.API_URL
# The deploy url. # The deploy url.
deploy_url: Optional[str] = None deploy_url: Optional[str] = constants.DEPLOY_URL
# The database url. # The database url.
db_url: Optional[str] = constants.DB_URL db_url: Optional[str] = constants.DB_URL
@ -150,7 +153,7 @@ class Config(Base):
db_config: Optional[DBConfig] = None db_config: Optional[DBConfig] = None
# The redis url. # The redis url.
redis_url: Optional[str] = None redis_url: Optional[str] = constants.REDIS_URL
# Telemetry opt-in. # Telemetry opt-in.
telemetry_enabled: bool = True telemetry_enabled: bool = True
@ -176,7 +179,7 @@ class Config(Base):
] = constants.Transports.WEBSOCKET_POLLING ] = constants.Transports.WEBSOCKET_POLLING
# List of origins that are allowed to connect to the backend API. # 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. # Whether credentials (cookies, authentication) are allowed in requests to the backend API.
cors_credentials: Optional[bool] = True cors_credentials: Optional[bool] = True
@ -184,6 +187,12 @@ class Config(Base):
# The maximum size of a message when using the polling backend transport. # 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 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): def __init__(self, *args, **kwargs):
"""Initialize the config values. """Initialize the config values.
@ -198,6 +207,36 @@ class Config(Base):
super().__init__(*args, **kwargs) 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: def get_config() -> Config:
"""Get the app config. """Get the app config.
@ -210,5 +249,6 @@ def get_config() -> Config:
sys.path.append(os.getcwd()) sys.path.append(os.getcwd())
try: try:
return __import__(constants.CONFIG_MODULE).config return __import__(constants.CONFIG_MODULE).config
except ImportError: except ImportError:
return Config(app_name="") # type: ignore return Config(app_name="") # type: ignore

View File

@ -4,9 +4,36 @@ import os
import re import re
from enum import Enum from enum import Enum
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Type
import pkg_resources 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. # App names and versions.
# The name of the Pynecone package. # The name of the Pynecone package.
MODULE_NAME = "pynecone" 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") ENV_JSON = os.path.join(WEB_DIR, "env.json")
# Commands to run the app. # Commands to run the app.
DOT_ENV_FILE = ".env"
# The frontend default port. # The frontend default port.
FRONTEND_PORT = "3000" FRONTEND_PORT = get_value("FRONTEND_PORT", "3000")
# The backend default port. # The backend default port.
BACKEND_PORT = "8000" BACKEND_PORT = get_value("BACKEND_PORT", "8000")
# The backend api url. # 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 location
BUN_ROOT_PATH = "$HOME/.bun" BUN_ROOT_PATH = "$HOME/.bun"
# The default path where bun is installed. # The default path where bun is installed.
@ -74,7 +104,7 @@ BUN_PATH = f"{BUN_ROOT_PATH}/bin/bun"
# Command to install bun. # Command to install bun.
INSTALL_BUN = f"curl -fsSL https://bun.sh/install | bash -s -- bun-v{MAX_BUN_VERSION}" INSTALL_BUN = f"curl -fsSL https://bun.sh/install | bash -s -- bun-v{MAX_BUN_VERSION}"
# Default host in dev mode. # 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. # The default timeout when launching the gunicorn server.
TIMEOUT = 120 TIMEOUT = 120
# The command to run the backend in production mode. # 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. # The name of the backend zip during deployment.
BACKEND_ZIP = "backend.zip" BACKEND_ZIP = "backend.zip"
# The name of the sqlite database. # The name of the sqlite database.
DB_NAME = "pynecone.db" DB_NAME = os.getenv("DB_NAME", "pynecone.db")
# The sqlite url. # 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. # The default title to show for Pynecone apps.
DEFAULT_TITLE = "Pynecone App" DEFAULT_TITLE = "Pynecone App"
# The default description to show for Pynecone apps. # 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. # The default meta list to show for Pynecone apps.
DEFAULT_META_LIST = [] DEFAULT_META_LIST = []
# The gitignore file. # The gitignore file.
GITIGNORE_FILE = ".gitignore" GITIGNORE_FILE = ".gitignore"
# Files to gitignore. # Files to gitignore.
@ -306,5 +337,5 @@ COLOR_MODE = "colorMode"
TOGGLE_COLOR_MODE = "toggleColorMode" TOGGLE_COLOR_MODE = "toggleColorMode"
# Server socket configuration variables # Server socket configuration variables
CORS_ALLOWED_ORIGINS = "*" CORS_ALLOWED_ORIGINS = get_value("CORS_ALLOWED_ORIGINS", ["*"], list)
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000 POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000

View File

@ -72,7 +72,7 @@ def run(
loglevel: constants.LogLevel = typer.Option( loglevel: constants.LogLevel = typer.Option(
constants.LogLevel.ERROR, help="The log level to use." 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_port: str = typer.Option(None, help="Specify a different backend port."),
backend_host: str = typer.Option(None, help="Specify the backend host."), backend_host: str = typer.Option(None, help="Specify the backend host."),
): ):
@ -81,8 +81,17 @@ def run(
console.print( 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." "[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_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 backend_host = get_config().backend_host if backend_host is None else backend_host

View File

@ -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): def generate_sitemap(deploy_url: str):
"""Generate the sitemap config file. """Generate the sitemap config file.

View File

@ -96,7 +96,7 @@ def run_frontend(
# Run the frontend in development mode. # Run the frontend in development mode.
console.rule("[bold green]App Running") 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( run_process_and_launch_url(
[prerequisites.get_package_manager(), "run", "dev"], root, loglevel [prerequisites.get_package_manager(), "run", "dev"], root, loglevel
) )
@ -123,7 +123,7 @@ def run_frontend_prod(
export_app(app, loglevel=loglevel) export_app(app, loglevel=loglevel)
# Set the port. # 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. # Run the frontend in production mode.
console.rule("[bold green]App Running") console.rule("[bold green]App Running")

View File

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

View File

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