Skip to content

Commit

Permalink
feat: Pydantic v2
Browse files Browse the repository at this point in the history
  • Loading branch information
puehringer committed Dec 5, 2024
1 parent 76cb796 commit 3650f43
Show file tree
Hide file tree
Showing 16 changed files with 85 additions and 96 deletions.
64 changes: 20 additions & 44 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,50 +1,26 @@
# a2wsgi==1.6.0 # This WSIGMiddleware is not compatible with starlette_context
alembic==1.13.2
cachetools==5.3.3
fastapi==0.112.2
alembic==1.14.0
cachetools==5.5.0
fastapi[all]==0.115.6
json-cfg==0.4.2
openpyxl==3.1.5
opentelemetry-api==1.27.0
opentelemetry-exporter-otlp==1.27.0
opentelemetry-exporter-prometheus==0.48b0
opentelemetry-instrumentation-fastapi==0.48b0
opentelemetry-instrumentation-httpx==0.48b0
opentelemetry-instrumentation-logging==0.48b0
opentelemetry-instrumentation-requests==0.48b0
opentelemetry-instrumentation-sqlalchemy==0.48b0
opentelemetry-instrumentation-system-metrics==0.48b0
opentelemetry-sdk==1.27.0
psycopg==3.2.1
psycopg2==2.9.9
pydantic==1.10.17
pyjwt[crypto]==2.9.0
pytest-postgresql==6.0.1
opentelemetry-api==1.28.2
opentelemetry-exporter-otlp==1.28.2
opentelemetry-exporter-prometheus==0.49b2
opentelemetry-instrumentation-fastapi==0.49b2
opentelemetry-instrumentation-httpx==0.49b2
opentelemetry-instrumentation-logging==0.49b2
opentelemetry-instrumentation-requests==0.49b2
opentelemetry-instrumentation-sqlalchemy==0.49b2
opentelemetry-instrumentation-system-metrics==0.49b2
opentelemetry-sdk==1.28.2
psycopg==3.2.3
psycopg2==2.9.10
pydantic==2.10.3
pyjwt[crypto]==2.10.1
pytest-postgresql==6.1.1
python-dateutil==2.9.0.post0
requests==2.32.3
sentry-sdk~=2.13.0
SQLAlchemy>=1.4.40,<=1.4.53
sentry-sdk~=2.19.1
SQLAlchemy>=1.4.50,<2.0.0
starlette-context==0.3.6
# Extras from fastapi[all], which we can't install because it requires pydantic v2: https://github.com/tiangolo/fastapi/blob/master/pyproject.toml#L79-L103
fastapi-cli[standard]>=0.0.5
# For the test client
httpx>=0.23.0
# For templates
jinja2>=2.11.2
# For forms and file uploads
python-multipart>=0.0.7
# For Starlette's SessionMiddleware, not commonly used with FastAPI
itsdangerous>=1.1.0
# For Starlette's schema generation, would not be used with FastAPI
pyyaml>=5.3.1
# For UJSONResponse
ujson>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0
# For ORJSONResponse
orjson>=3.2.1
# To validate email fields
email-validator>=2.0.0
# Uvicorn with uvloop
uvicorn[standard]>=0.30.6
# Disabled as we are still on Pydantic 1: Settings management
# pydantic-settings>=2.0.0
# Disabled as we are still on Pydantic 1: Extra Pydantic data types
# pydantic-extra-types>=2.0.0
8 changes: 4 additions & 4 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
black~=24.4.2
pyright==1.1.366
black~=24.10.0
pyright==1.1.390
pytest-runner~=6.0.1
pytest~=8.2.2
ruff==0.4.10
pytest~=8.3.4
ruff==0.8.2
1 change: 0 additions & 1 deletion src/app/login/UserStoreUIMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ export function AutoLoginForm({ setError, store }: IUserStoreRenderProps) {
e.stopPropagation();
login();
}}
size="sm"
>
here
</Anchor>{' '}
Expand Down
4 changes: 2 additions & 2 deletions visyn_core/dbview.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ def is_valid_replacement(self, key, value):
v = self.valid_replacements[key]
if isinstance(v, list):
return value in v
if v == int:
if v is int:
try:
int(value) # try to cast value to int
return True # successful type cast
except ValueError:
return False
if v == float:
if v is float:
try:
float(value) # try to cast value to float
return True # successful type cast
Expand Down
2 changes: 1 addition & 1 deletion visyn_core/id_mapping/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,6 @@ def create_id_mapping_manager() -> MappingManager:
# Load mapping providers
providers = []
for plugin in manager.registry.list("mapping_provider"):
providers = providers + list(plugin.load().factory())
providers = providers + list(plugin.load().factory()) # type: ignore
_log.info(f"Initializing MappingManager with {len(providers)} provider(s)")
return MappingManager(providers)
2 changes: 1 addition & 1 deletion visyn_core/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.v1 import BaseModel


