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]]
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"

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

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):
"""Generate the sitemap config file.

View File

@ -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")

View File

@ -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"

View File

@ -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"]