Skip to content

Commit

Permalink
chore: bump schemathesis
Browse files Browse the repository at this point in the history
  • Loading branch information
Panaetius committed Jan 13, 2025
1 parent fce24d6 commit 4f35b42
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 51 deletions.
13 changes: 12 additions & 1 deletion bases/renku_data_services/data_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from sanic import Sanic
from ulid import ULID

from renku_data_services import errors
from renku_data_services.app_config import Config
from renku_data_services.base_api.error_handler import CustomErrorHandler
from renku_data_services.base_api.misc import MiscBP
from renku_data_services.base_models.core import Slug
from renku_data_services.connected_services.blueprints import OAuth2ClientsBP, OAuth2ConnectionsBP
from renku_data_services.crc import apispec
from renku_data_services.crc.blueprints import (
Expand All @@ -27,10 +29,19 @@
from renku_data_services.users.blueprints import KCUsersBP, UserPreferencesBP, UserSecretsBP


def str_to_slug(value: str) -> Slug:
"""Convert a str to Slug."""
try:
return Slug(value)
except errors.ValidationError:
raise ValueError("Couldn't parse slug")


def register_all_handlers(app: Sanic, config: Config) -> Sanic:
"""Register all handlers on the application."""
# WARNING: The regex is not actually used in most cases, instead the conversion function must raise a ValueError
app.router.register_pattern("ulid", ULID.from_str, r"^[0-7][0-9A-HJKMNP-TV-Z]{25}$")
app.router.register_pattern("renku_slug", str, r"^[a-zA-Z0-9][a-zA-Z0-9\-_.]*$")
app.router.register_pattern("renku_slug", str_to_slug, r"^[a-zA-Z0-9][a-zA-Z0-9-_.]*$")

url_prefix = "/api/data"
resource_pools = ResourcePoolsBP(
Expand Down
3 changes: 2 additions & 1 deletion components/renku_data_services/data_connectors/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from renku_data_services.base_api.etag import extract_if_none_match, if_match_required
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
from renku_data_services.base_api.pagination import PaginationRequest, paginate
from renku_data_services.base_models.core import Slug
from renku_data_services.base_models.validation import validate_and_dump, validated_json
from renku_data_services.data_connectors import apispec, models
from renku_data_services.data_connectors.core import (
Expand Down Expand Up @@ -122,7 +123,7 @@ async def _get_one_by_slug(
_: Request,
user: base_models.APIUser,
namespace: str,
slug: str,
slug: Slug,
etag: str | None,
validator: RCloneValidator,
) -> HTTPResponse:
Expand Down
13 changes: 7 additions & 6 deletions components/renku_data_services/data_connectors/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from renku_data_services.authz.authz import Authz, AuthzOperation, ResourceType
from renku_data_services.authz.models import CheckPermissionItem, Scope
from renku_data_services.base_api.pagination import PaginationRequest
from renku_data_services.base_models.core import Slug
from renku_data_services.data_connectors import apispec, models
from renku_data_services.data_connectors import orm as schemas
from renku_data_services.namespace import orm as ns_schemas
Expand Down Expand Up @@ -103,17 +104,17 @@ async def get_data_connectors_names_and_ids(
return [(dc.name, f"{dc.slug.namespace.slug}/{dc.slug.slug}") for dc in data_connectors_orms]

async def get_data_connector_by_slug(
self, user: base_models.APIUser, namespace: str, slug: str
self, user: base_models.APIUser, namespace: str, slug: Slug
) -> models.DataConnector:
"""Get one data connector from the database by slug."""
not_found_msg = (
f"Data connector with identifier '{namespace}/{slug}' does not exist or you do not have access to it."
f"Data connector with identifier '{namespace}/{slug.value}' does not exist or you do not have access to it."
)

async with self.session_maker() as session:
stmt = select(schemas.DataConnectorORM)
stmt = _filter_by_namespace_slug(stmt, namespace)
stmt = stmt.where(ns_schemas.EntitySlugORM.slug == slug.lower())
stmt = stmt.where(ns_schemas.EntitySlugORM.slug == slug.value.lower())
result = await session.scalars(stmt)
data_connector = result.one_or_none()

Expand All @@ -124,14 +125,14 @@ async def get_data_connector_by_slug(
.where(ns_schemas.NamespaceOldORM.latest_slug_id == ns_schemas.NamespaceORM.id)
.where(ns_schemas.EntitySlugORM.namespace_id == ns_schemas.NamespaceORM.id)
.where(schemas.DataConnectorORM.id == ns_schemas.EntitySlugORM.data_connector_id)
.where(schemas.DataConnectorORM.slug.has(ns_schemas.EntitySlugORM.slug == slug.lower()))
.where(schemas.DataConnectorORM.slug.has(ns_schemas.EntitySlugORM.slug == slug.value.lower()))
)
old_data_connector_stmt_current_ns_old_slug = (
select(schemas.DataConnectorORM.id)
.where(ns_schemas.NamespaceORM.slug == namespace.lower())
.where(ns_schemas.EntitySlugORM.namespace_id == ns_schemas.NamespaceORM.id)
.where(schemas.DataConnectorORM.id == ns_schemas.EntitySlugORM.data_connector_id)
.where(ns_schemas.EntitySlugOldORM.slug == slug.lower())
.where(ns_schemas.EntitySlugOldORM.slug == slug.value.lower())
.where(ns_schemas.EntitySlugOldORM.latest_slug_id == ns_schemas.EntitySlugORM.id)
)
old_data_connector_stmt_old_ns_old_slug = (
Expand All @@ -140,7 +141,7 @@ async def get_data_connector_by_slug(
.where(ns_schemas.NamespaceOldORM.latest_slug_id == ns_schemas.NamespaceORM.id)
.where(ns_schemas.EntitySlugORM.namespace_id == ns_schemas.NamespaceORM.id)
.where(schemas.DataConnectorORM.id == ns_schemas.EntitySlugORM.data_connector_id)
.where(ns_schemas.EntitySlugOldORM.slug == slug.lower())
.where(ns_schemas.EntitySlugOldORM.slug == slug.value.lower())
.where(ns_schemas.EntitySlugOldORM.latest_slug_id == ns_schemas.EntitySlugORM.id)
)
old_data_connector_stmt = old_data_connector_stmt_old_ns_current_slug.union(
Expand Down
15 changes: 8 additions & 7 deletions components/renku_data_services/namespace/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
from renku_data_services.base_api.pagination import PaginationRequest, paginate
from renku_data_services.base_models.core import Slug
from renku_data_services.base_models.validation import validate_and_dump, validated_json
from renku_data_services.errors import errors
from renku_data_services.namespace import apispec, models
Expand Down Expand Up @@ -62,7 +63,7 @@ def get_one(self) -> BlueprintFactoryResponse:
"""Get a specific group."""

@authenticate(self.authenticator)
async def _get_one(_: Request, user: base_models.APIUser, slug: str) -> JSONResponse:
async def _get_one(_: Request, user: base_models.APIUser, slug: Slug) -> JSONResponse:
result = await self.group_repo.get_group(user=user, slug=slug)
return validated_json(apispec.GroupResponse, result)

Expand All @@ -73,7 +74,7 @@ def delete(self) -> BlueprintFactoryResponse:

@authenticate(self.authenticator)
@only_authenticated
async def _delete(_: Request, user: base_models.APIUser, slug: str) -> HTTPResponse:
async def _delete(_: Request, user: base_models.APIUser, slug: Slug) -> HTTPResponse:
await self.group_repo.delete_group(user=user, slug=slug)
return HTTPResponse(status=204)

Expand All @@ -86,7 +87,7 @@ def patch(self) -> BlueprintFactoryResponse:
@only_authenticated
@validate(json=apispec.GroupPatchRequest)
async def _patch(
_: Request, user: base_models.APIUser, slug: str, body: apispec.GroupPatchRequest
_: Request, user: base_models.APIUser, slug: Slug, body: apispec.GroupPatchRequest
) -> JSONResponse:
group_patch = validate_group_patch(body)
res = await self.group_repo.update_group(user=user, slug=slug, patch=group_patch)
Expand All @@ -98,7 +99,7 @@ def get_all_members(self) -> BlueprintFactoryResponse:
"""List all group members."""

@authenticate(self.authenticator)
async def _get_all_members(_: Request, user: base_models.APIUser, slug: str) -> JSONResponse:
async def _get_all_members(_: Request, user: base_models.APIUser, slug: Slug) -> JSONResponse:
members = await self.group_repo.get_group_members(user, slug)
return validated_json(
apispec.GroupMemberResponseList,
Expand All @@ -123,7 +124,7 @@ def update_members(self) -> BlueprintFactoryResponse:
@only_authenticated
@validate_body_root_model(json=apispec.GroupMemberPatchRequestList)
async def _update_members(
_: Request, user: base_models.APIUser, slug: str, body: apispec.GroupMemberPatchRequestList
_: Request, user: base_models.APIUser, slug: Slug, body: apispec.GroupMemberPatchRequestList
) -> JSONResponse:
members = [UnsavedMember(Role.from_group_role(member.role), member.id) for member in body.root]
res = await self.group_repo.update_group_members(
Expand All @@ -150,7 +151,7 @@ def delete_member(self) -> BlueprintFactoryResponse:
@authenticate(self.authenticator)
@validate_path_user_id
@only_authenticated
async def _delete_member(_: Request, user: base_models.APIUser, slug: str, user_id: str) -> HTTPResponse:
async def _delete_member(_: Request, user: base_models.APIUser, slug: Slug, user_id: str) -> HTTPResponse:
await self.group_repo.delete_group_member(user=user, slug=slug, user_id_to_delete=user_id)
return HTTPResponse(status=204)

Expand All @@ -160,7 +161,7 @@ def get_permissions(self) -> BlueprintFactoryResponse:
"""Get the permissions of the current user on the group."""

@authenticate(self.authenticator)
async def _get_permissions(_: Request, user: base_models.APIUser, slug: str) -> JSONResponse:
async def _get_permissions(_: Request, user: base_models.APIUser, slug: Slug) -> JSONResponse:
permissions = await self.group_repo.get_group_permissions(user=user, slug=slug)
return validated_json(apispec.GroupPermissions, permissions)

Expand Down
35 changes: 18 additions & 17 deletions components/renku_data_services/namespace/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from renku_data_services.authz.authz import Authz, AuthzOperation, ResourceType
from renku_data_services.authz.models import CheckPermissionItem, Member, MembershipChange, Role, Scope, UnsavedMember
from renku_data_services.base_api.pagination import PaginationRequest
from renku_data_services.base_models.core import Slug
from renku_data_services.message_queue import events
from renku_data_services.message_queue.avro_models.io.renku.events.v2 import GroupAdded, GroupRemoved, GroupUpdated
from renku_data_services.message_queue.db import EventRepository
Expand Down Expand Up @@ -107,18 +108,18 @@ async def get_all_groups(self, requested_by: base_models.APIUser) -> AsyncGenera
yield group.dump()

async def _get_group(
self, session: AsyncSession, user: base_models.APIUser, slug: str, load_members: bool = False
self, session: AsyncSession, user: base_models.APIUser, slug: Slug, load_members: bool = False
) -> tuple[schemas.GroupORM, list[Member]]:
transaction = nullcontext() if session.in_transaction() else session.begin()
async with transaction:
stmt = select(schemas.GroupORM).where(
schemas.GroupORM.namespace.has(schemas.NamespaceORM.slug == slug.lower())
schemas.GroupORM.namespace.has(schemas.NamespaceORM.slug == slug.value.lower())
)
group = await session.scalar(stmt)
if not group:
stmt_old_ns = (
select(schemas.NamespaceOldORM)
.where(schemas.NamespaceOldORM.slug == slug.lower())
.where(schemas.NamespaceOldORM.slug == slug.value.lower())
.order_by(schemas.NamespaceOldORM.created_at.desc())
.limit(1)
.options(
Expand All @@ -137,15 +138,15 @@ async def _get_group(
members = await self.authz.members(user, ResourceType.group, group.id)
return group, members

async def get_group(self, user: base_models.APIUser, slug: str) -> models.Group:
async def get_group(self, user: base_models.APIUser, slug: Slug) -> models.Group:
"""Get a group from the DB."""
async with self.session_maker() as session:
group_orm, _ = await self._get_group(session, user, slug)
return group_orm.dump()

@with_db_transaction
async def get_group_members(
self, user: base_models.APIUser, slug: str, *, session: AsyncSession | None = None
self, user: base_models.APIUser, slug: Slug, *, session: AsyncSession | None = None
) -> list[models.GroupMemberDetails]:
"""Get all the users that are direct members of a group."""
if not session:
Expand Down Expand Up @@ -173,22 +174,22 @@ async def get_group_members(
@with_db_transaction
@dispatch_message(GroupUpdated)
async def update_group(
self, user: base_models.APIUser, slug: str, patch: models.GroupPatch, *, session: AsyncSession | None = None
self, user: base_models.APIUser, slug: Slug, patch: models.GroupPatch, *, session: AsyncSession | None = None
) -> models.Group:
"""Update a group in the DB."""
if not session:
raise errors.ProgrammingError(message="A database session is required")
group, _ = await self._get_group(session, user, slug)
if group.namespace.slug != slug.lower():
if group.namespace.slug != slug.value.lower():
raise errors.UpdatingWithStaleContentError(
message=f"You cannot update a group by using its old slug {slug}.",
message=f"You cannot update a group by using its old slug {slug.value}.",
detail=f"The latest slug is {group.namespace.slug}, please use this for updates.",
)

authorized = await self.authz.has_permission(user, ResourceType.group, group.id, Scope.DELETE)
if not authorized:
raise errors.MissingResourceError(
message=f"Group with slug '{slug}' does not exist or you do not have access to it."
message=f"Group with slug '{slug.value}' does not exist or you do not have access to it."
)

new_slug_str = patch.slug.lower() if patch.slug is not None else None
Expand All @@ -215,7 +216,7 @@ async def update_group(
async def update_group_members(
self,
user: base_models.APIUser,
slug: str,
slug: Slug,
members: list[UnsavedMember],
*,
session: AsyncSession | None = None,
Expand All @@ -224,9 +225,9 @@ async def update_group_members(
if not session:
raise errors.ProgrammingError(message="A database session is required")
group, _ = await self._get_group(session, user, slug, load_members=True)
if group.namespace.slug != slug.lower():
if group.namespace.slug != slug.value.lower():
raise errors.UpdatingWithStaleContentError(
message=f"You cannot update group members by using an old group slug {slug}.",
message=f"You cannot update group members by using an old group slug {slug.value}.",
detail=f"The latest slug is {group.namespace.slug}, please use this for updates.",
)

Expand All @@ -239,7 +240,7 @@ async def update_group_members(
@Authz.authz_change(AuthzOperation.delete, ResourceType.group)
@dispatch_message(GroupRemoved)
async def delete_group(
self, user: base_models.APIUser, slug: str, *, session: AsyncSession | None = None
self, user: base_models.APIUser, slug: Slug, *, session: AsyncSession | None = None
) -> models.DeletedGroup | None:
"""Delete a specific group."""
if not session:
Expand All @@ -249,9 +250,9 @@ async def delete_group(
group, _ = await self._get_group(session, user, slug)
except errors.MissingResourceError:
return None
if group.namespace.slug != slug.lower():
if group.namespace.slug != slug.value.lower():
raise errors.UpdatingWithStaleContentError(
message=f"You cannot delete a group by using an old group slug {slug}.",
message=f"You cannot delete a group by using an old group slug {slug.value}.",
detail=f"The latest slug is {group.namespace.slug}, please use this for deletions.",
)
# NOTE: We have a stored procedure that gets triggered when a project slug is removed to remove the project.
Expand All @@ -265,7 +266,7 @@ async def delete_group(
@with_db_transaction
@dispatch_message(events.GroupMembershipChanged)
async def delete_group_member(
self, user: base_models.APIUser, slug: str, user_id_to_delete: str, *, session: AsyncSession | None = None
self, user: base_models.APIUser, slug: Slug, user_id_to_delete: str, *, session: AsyncSession | None = None
) -> list[MembershipChange]:
"""Delete a specific group member."""
if not session:
Expand Down Expand Up @@ -314,7 +315,7 @@ async def insert_group(
await session.refresh(group)
return group.dump()

async def get_group_permissions(self, user: base_models.APIUser, slug: str) -> models.GroupPermissions:
async def get_group_permissions(self, user: base_models.APIUser, slug: Slug) -> models.GroupPermissions:
"""Get the permissions of the user on a given group."""
group = await self.get_group(user=user, slug=slug)

Expand Down
3 changes: 2 additions & 1 deletion components/renku_data_services/project/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from renku_data_services.base_api.etag import extract_if_none_match, if_match_required
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
from renku_data_services.base_api.pagination import PaginationRequest, paginate
from renku_data_services.base_models.core import Slug
from renku_data_services.base_models.validation import validate_and_dump, validated_json
from renku_data_services.data_connectors.db import DataConnectorProjectLinkRepository, DataConnectorRepository
from renku_data_services.errors import errors
Expand Down Expand Up @@ -165,7 +166,7 @@ async def _get_one_by_namespace_slug(
_: Request,
user: base_models.APIUser,
namespace: str,
slug: str,
slug: Slug,
etag: str | None,
query: apispec.NamespacesNamespaceProjectsSlugGetParametersQuery,
) -> JSONResponse | HTTPResponse:
Expand Down
Loading

0 comments on commit 4f35b42

Please sign in to comment.