class RegHelper:
Expand Down
2 changes: 1 addition & 1 deletion visyn_core/plugin/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from functools import cached_property, lru_cache
from importlib.metadata import EntryPoint, entry_points

from pydantic import BaseModel
from pydantic.v1 import BaseModel

from .. import manager
from .model import AVisynPlugin, RegHelper
Expand Down
3 changes: 2 additions & 1 deletion visyn_core/plugin/registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

from fastapi import FastAPI

Expand Down Expand Up @@ -30,7 +31,7 @@ def __call__(self, *args, **kwargs):
self._cache = v
return v

def factory(self, *args, **kwargs):
def factory(self, *args, **kwargs) -> Any:
return self(*args, **kwargs)


Expand Down
2 changes: 1 addition & 1 deletion visyn_core/rdkit/img_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
def draw_smiles(
structure: SmilesMolecule, substructure: SmilesMolecule | None = None, align: SmilesMolecule | None = None, size: int = 300
):
return draw(structure.mol, size=size, substructure=aligned(structure.mol, align and align.mol) or substructure and substructure.mol)
return draw(structure.mol, size=size, substructure=aligned(structure.mol, align and align.mol) or (substructure and substructure.mol))


@app.post("/")
Expand Down
39 changes: 26 additions & 13 deletions visyn_core/rdkit/models.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
from typing import ClassVar
from typing import Any, ClassVar

from pydantic import BaseModel
from pydantic import BaseModel, GetCoreSchemaHandler
from pydantic.annotated_handlers import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
from rdkit.Chem import Mol, MolFromSmarts, MolFromSmiles # type: ignore
from starlette.responses import Response


class SmilesMolecule(str):
"""We can't directly extend mol, as this would break swagger"""

parsers: ClassVar = [MolFromSmiles]
_mol: Mol

