Compare commits

..

No commits in common. "07aac38dbcbd1d6dea1c0baa7ea1d575a92e53fa" and "0d884e583670f7349cd6060a86ac4f9cb568966d" have entirely different histories.

14 changed files with 39 additions and 69 deletions

View file

@ -1,7 +1,6 @@
{ {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8080, "port": 8080,
"dev_mode": false,
"logging": { "logging": {
"level": "info" "level": "info"
} }

View file

@ -3,18 +3,7 @@ with python313Packages;
buildPythonApplication { buildPythonApplication {
pname = "cereal"; pname = "cereal";
version = "0.0.1"; version = "0.0.1";
propagatedBuildInputs = [ propagatedBuildInputs = [ flask requests waitress sqlalchemy argon2-cffi pyjwt cryptography jinja2];
flask
requests
waitress
sqlalchemy
argon2-cffi
pyjwt
cryptography
jinja2
flask-talisman
flask-cors
];
src = ./.; src = ./.;
pyproject = true; pyproject = true;
build-system = [setuptools]; build-system = [setuptools];

View file

@ -9,8 +9,6 @@ let
pyjwt pyjwt
cryptography cryptography
jinja2 jinja2
flask-talisman
flask-cors
]); ]);
in in
with pkgs; with pkgs;

View file

View file

