Add Database configuration object (#763)

* Add DB config object (#759)

* Add DBConfig to base (#759)
This commit is contained in:
Kasun Herath 2023-04-04 10:09:44 +05:30 committed by GitHub
parent bb29bd864d
commit e9928d9838
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 460 additions and 2 deletions

View File

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

View File

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

View File

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