Skip to content

Commit 6046a5d

Browse files
committed
Remove need for decopule dep
1 parent e0b413e commit 6046a5d

File tree

13 files changed

+109
-48
lines changed

13 files changed

+109
-48
lines changed

api/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
22

3-
from api import constants
3+
from api import settings
44

55
format_string = "[%(asctime)s] [%(process)d] [%(levelname)s] %(name)s - %(message)s"
66
date_format_string = "%Y-%m-%d %H:%M:%S %z"
77

88
logging.basicConfig(
9-
format=format_string, datefmt=date_format_string, level=getattr(logging, constants.Config.LOG_LEVEL.upper())
9+
format=format_string, datefmt=date_format_string, level=getattr(logging, settings.Server.LOG_LEVEL.upper())
1010
)
1111

1212
logging.getLogger().info("Logging initialization complete")

api/constants.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

api/database.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
from fastapi import Depends
44
from sqlalchemy import BigInteger, Boolean, Column, Enum, ForeignKey, Index, Integer, PrimaryKeyConstraint, Text, text
5-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
5+
from sqlalchemy.ext.asyncio import AsyncSession
66
from sqlalchemy.ext.declarative import declarative_base
7-
from sqlalchemy.orm import relationship, sessionmaker
7+
from sqlalchemy.orm import relationship
88

9-
from api.constants import Config
109
from api.dependencies import get_db_session
1110

12-
engine = create_async_engine(Config.DATABASE_URL)
1311
Base = declarative_base()
1412

15-
Session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
1613
DBSession = Annotated[AsyncSession, Depends(get_db_session)]
1714

1815

api/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from fastapi import FastAPI
22
from starlette.middleware.authentication import AuthenticationMiddleware
33

4-
from api.constants import Config
54
from api.middleware import TokenAuthentication, on_auth_error
65
from api.routers import codejams, infractions, teams, users, winners
6+
from api.settings import Server
77

88
app = FastAPI(redoc_url="/", docs_url="/swagger")
99

1010
app.add_middleware(
1111
AuthenticationMiddleware,
12-
backend=TokenAuthentication(token=Config.TOKEN),
12+
backend=TokenAuthentication(token=Server.API_TOKEN.get_secret_value()),
1313
on_error=on_auth_error,
1414
)
1515

api/middleware.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from starlette.requests import Request
44
from starlette.responses import JSONResponse
55

6-
from api.constants import Config
6+
from api.settings import Server
77

88
NO_AUTHORIZATION_HEADER = "no `Authorization` header in request."
99
INVALID_CREDENTIALS = "invalid credentials."
@@ -17,7 +17,7 @@ def __init__(self, token: str) -> None:
1717

1818
async def authenticate(self, request: Request) -> tuple[AuthCredentials, SimpleUser]:
1919
"""Authenticate the request based on the Authorization header."""
20-
if Config.DEBUG:
20+
if Server.DEBUG:
2121
credentials = AuthCredentials(scopes=["debug"])
2222
user = SimpleUser(username="api_client")
2323
return credentials, user

api/settings.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pydantic
2+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
3+
4+
from api.utils.settings import CJMSBaseSettings
5+
6+
7+
class _ConnectionURLs(CJMSBaseSettings):
8+
"""URLs for connecting to other internal services."""
9+
10+
DATABASE_URL: pydantic.SecretStr
11+
SENTRY_DSN: pydantic.SecretStr | None
12+
13+
14+
ConnectionURLs = _ConnectionURLs()
15+
16+
17+
class Connections:
18+
"""How to connect to other, internal services."""
19+
20+
DB_ENGINE = create_async_engine(ConnectionURLs.DATABASE_URL.get_secret_value())
21+
DB_SESSION = async_sessionmaker(DB_ENGINE)
22+
23+
24+
class _Server(CJMSBaseSettings):
25+
"""Basic configuration for the Code Jam Management System."""
26+
27+
LOG_LEVEL: str = "INFO"
28+
DEBUG: bool = False
29+
API_TOKEN: pydantic.SecretStr
30+
31+
32+
Server = _Server(API_TOKEN="badbot13m0n8f570f942013fc818f234916ca531")

api/utils/settings.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Utilities for working with the project's constants."""
2+
import logging
3+
import typing
4+
from collections.abc import Sequence
5+
6+
import pydantic
7+
from pydantic.error_wrappers import ErrorWrapper
8+
9+
logger = logging.getLogger("cjms.settings")
10+
11+
# This is available in pydantic as pydantic.error_wrappers.ErrorList
12+
# but is typehinted as a Sequence[any], due to being a recursive type.
13+
# This makes it harder to handle the types.
14+
# For our purposes, a fully accurate representation is not necessary.
15+
_PYDANTIC_ERROR_TYPE = Sequence[ErrorWrapper | Sequence[ErrorWrapper]]
16+
17+
18+
class CJMSBaseSettings(pydantic.BaseSettings):
19+
"""Base class for settings with .env support and nicer error messages."""
20+
21+
@staticmethod
22+
def __log_missing_errors(base_error: pydantic.ValidationError, errors: _PYDANTIC_ERROR_TYPE) -> bool:
23+
"""
24+
Log out a nice representation for missing environment variables.
25+
26+
Returns false if none of the errors were caused by missing variables.
27+
"""
28+
found_relevant_errors = False
29+
for error in errors:
30+
if isinstance(error, Sequence):
31+
found_relevant_errors = (
32+
CJMSBaseSettings.__log_missing_errors(base_error, error) or found_relevant_errors
33+
)
34+
elif isinstance(error.exc, pydantic.MissingError):
35+
logger.error(f"Missing environment variable {base_error.args[1].__name__}.{error.loc_tuple()[0]}")
36+
found_relevant_errors = True
37+
38+
return found_relevant_errors
39+
40+
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
41+
"""Try to instantiate the class, and print a nicer message for unset variables."""
42+
try:
43+
super().__init__(*args, **kwargs)
44+
except pydantic.ValidationError as error:
45+
if CJMSBaseSettings.__log_missing_errors(error, error.raw_errors):
46+
exit(1)
47+
else:
48+
# The validation error is not due to an unset environment variable, propagate the error as normal
49+
raise error from None
50+
51+
class Config:
52+
"""Enable env files."""
53+
54+
frozen = True
55+
56+
env_file = ".env"
57+
env_file_encoding = "utf-8"

main-requirements.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,6 @@ pydantic==1.10.6 ; python_full_version >= "3.11.0" and python_full_version < "3.
253253
python-dateutil==2.8.2 ; python_full_version >= "3.11.0" and python_full_version < "3.12.0" \
254254
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
255255
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
256-
python-decouple==3.8 ; python_full_version >= "3.11.0" and python_full_version < "3.12.0" \
257-
--hash=sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f \
258-
--hash=sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66
259256
python-dotenv==1.0.0 ; python_full_version >= "3.11.0" and python_full_version < "3.12.0" \
260257
--hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \
261258
--hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a

migrations/env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
# This is a required step by Alembic to properly generate migrations
1010
from api import database
11-
from api.constants import Config
11+
from api.settings import ConnectionURLs
1212

1313
target_metadata = database.Base.metadata
1414

@@ -19,7 +19,7 @@
1919
# Interpret the config file for Python logging.
2020
# This line sets up loggers basically.
2121
fileConfig(config.config_file_name)
22-
config.set_main_option("sqlalchemy.url", Config.DATABASE_URL)
22+
config.set_main_option("sqlalchemy.url", ConnectionURLs.DATABASE_URL.get_secret_value())
2323

2424

2525
def run_migrations_offline() -> None:

poetry.lock

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)