Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.11.0
current_version = 1.12.0
commit = False
tag = False

Expand Down
270 changes: 203 additions & 67 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rasenmaeher_api"
version = "1.11.0"
version = "1.12.0"
description = "python-rasenmaeher-api"
authors = [
"Aciid <703382+Aciid@users.noreply.github.com>",
Expand Down Expand Up @@ -79,16 +79,14 @@ python = "^3.11"
libadvian = "^1.6"
click = "^8.0"
fastapi = ">=0.89,<1.0" # caret behaviour on 0.x is to lock to 0.x.*
# FIXME: Migrate to v2, see https://docs.pydantic.dev/2.3/migration/#basesettings-has-moved-to-pydantic-settings
pydantic= ">=1.10,<2.0"
pydantic-collections = ">=0.5,<1.0" # caret behaviour on 0.x is to lock to 0.x.*
pydantic = ">=2.0,<3.0"
pydantic-settings = ">=2.0,<3.0"
requests = "^2.31"
multikeyjwt = "^1.0"
uvicorn = {version = "^0.20", extras = ["standard"]}
gunicorn = "^20.1"
pyopenssl = "^23.1"
# Can't update to 2.0 before pydantic migration is done
libpvarki = { version="^1.9", source="nexuslocal"}
libpvarki = { version="^2.0", source="nexuslocal"}
openapi-readme = "^0.2"
python-multipart = ">=0.0.21,<1.0.0"
aiohttp = ">=3.11.10,<4.0"
Expand Down
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""python-rasenmaeher-api"""

__version__ = "1.11.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly
__version__ = "1.12.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
LOGGER = logging.getLogger(__name__)


class ORMBaseModel(SQLModel, table=False): # type: ignore[call-arg]
class ORMBaseModel(SQLModel, table=False):
"""Baseclass with common fields"""

__table_args__ = {"schema": "raesenmaeher"}
Expand Down
4 changes: 2 additions & 2 deletions src/rasenmaeher_api/db/enrollments.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def generate_code() -> str:
return code


class EnrollmentPool(ORMBaseModel, table=True): # type: ignore[call-arg]
class EnrollmentPool(ORMBaseModel, table=True):
"""Enrollment pools aka links, pk is UUID and comes from basemodel"""

__tablename__ = "enrollmentpools"
Expand Down Expand Up @@ -150,7 +150,7 @@ class EnrollmentState(enum.IntEnum):
REJECTED = 2


class Enrollment(ORMBaseModel, table=True): # type: ignore[call-arg]
class Enrollment(ORMBaseModel, table=True):
"""Enrollments, pk is UUID and comes from basemodel"""

__tablename__ = "enrollments"
Expand Down
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/db/logincodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
CODE_MAX_ATTEMPTS = 100


class LoginCode(ORMBaseModel, table=True): # type: ignore[call-arg]
class LoginCode(ORMBaseModel, table=True):
"""Track the login codes that can be exchanged for session JWTs"""

__tablename__ = "logincodes"
Expand Down
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/db/nonces.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
LOGGER = logging.getLogger(__name__)


class SeenToken(ORMBaseModel, table=True): # type: ignore[call-arg]
class SeenToken(ORMBaseModel, table=True):
"""Store tokens we should see used only once"""

__tablename__ = "seentokens"
Expand Down
6 changes: 3 additions & 3 deletions src/rasenmaeher_api/db/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
LOGGER = logging.getLogger(__name__)


class Person(ORMBaseModel, table=True): # type: ignore[call-arg] # pylint: disable=too-many-public-methods
class Person(ORMBaseModel, table=True): # pylint: disable=too-many-public-methods
"""People, pk is UUID and comes from basemodel

NOTE: at some point we want to stop keeping track of people in our own db
Expand Down Expand Up @@ -380,7 +380,7 @@ async def roles(self) -> AsyncGenerator[str, None]:
yield result.role


class Role(SQLModel, table=True): # type: ignore[call-arg]
class Role(SQLModel, table=True):
"""Give a person a role"""

__tablename__ = "roles"
Expand All @@ -399,7 +399,7 @@ async def post_user_crud(userinfo: UserCRUDRequest, endpoint_suffix: str) -> Non
"""Wrapper to be more DRY in the basic CRUD things"""
endpoint = f"api/v1/users/{endpoint_suffix}"
# We can't do anything about any issues with the responses so don't collect them
await post_to_all_products(endpoint, userinfo.dict(), OperationResultResponse, collect_responses=False)
await post_to_all_products(endpoint, userinfo.model_dump(), OperationResultResponse, collect_responses=False)


async def user_created(person: Person) -> None:
Expand Down
9 changes: 3 additions & 6 deletions src/rasenmaeher_api/kchelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import json

from libpvarki.schemas.product import UserCRUDRequest
from pydantic import BaseModel, Extra, Field
from pydantic import BaseModel, Field, ConfigDict
from keycloak.keycloak_admin import KeycloakAdmin
from keycloak.exceptions import KeycloakError

Expand All @@ -20,16 +20,13 @@
class KCUserData(BaseModel):
"""Represent KC user object manipulations"""

model_config = ConfigDict(extra="forbid")

productdata: UserCRUDRequest = Field(description="Data that would be sent to productAPIs")
roles: Set[str] = Field(default_factory=set, description="Local roles")
kc_id: Optional[str] = Field(description="KC id (uuid)", default=None)
kc_data: Dict[str, Any] = Field(description="Full KC data", default_factory=dict)

class Config: # pylint: disable=too-few-public-methods
"""Example values for schema"""

extra = Extra.forbid


# PONDER: Maybe switch to https://python-keycloak.readthedocs.io/en/latest/modules/async.html
@dataclass
Expand Down
42 changes: 22 additions & 20 deletions src/rasenmaeher_api/rmsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
import json

from pydantic import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -37,12 +37,11 @@ class RMSettings(BaseSettings): # pylint: disable=too-few-public-methods
with environment variables.
"""

class Config: # pylint: disable=too-few-public-methods
"""Configuration of settings."""

env_file = ".env"
env_prefix = "RM_"
env_file_encoding = "utf-8"
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="RM_",
env_file_encoding="utf-8",
)

host: str = "127.0.0.1"
port: int = 8000
Expand All @@ -54,18 +53,21 @@ class Config: # pylint: disable=too-few-public-methods
# Current environment
environment: str = "dev"

# Set log_level (str) and log_level_int (int) for later use
# if log_level is not set, then log level will be DEBUG
# Set log_level - default is DEBUG
log_level: LogLevel = LogLevel.DEBUG
log_level_int: int = logging.DEBUG
if log_level == "INFO":
log_level_int = logging.INFO
elif log_level == "WARNING":
log_level_int = logging.WARNING
elif log_level == "ERROR":
log_level_int = logging.ERROR
elif log_level == "FATAL":
log_level_int = logging.FATAL

@property
def log_level_int(self) -> int:
"""Return the integer log level based on the LogLevel enum"""
level_map = {
LogLevel.NOTSET: logging.NOTSET,
LogLevel.DEBUG: logging.DEBUG,
LogLevel.INFO: logging.INFO,
LogLevel.WARNING: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.FATAL: logging.FATAL,
}
return level_map.get(self.log_level, logging.DEBUG)

# Manifest file from kraftwerk
integration_api_port: int = 4625
Expand Down Expand Up @@ -95,12 +97,12 @@ class Config: # pylint: disable=too-few-public-methods
ocsprest_port: str = "8887"
cfssl_timeout: float = 2.5

persistent_data_dir = "/data/persistent"
persistent_data_dir: str = "/data/persistent"

# mtls
mtls_client_cert_path: Optional[str] = None
mtls_client_key_path: Optional[str] = None
mtls_client_cert_cn = "rasenmaeher"
mtls_client_cert_cn: str = "rasenmaeher"

# LDAP configuration
ldap_conn_string: Optional[str] = None
Expand Down
41 changes: 13 additions & 28 deletions src/rasenmaeher_api/web/api/descriptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""product descriptions endpoints"""

from typing import Literal, Optional, cast
from typing import Literal, Optional, List, cast
import logging

from fastapi import APIRouter, Depends
from pydantic import BaseModel, Extra, Field
from pydantic_collections import BaseCollectionModel
from pydantic import BaseModel, Field, ConfigDict, RootModel
from libpvarki.middleware import MTLSHeader
from rasenmaeher_api.web.api.middleware.user import ValidUser
from ...productapihelpers import get_from_all_products, get_from_product
Expand All @@ -23,17 +22,14 @@
class ProductDescription(BaseModel):
"""Description of a product"""

model_config = ConfigDict(extra="forbid")

shortname: str = Field(description="Short name for the product, used as slug/key in dicts and urls")
title: str = Field(description="Fancy name for the product")
icon: Optional[str] = Field(description="URL for icon")
description: str = Field(description="Short-ish description of the product")
language: str = Field(description="Language of this response")

class Config: # pylint: disable=too-few-public-methods
"""Pydantic configs"""

extra = Extra.forbid


class ProductComponent(BaseModel):
"""Product component info"""
Expand All @@ -45,6 +41,8 @@ class ProductComponent(BaseModel):
class ProductDescriptionExtended(BaseModel):
"""Description of a product"""

model_config = ConfigDict(extra="forbid")

shortname: str = Field(description="Short name for the product, used as slug/key in dicts and urls")
title: str = Field(description="Fancy name for the product")
icon: Optional[str] = Field(description="URL for icon")
Expand All @@ -53,29 +51,16 @@ class ProductDescriptionExtended(BaseModel):
docs: Optional[str] = Field(description="Link to documentation")
component: ProductComponent = Field(description="Component type and ref")

class Config: # pylint: disable=too-few-public-methods
"""Pydantic configs"""

extra = Extra.forbid


class ProductDescriptionList(BaseCollectionModel[ProductDescription]): # type: ignore[misc] # pylint: disable=too-few-public-methods
class ProductDescriptionList(RootModel[List[ProductDescription]]): # pylint: disable=too-few-public-methods
"""List of product descriptions"""

class Config: # pylint: disable=too-few-public-methods
"""Pydantic configs"""

extra = Extra.forbid


class ProductDescriptionExtendedList(BaseCollectionModel[ProductDescriptionExtended]): # type: ignore[misc] # pylint: disable=too-few-public-methods
class ProductDescriptionExtendedList(
RootModel[List[ProductDescriptionExtended]]
): # pylint: disable=too-few-public-methods
"""List of product descriptions"""

class Config: # pylint: disable=too-few-public-methods
"""Pydantic configs"""

extra = Extra.forbid


@router.get(
"/{language}",
Expand All @@ -86,7 +71,7 @@ async def list_product_descriptions(language: str) -> ProductDescriptionList:
responses = await get_from_all_products(f"api/v1/description/{language}", ProductDescription)
if responses is None:
raise ValueError("Everything is broken")
return ProductDescriptionList([res for res in responses.values() if res])
return ProductDescriptionList([cast(ProductDescription, res) for res in responses.values() if res])


@router.get(
Expand All @@ -112,7 +97,7 @@ async def list_product_descriptions_extended(language: str) -> ProductDescriptio
responses = await get_from_all_products(f"api/v2/description/{language}", ProductDescriptionExtended)
if responses is None:
raise ValueError("Everything is broken")
return ProductDescriptionExtendedList([res for res in responses.values() if res])
return ProductDescriptionExtendedList([cast(ProductDescriptionExtended, res) for res in responses.values() if res])


@router_v2.get(
Expand Down Expand Up @@ -140,7 +125,7 @@ async def list_admin_product_descriptions_extended(language: str) -> ProductDesc
responses = await get_from_all_products(f"api/v2/admin/description/{language}", ProductDescriptionExtended)
if responses is None:
raise ValueError("Everything is broken")
return ProductDescriptionExtendedList([res for res in responses.values() if res])
return ProductDescriptionExtendedList([cast(ProductDescriptionExtended, res) for res in responses.values() if res])


@router_v2_admin.get(
Expand Down
Loading