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 import *
|
||||||
from .components.component import custom_component as memo
|
from .components.component import custom_component as memo
|
||||||
from .components.graphing.victory import data
|
from .components.graphing.victory import data
|
||||||
from .config import Config
|
from .config import Config, DBConfig
|
||||||
from .constants import Env, Transports
|
from .constants import Env, Transports
|
||||||
from .event import (
|
from .event import (
|
||||||
EVENT_ARG,
|
EVENT_ARG,
|
||||||
|
@ -1,13 +1,124 @@
|
|||||||
"""The Pynecone config."""
|
"""The Pynecone config."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pynecone import constants
|
from pynecone import constants
|
||||||
from pynecone.base import Base
|
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):
|
class Config(Base):
|
||||||
"""A Pynecone config."""
|
"""A Pynecone config."""
|
||||||
|
|
||||||
@ -32,6 +143,9 @@ class Config(Base):
|
|||||||
# The database url.
|
# The database url.
|
||||||
db_url: Optional[str] = constants.DB_URL
|
db_url: Optional[str] = constants.DB_URL
|
||||||
|
|
||||||
|
# The database config.
|
||||||
|
db_config: Optional[DBConfig] = None
|
||||||
|
|
||||||
# The redis url.
|
# The redis url.
|
||||||
redis_url: Optional[str] = None
|
redis_url: Optional[str] = None
|
||||||
|
|
||||||
@ -67,6 +181,20 @@ 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
|
||||||
|
|
||||||
|
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:
|
def get_config() -> Config:
|
||||||
"""Get the app config.
|
"""Get the app config.
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""Test fixtures."""
|
"""Test fixtures."""
|
||||||
import platform
|
import platform
|
||||||
from typing import Generator, List
|
from typing import Dict, Generator, List
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import pynecone as pc
|
import pynecone as pc
|
||||||
|
from pynecone import constants
|
||||||
from pynecone.event import EventSpec
|
from pynecone.event import EventSpec
|
||||||
|
|
||||||
|
|
||||||
@ -262,3 +263,37 @@ def upload_state(tmp_path):
|
|||||||
self.img_list.append(file.filename)
|
self.img_list.append(file.filename)
|
||||||
|
|
||||||
return FileUploadState
|
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