Add Database configuration object (#763)
* Add DB config object (#759) * Add DBConfig to base (#759)
This commit is contained in:
parent
bb29bd864d
commit
e9928d9838
@ -9,7 +9,7 @@ from .base import Base
|
||||
from .components import *
|
||||
from .components.component import custom_component as memo
|
||||
from .components.graphing.victory import data
|
||||
from .config import Config
|
||||
from .config import Config, DBConfig
|
||||
from .constants import Env, Transports
|
||||
from .event import (
|
||||
EVENT_ARG,
|
||||
|
@ -1,13 +1,124 @@
|
||||
"""The Pynecone config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
from typing import List, Optional
|
||||
|
||||
from pynecone import constants
|
||||
from pynecone.base import Base
|
||||
|
||||
|
||||
class DBConfig(Base):
|
||||
"""Database config."""
|
||||
|
||||
engine: str
|
||||
username: Optional[str] = ""
|
||||
password: Optional[str] = ""
|
||||
host: Optional[str] = ""
|
||||
port: Optional[int] = None
|
||||
database: str
|
||||
|
||||
@classmethod
|
||||
def postgresql(
|
||||
cls,
|
||||
database: str,
|
||||
username: str,
|
||||
password: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = 5432,
|
||||
) -> DBConfig:
|
||||
"""Create an instance with postgresql engine.
|
||||
|
||||
Args:
|
||||
database: Database name.
|
||||
username: Database username.
|
||||
password: Database password.
|
||||
host: Database host.
|
||||
port: Database port.
|
||||
|
||||
Returns:
|
||||
DBConfig instance.
|
||||
"""
|
||||
return cls(
|
||||
engine="postgresql",
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def postgresql_psycopg2(
|
||||
cls,
|
||||
database: str,
|
||||
username: str,
|
||||
password: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = 5432,
|
||||
) -> DBConfig:
|
||||
"""Create an instance with postgresql+psycopg2 engine.
|
||||
|
||||
Args:
|
||||
database: Database name.
|
||||
username: Database username.
|
||||
password: Database password.
|
||||
host: Database host.
|
||||
port: Database port.
|
||||
|
||||
Returns:
|
||||
DBConfig instance.
|
||||
"""
|
||||
return cls(
|
||||
engine="postgresql+psycopg2",
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def sqlite(
|
||||
cls,
|
||||
database: str,
|
||||
) -> DBConfig:
|
||||
"""Create an instance with sqlite engine.
|
||||
|
||||
Args:
|
||||
database: Database name.
|
||||
|
||||
Returns:
|
||||
DBConfig instance.
|
||||
"""
|
||||
return cls(
|
||||
engine="sqlite",
|
||||
database=database,
|
||||
)
|
||||
|
||||
def get_url(self) -> str:
|
||||
"""Get database URL.
|
||||
|
||||
Returns:
|
||||
The database URL.
|
||||
"""
|
||||
host = (
|
||||
f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
|
||||
)
|
||||
username = urllib.parse.quote_plus(self.username) if self.username else ""
|
||||
password = urllib.parse.quote_plus(self.password) if self.password else ""
|
||||
|
||||
if username:
|
||||
path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
|
||||
else:
|
||||
path = f"{host}"
|
||||
|
||||
return f"{self.engine}://{path}/{self.database}"
|
||||
|
||||
|
||||
class Config(Base):
|
||||
"""A Pynecone config."""
|
||||
|
||||
@ -32,6 +143,9 @@ class Config(Base):
|
||||
# The database url.
|
||||
db_url: Optional[str] = constants.DB_URL
|
||||
|
||||
# The database config.
|
||||
db_config: Optional[DBConfig] = None
|
||||
|
||||
# The redis url.
|
||||
redis_url: Optional[str] = None
|
||||
|
||||
@ -67,6 +181,20 @@ 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
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the config values.
|
||||
|
||||
If db_url is not provided gets it from db_config.
|
||||
|
||||
Args:
|
||||
*args: The args to pass to the Pydantic init method.
|
||||
**kwargs: The kwargs to pass to the Pydantic init method.
|
||||
"""
|
||||
if "db_url" not in kwargs and "db_config" in kwargs:
|
||||
kwargs["db_url"] = kwargs["db_config"].get_url()
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def get_config() -> Config:
|
||||
"""Get the app config.
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""Test fixtures."""
|
||||
import platform
|
||||
from typing import Generator, List
|
||||
from typing import Dict, Generator, List
|
||||
|
||||
import pytest
|
||||
|
||||
import pynecone as pc
|
||||
from pynecone import constants
|
||||
from pynecone.event import EventSpec
|
||||
|
||||
|
||||
@ -262,3 +263,37 @@ def upload_state(tmp_path):
|
||||
self.img_list.append(file.filename)
|
||||
|
||||
return FileUploadState
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_config_values() -> Dict:
|
||||
"""Get base config values.
|
||||
|
||||
Returns:
|
||||
Dictionary of base config values
|
||||
"""
|
||||
return {"app_name": "app", "db_url": constants.DB_URL, "env": pc.Env.DEV}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_db_config_values() -> Dict:
|
||||
"""Get base DBConfig values.
|
||||
|
||||
Returns:
|
||||
Dictionary of base db config values
|
||||
"""
|
||||
return {"database": "db"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sqlite_db_config_values(base_db_config_values) -> Dict:
|
||||
"""Get sqlite DBConfig values.
|
||||
|
||||
Args:
|
||||
base_db_config_values: Base DBConfig fixture.
|
||||
|
||||
Returns:
|
||||
Dictionary of sqlite DBConfig values
|
||||
"""
|
||||
base_db_config_values["engine"] = "sqlite"
|
||||
return base_db_config_values
|
||||
|
91
tests/test_config.py
Normal file
91
tests/test_config.py
Normal file
@ -0,0 +1,91 @@
|
||||
from typing import Dict
|
||||
|
||||
import pytest
|
||||
|
||||
import pynecone as pc
|
||||
from pynecone import constants
|
||||
from pynecone.config import DBConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_no_db_url_values(base_config_values) -> Dict:
|
||||
"""Create config values with no db_url.
|
||||
|
||||
Args:
|
||||
base_config_values: Base config fixture.
|
||||
|
||||
Returns:
|
||||
Config values.
|
||||
"""
|
||||
base_config_values.pop("db_url")
|
||||
return base_config_values
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_empty_db_url_values(base_config_values) -> Dict:
|
||||
"""Create config values with empty db_url.
|
||||
|
||||
Args:
|
||||
base_config_values: Base config values fixture.
|
||||
|
||||
Returns:
|
||||
Config values
|
||||
"""
|
||||
base_config_values["db_url"] = None
|
||||
return base_config_values
|
||||
|
||||
|
||||
def test_config_db_url(base_config_values):
|
||||
"""Test defined db_url is not changed.
|
||||
|
||||
Args:
|
||||
base_config_values: base_config_values fixture.
|
||||
"""
|
||||
config = pc.Config(**base_config_values)
|
||||
assert config.db_url == base_config_values["db_url"]
|
||||
|
||||
|
||||
def test_default_db_url(config_no_db_url_values):
|
||||
"""Test that db_url is assigned the default value if not passed.
|
||||
|
||||
Args:
|
||||
config_no_db_url_values: Config values with no db_url defined.
|
||||
"""
|
||||
config = pc.Config(**config_no_db_url_values)
|
||||
assert config.db_url == constants.DB_URL
|
||||
|
||||
|
||||
def test_empty_db_url(config_empty_db_url_values):
|
||||
"""Test that db_url is not automatically assigned if an empty value is defined.
|
||||
|
||||
Args:
|
||||
config_empty_db_url_values: Config values with empty db_url.
|
||||
"""
|
||||
config = pc.Config(**config_empty_db_url_values)
|
||||
assert config.db_url is None
|
||||
|
||||
|
||||
def test_db_url_precedence(base_config_values, sqlite_db_config_values):
|
||||
"""Test that db_url is not overwritten when db_url is defined.
|
||||
|
||||
Args:
|
||||
base_config_values: config values that include db_ur.
|
||||
sqlite_db_config_values: DB config values.
|
||||
"""
|
||||
db_config = DBConfig(**sqlite_db_config_values)
|
||||
base_config_values["db_config"] = db_config
|
||||
config = pc.Config(**base_config_values)
|
||||
assert config.db_url == base_config_values["db_url"]
|
||||
|
||||
|
||||
def test_db_url_from_db_config(config_no_db_url_values, sqlite_db_config_values):
|
||||
"""Test db_url generation from db_config.
|
||||
|
||||
Args:
|
||||
config_no_db_url_values: Config values with no db_url.
|
||||
sqlite_db_config_values: DB config values.
|
||||
"""
|
||||
db_config = DBConfig(**sqlite_db_config_values)
|
||||
config_no_db_url_values["db_config"] = db_config
|
||||
config = pc.Config(**config_no_db_url_values)
|
||||
assert config.db_url == db_config.get_url()
|
204
tests/test_db_config.py
Normal file
204
tests/test_db_config.py
Normal file
@ -0,0 +1,204 @@
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
from pynecone.config import DBConfig
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"engine,username,password,host,port,database,expected_url",
|
||||
[
|
||||
(
|
||||
"postgresql",
|
||||
"user",
|
||||
"pass",
|
||||
"localhost",
|
||||
5432,
|
||||
"db",
|
||||
"postgresql://user:pass@localhost:5432/db",
|
||||
),
|
||||
(
|
||||
"postgresql",
|
||||
"user",
|
||||
"pass",
|
||||
"localhost",
|
||||
None,
|
||||
"db",
|
||||
"postgresql://user:pass@localhost/db",
|
||||
),
|
||||
(
|
||||
"postgresql",
|
||||
"user",
|
||||
None,
|
||||
"localhost",
|
||||
None,
|
||||
"db",
|
||||
"postgresql://user@localhost/db",
|
||||
),
|
||||
("postgresql", "user", None, None, None, "db", "postgresql://user@/db"),
|
||||
("postgresql", "user", None, None, 5432, "db", "postgresql://user@/db"),
|
||||
(
|
||||
"postgresql",
|
||||
None,
|
||||
None,
|
||||
"localhost",
|
||||
5432,
|
||||
"db",
|
||||
"postgresql://localhost:5432/db",
|
||||
),
|
||||
("sqlite", None, None, None, None, "db.sqlite", "sqlite:///db.sqlite"),
|
||||
],
|
||||
)
|
||||
def test_get_url(engine, username, password, host, port, database, expected_url):
|
||||
"""Test generation of URL.
|
||||
|
||||
Args:
|
||||
engine: Database engine.
|
||||
username: Database username.
|
||||
password: Database password.
|
||||
host: Database host.
|
||||
port: Database port.
|
||||
database: Database name.
|
||||
expected_url: Expected database URL generated.
|
||||
"""
|
||||
db_config = DBConfig(
|
||||
engine=engine,
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
)
|
||||
assert db_config.get_url() == expected_url
|
||||
|
||||
|
||||
def test_url_encode():
|
||||
"""Test username and password are urlencoded when database URL is generated."""
|
||||
username = "user@user"
|
||||
password = "pass@pass"
|
||||
database = "db"
|
||||
username_encoded = urllib.parse.quote_plus(username)
|
||||
password_encoded = urllib.parse.quote_plus(password)
|
||||
engine = "postgresql"
|
||||
|
||||
db_config = DBConfig(
|
||||
engine=engine, username=username, password=password, database=database
|
||||
)
|
||||
assert (
|
||||
db_config.get_url()
|
||||
== f"{engine}://{username_encoded}:{password_encoded}@/{database}"
|
||||
)
|
||||
|
||||
|
||||
def test_url_encode_database_name():
|
||||
"""Test database name is not URL encoded."""
|
||||
username = "user"
|
||||
password = "pass"
|
||||
database = "db@prod"
|
||||
engine = "postgresql"
|
||||
|
||||
db_config = DBConfig(
|
||||
engine=engine, username=username, password=password, database=database
|
||||
)
|
||||
assert db_config.get_url() == f"{engine}://{username}:{password}@/{database}"
|
||||
|
||||
|
||||
def test_constructor_sqlite():
|
||||
"""Test DBConfig.sqlite constructor create the instance correctly."""
|
||||
db_config = DBConfig.sqlite(database="app.db")
|
||||
assert db_config.engine == "sqlite"
|
||||
assert db_config.username == ""
|
||||
assert db_config.password == ""
|
||||
assert db_config.host == ""
|
||||
assert db_config.port is None
|
||||
assert db_config.database == "app.db"
|
||||
assert db_config.get_url() == "sqlite:///app.db"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username,password,host,port,database,expected_url",
|
||||
[
|
||||
(
|
||||
"user",
|
||||
"pass",
|
||||
"localhost",
|
||||
5432,
|
||||
"db",
|
||||
"postgresql://user:pass@localhost:5432/db",
|
||||
),
|
||||
("user", "", "localhost", None, "db", "postgresql://user@localhost/db"),
|
||||
("user", "", "", None, "db", "postgresql://user@/db"),
|
||||
("", "", "localhost", 5432, "db", "postgresql://localhost:5432/db"),
|
||||
("", "", "", None, "db", "postgresql:///db"),
|
||||
],
|
||||
)
|
||||
def test_constructor_postgresql(username, password, host, port, database, expected_url):
|
||||
"""Test DBConfig.postgresql constructor creates the instance correctly.
|
||||
|
||||
Args:
|
||||
username: Database username.
|
||||
password: Database password.
|
||||
host: Database host.
|
||||
port: Database port.
|
||||
database: Database name.
|
||||
expected_url: Expected database URL generated.
|
||||
"""
|
||||
db_config = DBConfig.postgresql(
|
||||
username=username, password=password, host=host, port=port, database=database
|
||||
)
|
||||
assert db_config.engine == "postgresql"
|
||||
assert db_config.username == username
|
||||
assert db_config.password == password
|
||||
assert db_config.host == host
|
||||
assert db_config.port == port
|
||||
assert db_config.database == database
|
||||
assert db_config.get_url() == expected_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username,password,host,port,database,expected_url",
|
||||
[
|
||||
(
|
||||
"user",
|
||||
"pass",
|
||||
"localhost",
|
||||
5432,
|
||||
"db",
|
||||
"postgresql+psycopg2://user:pass@localhost:5432/db",
|
||||
),
|
||||
(
|
||||
"user",
|
||||
"",
|
||||
"localhost",
|
||||
None,
|
||||
"db",
|
||||
"postgresql+psycopg2://user@localhost/db",
|
||||
),
|
||||
("user", "", "", None, "db", "postgresql+psycopg2://user@/db"),
|
||||
("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"),
|
||||
("", "", "", None, "db", "postgresql+psycopg2:///db"),
|
||||
],
|
||||
)
|
||||
def test_constructor_postgresql_psycopg2(
|
||||
username, password, host, port, database, expected_url
|
||||
):
|
||||
"""Test DBConfig.postgresql_psycopg2 constructor creates the instance correctly.
|
||||
|
||||
Args:
|
||||
username: Database username.
|
||||
password: Database password.
|
||||
host: Database host.
|
||||
port: Database port.
|
||||
database: Database name.
|
||||
expected_url: Expected database URL generated.
|
||||
"""
|
||||
db_config = DBConfig.postgresql_psycopg2(
|
||||
username=username, password=password, host=host, port=port, database=database
|
||||
)
|
||||
assert db_config.engine == "postgresql+psycopg2"
|
||||
assert db_config.username == username
|
||||
assert db_config.password == password
|
||||
assert db_config.host == host
|
||||
assert db_config.port == port
|
||||
assert db_config.database == database
|
||||
assert db_config.get_url() == expected_url
|
Loading…
Reference in New Issue
Block a user