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

View File

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

View File

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