@property
def mol(self):
return self._mol
@classmethod
def __get_pydantic_core_schema__(
cls,
_source: type[Any],
_handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(cls._validate, core_schema.str_schema())

@classmethod
def __get_validators__(cls):
yield cls.validate
def __get_pydantic_json_schema__(cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
field_schema = handler(core_schema)
field_schema.update(type="string", format="email")
return field_schema

@classmethod
def validate(cls, value: str | None) -> "SmilesMolecule":
def _validate(cls, input_value: str, /) -> "SmilesMolecule":
for parser in cls.parsers:
mol = parser(value)
mol = parser(input_value)
if mol:
sm = SmilesMolecule(value)
sm = SmilesMolecule(input_value)
sm._mol = mol
return sm
else:
raise ValueError("Unparsable smiles")

parsers: ClassVar = [MolFromSmiles]
_mol: Mol

@property
def mol(self):
return self._mol


class SmilesSmartsMolecule(SmilesMolecule):
"""Try parings smiles first, then smarts"""
Expand Down
2 changes: 1 addition & 1 deletion visyn_core/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def _delegate_stores_until_not_none(self, store_method_name: str, *args):
method = getattr(store, store_method_name, None)
if callable(method):
try:
value = method(*args)
value: User | None = method(*args) # type: ignore
except Exception:
_log.exception(f"Error executing {store_method_name} in {store}")
else:
Expand Down
2 changes: 1 addition & 1 deletion visyn_core/server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def _init_legacy_app(app: Flask):
if manager.settings.visyn_core:
app.config["SECRET_KEY"] = manager.settings.secret_key

@app.errorhandler(FlaskHTTPException)
@app.errorhandler(FlaskHTTPException) # type: ignore
@app.errorhandler(Exception) # type: ignore
async def handle_exception(e):
"""Handles Flask exceptions by returning the same JSON response as FastAPI#HTTPException would."""
Expand Down
4 changes: 2 additions & 2 deletions visyn_core/server/visyn_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from pydantic import create_model
from pydantic.utils import deep_update
from pydantic.v1 import create_model
from pydantic.v1.utils import deep_update
from starlette_context.middleware import RawContextMiddleware

from ..settings.constants import default_logging_dict
Expand Down
26 changes: 13 additions & 13 deletions visyn_core/settings/model.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import contextlib
from typing import Any, Literal

from pydantic import AnyHttpUrl, BaseConfig, BaseModel, BaseSettings, Extra, Field
from pydantic.env_settings import EnvSettingsSource, SettingsSourceCallable
from pydantic.v1 import AnyHttpUrl, BaseConfig, BaseModel, BaseSettings, Extra, Field
from pydantic.v1.env_settings import EnvSettingsSource, SettingsSourceCallable

from .constants import default_logging_dict

Expand All @@ -12,13 +12,13 @@ class DBMigrationSettings(BaseModel):


class DisableSettings(BaseModel):
plugins: list[str] = []
extensions: list[str] = []
plugins: list[str] = [] # NOQA RUF012
extensions: list[str] = [] # NOQA RUF012


class DummyStoreSettings(BaseModel):
enable: bool = False
users: list[dict[str, Any]] = [
users: list[dict[str, Any]] = [ # NOQA RUF012
{
"name": "admin",
"salt": "dcf46ce914154a44b1557eba91c1f50d",
Expand All @@ -44,7 +44,7 @@ class AlbSecurityStoreSettings(BaseModel):
enable: bool = False
cookie_name: str | None = None
signout_url: str | None = None
email_token_field: str | list[str] = ["email"]
email_token_field: str | list[str] = ["email"] # NOQA RUF012
"""
Field in the JWT token that contains the email address of the user.
"""
Expand All @@ -65,7 +65,7 @@ class AlbSecurityStoreSettings(BaseModel):
"""
The region of the ALB to fetch the public key from.
"""
decode_algorithms: list[str] = ["ES256"]
decode_algorithms: list[str] = ["ES256"] # NOQA RUF012
"""
The algorithm used to sign the JWT token. See https://pyjwt.readthedocs.io/en/stable/algorithms.html for details.
"""
Expand All @@ -76,13 +76,13 @@ class OAuth2SecurityStoreSettings(BaseModel):
cookie_name: str | None = None
signout_url: str | None = None
access_token_header_name: str = "X-Forwarded-Access-Token"
email_token_field: str | list[str] = ["email"]
email_token_field: str | list[str] = ["email"] # NOQA RUF012


class NoSecurityStoreSettings(BaseModel):
enable: bool = False
user: str = "admin"
roles: list[str] = []
roles: list[str] = [] # NOQA RUF012


class SecurityStoreSettings(BaseModel):
Expand All @@ -108,7 +108,7 @@ class BaseExporterTelemetrySettings(BaseModel):
endpoint: AnyHttpUrl # could be "http://localhost:4318"
headers: dict[str, str] | None = None
timeout: int | None = None
kwargs: dict[str, Any] = {}
kwargs: dict[str, Any] = {} # NOQA RUF012


class MetricsExporterTelemetrySettings(BaseExporterTelemetrySettings):
Expand Down Expand Up @@ -163,7 +163,7 @@ class SentrySettings(BaseModel):
"""
Proxy Sentry envelopes to this URL. Used if an internal Sentry server is used, otherwise the original DSN is used.
"""
server_init_options: dict[str, Any] = {}
server_init_options: dict[str, Any] = {} # NOQA RUF012
"""
Options to be passed to the Sentry SDK during initialization.
"""
Expand Down Expand Up @@ -198,7 +198,7 @@ class VisynCoreSettings(BaseModel):
"""

disable: DisableSettings = DisableSettings()
enabled_plugins: list[str] = []
enabled_plugins: list[str] = [] # NOQA RUF012

# TODO: Proper typing. This is 1:1 passed to the logging.config.dictConfig(...).
logging: dict = Field(default_logging_dict)
Expand Down Expand Up @@ -231,7 +231,7 @@ class GlobalSettings(BaseSettings):
secret_key: str = "VERY_SECRET_STUFF_T0IB84wlQrdMH8RVT28w"

# JWT options mostly inspired by flask-jwt-extended: https://flask-jwt-extended.readthedocs.io/en/stable/options/#general-options
jwt_token_location: list[str] = ["headers", "cookies"]
jwt_token_location: list[str] = ["headers", "cookies"] # NOQA RUF012
jwt_expire_in_seconds: int = 24 * 60 * 60
jwt_refresh_if_expiring_in_seconds: int = 30 * 60
jwt_algorithm: str = "HS256"
Expand Down
12 changes: 6 additions & 6 deletions visyn_core/telemetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def filter(self, record: logging.LogRecord) -> bool:
exporter=OTLPMetricExporter(
# If we are using the global exporter settings, append the metrics path
endpoint=(
_append_metrics_path(exporter_settings.endpoint)
_append_metrics_path(str(exporter_settings.endpoint))
if exporter_settings == global_exporter_settings
else exporter_settings.endpoint
else str(exporter_settings.endpoint)
),
headers=exporter_settings.headers,
timeout=exporter_settings.timeout,
Expand Down Expand Up @@ -123,9 +123,9 @@ def deprecated_prometheus_metrics():
OTLPSpanExporter(
# If we are using the global exporter settings, append the traces path
endpoint=(
_append_trace_path(exporter_settings.endpoint)
_append_trace_path(str(exporter_settings.endpoint))
if exporter_settings == global_exporter_settings
else exporter_settings.endpoint
else str(exporter_settings.endpoint)
),
headers=exporter_settings.headers,
timeout=exporter_settings.timeout,
Expand Down Expand Up @@ -168,9 +168,9 @@ def shutdown_tracer_event():
OTLPLogExporter(
# If we are using the global exporter settings, append the logs path
endpoint=(
_append_logs_path(exporter_settings.endpoint)
_append_logs_path(str(exporter_settings.endpoint))
if exporter_settings == global_exporter_settings
else exporter_settings.endpoint
else str(exporter_settings.endpoint)
),
headers=exporter_settings.headers,
timeout=exporter_settings.timeout,
Expand Down
Loading

0 comments on commit 3650f43

Please sign in to comment.