@ -1,6 +1,7 @@
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from .logging import Logging from .logging import Logging
from .email import Email from .email import Email
from .auth import Auth from .auth import Auth
@ -12,7 +13,6 @@ from .parse import parse_nested_config
class Config: class Config:
host: str host: str
port: int | None port: int | None
dev_mode: bool
logging: Logging logging: Logging
email: Email email: Email
database: Database database: Database
@ -25,17 +25,9 @@ class Config:
db_config = parse_nested_config(config, 'database', Database.from_dict) db_config = parse_nested_config(config, 'database', Database.from_dict)
auth_config = parse_nested_config(config, 'auth', Auth.from_dict) auth_config = parse_nested_config(config, 'auth', Auth.from_dict)
mode = config.get('dev_mode')
if mode != True: # noqa: E712
# Explicitly checking for True and falling back to false for anything else. Only explicitly setting
# dev_mode to true will run in dev mode
mode = False
return Config( return Config(
host=config.get('host', '0.0.0.0'), host=config.get('host', '0.0.0.0'),
port=config.get('port'), port=config.get('port'),
dev_mode=mode,
email=email_config, email=email_config,
logging=log_config, logging=log_config,
database=db_config, database=db_config,

View file

@ -1,4 +1,4 @@
from .db import Base, CRUDRepoImpl, UpdateMissingEntryError, get_database from .db import Base, CRUD, CRUDRepo, UpdateMissingEntryError, get_database
from .users import UserModel, UserRepoImpl from .users import UserModel, UserRepoImpl
from .publication import ( from .publication import (
PublicationEntryModel, PublicationEntryModel,
@ -11,6 +11,7 @@ from .subscription import SubscriptionModel, SubscriptionRepoImpl
__all__ = [ __all__ = [
'Base', 'Base',
'CRUD',
'CRUDRepo', 'CRUDRepo',
'UpdateMissingEntryError', 'UpdateMissingEntryError',
'get_database', 'get_database',

View file

@ -1,8 +1,8 @@
from src.services.repo import CRUD
from typing import Callable from typing import Callable
from sqlalchemy.orm import DeclarativeBase, Session from sqlalchemy.orm import DeclarativeBase, Session
from src.config.database import Database as DatabaseConfig from src.config.database import Database as DatabaseConfig
from sqlalchemy import create_engine, Engine from sqlalchemy import create_engine, Engine
import abc
class Base(DeclarativeBase): class Base(DeclarativeBase):
@ -22,7 +22,25 @@ class UpdateMissingEntryError(Exception):
super().__init__(f'Attempted to update non-existing {model_name} {id}') super().__init__(f'Attempted to update non-existing {model_name} {id}')
class CRUDRepoImpl[T: Base, U, C](CRUD[U, C]): class CRUD[T, C](abc.ABC):
@abc.abstractmethod
def create(self, domain_item: C, verify: Callable[[T], None] | None) -> T:
pass
@abc.abstractmethod
def get(self, item_id: int) -> T | None:
pass
@abc.abstractmethod
def update(self, item_id: int, transform: Callable[[T], T]) -> T:
pass
@abc.abstractmethod
def delete(self, item_id: int, verify: Callable[[T], None] | None):
pass
class CRUDRepo[T: Base, U, C](CRUD[U, C]):
def __init__( def __init__(
self, self,
db: Engine, db: Engine,

View file

@ -1,12 +1,12 @@
from sqlalchemy import Engine from sqlalchemy import Engine
from typing import Callable from typing import Callable
from src.infra import CRUDRepoImpl, Base from src.infra import CRUDRepo, Base
import pytest import pytest
class CRUDRepoHelper[A: Base, B, C]: class CRUDRepoHelper[A: Base, B, C]:
@pytest.fixture @pytest.fixture
def repo(self, db: Engine) -> CRUDRepoImpl[A, B, C]: def repo(self, db: Engine) -> CRUDRepo[A, B, C]:
import pdb import pdb
pdb.set_trace() pdb.set_trace()
@ -17,7 +17,7 @@ class CRUDRepoHelper[A: Base, B, C]:
raise NotImplementedError() raise NotImplementedError()
@pytest.fixture @pytest.fixture
def item(self, repo: CRUDRepoImpl[A, B, C], dto: C) -> B: def item(self, repo: CRUDRepo[A, B, C], dto: C) -> B:
return repo.create(dto, None) return repo.create(dto, None)
@pytest.fixture @pytest.fixture
@ -31,28 +31,26 @@ class CRUDRepoHelper[A: Base, B, C]:
def test_create(self, item: B): def test_create(self, item: B):
assert item is not None assert item is not None
def test_create_returns_none_if_invalid(self, repo: CRUDRepoImpl[A, B, C], dto: C): def test_create_returns_none_if_invalid(self, repo: CRUDRepo[A, B, C], dto: C):
def always_invalid(*_args, **_kw): def always_invalid(*_args, **_kw):
raise ValueError('test') raise ValueError('test')
with pytest.raises(ValueError): with pytest.raises(ValueError):
repo.create(dto, always_invalid) repo.create(dto, always_invalid)
def test_create_returns_a_value_if_valid(self, repo: CRUDRepoImpl[A, B, C], dto: C): def test_create_returns_a_value_if_valid(self, repo: CRUDRepo[A, B, C], dto: C):
assert repo.create(dto, lambda _: None) is not None assert repo.create(dto, lambda _: None) is not None
def test_get_returns_none_when_missing(self, repo: CRUDRepoImpl[A, B, C]): def test_get_returns_none_when_missing(self, repo: CRUDRepo[A, B, C]):
assert repo.get(42) is None assert repo.get(42) is None
def test_get_returns_a_value_if_it_exists(self, repo: CRUDRepoImpl[A, B, C], item: B): def test_get_returns_a_value_if_it_exists(self, repo: CRUDRepo[A, B, C], item: B):
assert repo.get(1) == item assert repo.get(1) == item
def test_update( def test_update(self, repo: CRUDRepo[A, B, C], transform: Callable[[B], B], item: B, expected_transformed_value: B):
self, repo: CRUDRepoImpl[A, B, C], transform: Callable[[B], B], item: B, expected_transformed_value: B
):
assert repo.update(1, transform) == expected_transformed_value assert repo.update(1, transform) == expected_transformed_value
def test_delete(self, repo: CRUDRepoImpl[A, B, C], item: B): def test_delete(self, repo: CRUDRepo[A, B, C], item: B):
assert repo.get(1) is not None assert repo.get(1) is not None
repo.delete(1, None) repo.delete(1, None)
assert repo.get(1) is None assert repo.get(1) is None

View file

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from datetime import datetime, timedelta from datetime import datetime, timedelta
from src.infra.users import UserModel from src.infra.users import UserModel
from sqlalchemy.orm import Mapped, mapped_column, relationship, Session, joinedload from sqlalchemy.orm import Mapped, mapped_column, relationship, Session, joinedload
from src.infra.db import Base, CRUDRepoImpl from src.infra.db import Base, CRUDRepo
from sqlalchemy import Engine, ForeignKey, CheckConstraint, DateTime, func, UniqueConstraint, select from sqlalchemy import Engine, ForeignKey, CheckConstraint, DateTime, func, UniqueConstraint, select
from src.services.subscription import ( from src.services.subscription import (
SubscriptionRepo, SubscriptionRepo,
@ -62,7 +62,7 @@ def mutate_subscription(db_sub: SubscriptionModel, sub: Subscription):
db_sub.sequence_notified = sub.sequence_notified db_sub.sequence_notified = sub.sequence_notified
class SubscriptionRepoImpl(SubscriptionRepo, CRUDRepoImpl[SubscriptionModel, Subscription, SubscriptionCreateParams]): class SubscriptionRepoImpl(SubscriptionRepo, CRUDRepo[SubscriptionModel, Subscription, SubscriptionCreateParams]):
def __init__(self, db: Engine): def __init__(self, db: Engine):
super().__init__( super().__init__(
db, SubscriptionModel, model_to_subscription, mutate_subscription, lambda u: SubscriptionModel(**asdict(u)) db, SubscriptionModel, model_to_subscription, mutate_subscription, lambda u: SubscriptionModel(**asdict(u))

View file

@ -6,7 +6,7 @@ from src.config.email import EmailAddress
from sqlalchemy import String, VARCHAR, Engine, select from sqlalchemy import String, VARCHAR, Engine, select
from sqlalchemy.orm import mapped_column, Mapped, Session, relationship from sqlalchemy.orm import mapped_column, Mapped, Session, relationship
from argon2 import PasswordHasher from argon2 import PasswordHasher
from src.infra.db import Base, CRUDRepoImpl from src.infra.db import Base, CRUDRepo
from src.services.users.data import UserDTO, User from src.services.users.data import UserDTO, User
from src.services.users.repo import UserRepo from src.services.users.repo import UserRepo
@ -52,7 +52,7 @@ def make_create_user(hasher: PasswordHasher):
return create_user return create_user
class UserRepoImpl(CRUDRepoImpl[UserModel, User, UserDTO], UserRepo): class UserRepoImpl(CRUDRepo[UserModel, User, UserDTO], UserRepo):
def __init__(self, db: Engine): def __init__(self, db: Engine):
hasher = PasswordHasher() hasher = PasswordHasher()
super().__init__(db, UserModel, model_to_user, mutate_user, make_create_user(hasher)) super().__init__(db, UserModel, model_to_user, mutate_user, make_create_user(hasher))

View file

@ -9,9 +9,6 @@ from dataclasses import dataclass
from src.infra import get_database, UserRepoImpl from src.infra import get_database, UserRepoImpl
from waitress import serve from waitress import serve
from flask import Flask from flask import Flask
from flask_cors import CORS
from flask_talisman import Talisman
import argparse import argparse
import logging import logging
@ -36,8 +33,6 @@ class MyFlask(Flask):
def create_app(name: str, config: Config) -> Flask: def create_app(name: str, config: Config) -> Flask:
logging.basicConfig(level=config.logging.level, format=('%(asctime)s %(levelname)s [%(name)s] %(message)s')) logging.basicConfig(level=config.logging.level, format=('%(asctime)s %(levelname)s [%(name)s] %(message)s'))
app = MyFlask(name) app = MyFlask(name)
CORS(app)
Talisman(app)
# Configure Services # Configure Services
database = get_database(config.database) database = get_database(config.database)

View file

@ -1,20 +0,0 @@
from typing import Callable
import abc
class CRUD[T, C](abc.ABC):
@abc.abstractmethod
def create(self, domain_item: C, verify: Callable[[T], None] | None) -> T:
pass
@abc.abstractmethod
def get(self, item_id: int) -> T | None:
pass
@abc.abstractmethod
def update(self, item_id: int, transform: Callable[[T], T]) -> T:
pass
@abc.abstractmethod
def delete(self, item_id: int, verify: Callable[[T], None] | None):
pass

View file

@ -1,4 +1,4 @@
from src.services.repo import CRUD from src.infra.db import CRUD
import abc import abc
from datetime import datetime from datetime import datetime
from src.services.publications import ( from src.services.publications import (

View file

@ -1,4 +1,4 @@
from src.services.repo import CRUD from src.infra.db import CRUD
import abc import abc
from src.config.email import EmailAddress from src.config.email import EmailAddress