From c14ec708201e4165d40448227f97ea3f8aa9a549 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 12:42:40 +0300 Subject: [PATCH 01/55] chore(infrastructure): change log level --- .../authorization/local_authorization.py | 8 ++--- .../repositories/authorization/session.py | 12 +++---- .../infrastructure/repositories/company.py | 20 +++++------ .../infrastructure/repositories/context.py | 16 ++++----- .../infrastructure/repositories/contract.py | 2 +- .../infrastructure/repositories/edge.py | 22 ++++++------ .../infrastructure/repositories/grant/user.py | 10 +++--- .../infrastructure/repositories/group.py | 16 ++++----- .../infrastructure/repositories/node.py | 36 +++++++++---------- .../infrastructure/repositories/offer.py | 28 +++++++-------- .../infrastructure/repositories/permission.py | 16 ++++----- .../infrastructure/repositories/role.py | 22 ++++++------ .../infrastructure/repositories/user.py | 20 +++++------ 13 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/prodik/infrastructure/repositories/authorization/local_authorization.py b/src/prodik/infrastructure/repositories/authorization/local_authorization.py index 3d04cdb..4499a49 100644 --- a/src/prodik/infrastructure/repositories/authorization/local_authorization.py +++ b/src/prodik/infrastructure/repositories/authorization/local_authorization.py @@ -16,7 +16,7 @@ class LocalAuthorizationRepositoryImpl(LocalAuthorizationRepository): session: AsyncSession async def create(self, authorization: LocalAuthorization) -> None: - logger.info( + logger.debug( "Repository create local authorization", authorization_id=authorization.id, ) @@ -31,7 +31,7 @@ async def create(self, authorization: LocalAuthorization) -> None: ) async def update(self, authorization: LocalAuthorization) -> None: - logger.info( + logger.debug( "Repository update local authorization", authorization_id=authorization.id, ) @@ -47,13 +47,13 @@ async def update(self, authorization: LocalAuthorization) -> None: ) async def get_by_user_id(self, user_id: UserId) -> LocalAuthorization | None: - logger.info("Repository get local authorization by user id", user_id=user_id) + logger.debug("Repository get local authorization by user id", user_id=user_id) result = await self.session.execute( select(LocalAuthorization).where(LocalAuthorization.user_id == user_id), # type: ignore ) authorization = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched local authorization by user id", found=authorization is not None, ) diff --git a/src/prodik/infrastructure/repositories/authorization/session.py b/src/prodik/infrastructure/repositories/authorization/session.py index 5c4ea52..cf35f7f 100644 --- a/src/prodik/infrastructure/repositories/authorization/session.py +++ b/src/prodik/infrastructure/repositories/authorization/session.py @@ -18,7 +18,7 @@ class SessionRepositoryImpl(SessionRepository): config: CacheConfig async def create(self, session: Session) -> None: - logger.info("Repository create session", session_id=session.id) + logger.debug("Repository create session", session_id=session.id) session_key = f"session:{session.token}" host_key = f"host:{session.host}" @@ -39,7 +39,7 @@ async def create(self, session: Session) -> None: await self.client.expire(session_key, self.config.ttl) async def update(self, session: Session) -> None: - logger.info("Repository update session", session_id=session.id) + logger.debug("Repository update session", session_id=session.id) session_key = f"session:{session.token}" await self.client.hset( session_key, @@ -50,7 +50,7 @@ async def update(self, session: Session) -> None: ) async def get_by_token(self, token: str) -> Session | None: - logger.info("Repository get session by token") + logger.debug("Repository get session by token") data = await self.client.hgetall(f"session:{token}") # type: ignore if not data: @@ -65,11 +65,11 @@ async def get_by_token(self, token: str) -> Session | None: updated_at=datetime.fromisoformat(data["updated_at"]), ) - logger.info("Repository fetched session by token", found=session is not None) + logger.debug("Repository fetched session by token", found=session is not None) return session async def get_by_host(self, host: str) -> Session | None: - logger.info("Repository get session by host", host=host) + logger.debug("Repository get session by host", host=host) token = await self.client.get(f"host:{host}") if token is None: @@ -86,6 +86,6 @@ async def get_by_host(self, host: str) -> Session | None: updated_at=datetime.fromisoformat(data["updated_at"]), ) - logger.info("Repository fetched session by host", found=session is not None) + logger.debug("Repository fetched session by host", found=session is not None) return session diff --git a/src/prodik/infrastructure/repositories/company.py b/src/prodik/infrastructure/repositories/company.py index b1bd6a2..a9146aa 100644 --- a/src/prodik/infrastructure/repositories/company.py +++ b/src/prodik/infrastructure/repositories/company.py @@ -17,7 +17,7 @@ class CompanyRepositoryImpl(CompanyRepository): session: AsyncSession async def create(self, company: Company) -> None: - logger.info("Repository create company", company_id=company.id) + logger.debug("Repository create company", company_id=company.id) await self.session.execute( insert(Company).values( id=company.id, @@ -31,7 +31,7 @@ async def create(self, company: Company) -> None: ) async def update(self, company: Company) -> None: - logger.info("Repository update company", company_id=company.id) + logger.debug("Repository update company", company_id=company.id) await self.session.execute( update(Company) .where( @@ -46,7 +46,7 @@ async def update(self, company: Company) -> None: ) async def get_by_name(self, name: CompanyName) -> Company | None: - logger.info("Repository get company by name", company_name=name) + logger.debug("Repository get company by name", company_name=name) result = await self.session.execute( select(Company).where( Company.name == name, # type: ignore @@ -54,11 +54,11 @@ async def get_by_name(self, name: CompanyName) -> Company | None: ) company = result.scalar_one_or_none() - logger.info("Repository fetched company by name", found=company is not None) + logger.debug("Repository fetched company by name", found=company is not None) return company async def get_by_id(self, company_id: CompanyId) -> Company: - logger.info("Repository get company by id", company_id=company_id) + logger.debug("Repository get company by id", company_id=company_id) result = await self.session.execute( select(Company).where( Company.id == company_id, # type: ignore @@ -67,7 +67,7 @@ async def get_by_id(self, company_id: CompanyId) -> Company: company = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched company by id", found=company is not None, ) @@ -84,7 +84,7 @@ async def get_by_ids(self, company_ids: list[CompanyId]) -> list[Company]: if not company_ids: return [] - logger.info("Repository get companies by ids", request_count=len(company_ids)) + logger.debug("Repository get companies by ids", request_count=len(company_ids)) result = await self.session.execute( select(Company).where( @@ -94,7 +94,7 @@ async def get_by_ids(self, company_ids: list[CompanyId]) -> list[Company]: result_companies = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched companies by ids", found_count=len(result_companies), ) @@ -102,7 +102,7 @@ async def get_by_ids(self, company_ids: list[CompanyId]) -> list[Company]: return result_companies async def get_by_user_id(self, user_id: UserId) -> Company | None: - logger.info("Repository get company by user id", user_id=user_id) + logger.debug("Repository get company by user id", user_id=user_id) result = await self.session.execute( select(Company) .where( @@ -112,7 +112,7 @@ async def get_by_user_id(self, user_id: UserId) -> Company | None: ) company = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched company by user id", found=company is not None, ) diff --git a/src/prodik/infrastructure/repositories/context.py b/src/prodik/infrastructure/repositories/context.py index 2eded24..0635791 100644 --- a/src/prodik/infrastructure/repositories/context.py +++ b/src/prodik/infrastructure/repositories/context.py @@ -17,7 +17,7 @@ class ContextRepositoryImpl(ContextRepository): session: AsyncSession async def create(self, context: Context) -> None: - logger.info("Repository create context", context_id=context.id) + logger.debug("Repository create context", context_id=context.id) await self.session.execute( insert(Context).values( id=context.id, @@ -30,7 +30,7 @@ async def create(self, context: Context) -> None: ) async def get_by_id(self, context_id: ContextId) -> Context: - logger.info("Repository get context by id", context_id=context_id) + logger.debug("Repository get context by id", context_id=context_id) result = await self.session.execute( select(Context).where( Context.id == context_id, # type: ignore @@ -39,7 +39,7 @@ async def get_by_id(self, context_id: ContextId) -> Context: context = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched context by id", found=context is not None, ) @@ -53,7 +53,7 @@ async def get_by_id(self, context_id: ContextId) -> Context: return context async def delete(self, context: Context) -> None: - logger.info("Repository delete context", context_id=context.id) + logger.debug("Repository delete context", context_id=context.id) await self.session.execute( delete(Context).where( Context.id == context.id, # type: ignore @@ -61,7 +61,7 @@ async def delete(self, context: Context) -> None: ) async def get_all_by_company_id(self, company_id: CompanyId) -> list[Context]: - logger.info("Repository get groups by contexts id", company_id=company_id) + logger.debug("Repository get groups by contexts id", company_id=company_id) result = await self.session.execute( select(Context).where( Context.company_id == company_id, # type: ignore @@ -69,14 +69,14 @@ async def get_all_by_company_id(self, company_id: CompanyId) -> list[Context]: ) result_groups = list(result.scalars()) - logger.info( + logger.debug( "Repository fetched contexts by company_id", found_count=len(result_groups), ) return result_groups async def get_all_by_ids(self, context_ids: list[ContextId]) -> list[Context]: - logger.info("Repository get contexts by ids", request_count=len(context_ids)) + logger.debug("Repository get contexts by ids", request_count=len(context_ids)) if not context_ids: return [] @@ -85,7 +85,7 @@ async def get_all_by_ids(self, context_ids: list[ContextId]) -> list[Context]: ) result_contexts = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched contexts by ids", found_count=len(result_contexts), ) diff --git a/src/prodik/infrastructure/repositories/contract.py b/src/prodik/infrastructure/repositories/contract.py index 5890802..f6b9620 100644 --- a/src/prodik/infrastructure/repositories/contract.py +++ b/src/prodik/infrastructure/repositories/contract.py @@ -15,7 +15,7 @@ class ContractRepositoryImpl(ContractRepository): session: AsyncSession async def create(self, contract: Contract) -> None: - logger.info("Repository create contract", contract_id=contract.id) + logger.debug("Repository create contract", contract_id=contract.id) await self.session.execute( insert(Contract).values( diff --git a/src/prodik/infrastructure/repositories/edge.py b/src/prodik/infrastructure/repositories/edge.py index 94ce366..d9d3717 100644 --- a/src/prodik/infrastructure/repositories/edge.py +++ b/src/prodik/infrastructure/repositories/edge.py @@ -18,7 +18,7 @@ class EdgeRepositoryImpl(EdgeRepository): session: AsyncSession async def create(self, edge: Edge) -> None: - logger.info("Repository create edge", edge_id=edge.id) + logger.debug("Repository create edge", edge_id=edge.id) await self.session.execute( insert(Edge).values( id=edge.id, @@ -33,7 +33,7 @@ async def create(self, edge: Edge) -> None: ) async def delete(self, edge: Edge) -> None: - logger.info("Repository delete edge", edge_id=edge.id) + logger.debug("Repository delete edge", edge_id=edge.id) await self.session.execute( delete(Edge).where( Edge.id == edge.id, # type: ignore @@ -41,7 +41,7 @@ async def delete(self, edge: Edge) -> None: ) async def update(self, edge: Edge) -> None: - logger.info("Repository update edge", edge_id=edge.id) + logger.debug("Repository update edge", edge_id=edge.id) await self.session.execute( update(Edge) .where( @@ -54,7 +54,7 @@ async def update(self, edge: Edge) -> None: ) async def get_neighbours_by_node_id(self, node_id: NodeId) -> list[Edge]: - logger.info("Repository get neighbours by node id", node_id=node_id) + logger.debug("Repository get neighbours by node id", node_id=node_id) result = await self.session.execute( select(Edge).where( @@ -67,7 +67,7 @@ async def get_neighbours_by_node_id(self, node_id: NodeId) -> list[Edge]: result_edges = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched neighbours by node id", found_count=len(result_edges), ) @@ -75,7 +75,7 @@ async def get_neighbours_by_node_id(self, node_id: NodeId) -> list[Edge]: return result_edges async def get_by_id(self, edge_id: EdgeId) -> Edge: - logger.info("Repository get edge by id", edge_id=edge_id) + logger.debug("Repository get edge by id", edge_id=edge_id) result = await self.session.execute( select(Edge).where( Edge.id == edge_id, # type: ignore @@ -84,7 +84,7 @@ async def get_by_id(self, edge_id: EdgeId) -> Edge: edge = result.scalar_one_or_none() - logger.info("Repository fetched edge by id", found=edge is not None) + logger.debug("Repository fetched edge by id", found=edge is not None) if edge is None: raise EdgeNotFoundError( @@ -100,7 +100,7 @@ async def get_by_nodes_and_context( node_b_id: NodeId, context_id: ContextId, ) -> Edge | None: - logger.info( + logger.debug( "Repository get edge by nodes and context", node_a_id=node_a_id, node_b_id=node_b_id, @@ -117,7 +117,7 @@ async def get_by_nodes_and_context( ) edge = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched edge by nodes and context", found=edge is not None, ) @@ -125,7 +125,7 @@ async def get_by_nodes_and_context( return edge async def get_all_by_context_id(self, context_id: ContextId) -> list[Edge]: - logger.info("Repository get edges by context id", context_id=context_id) + logger.debug("Repository get edges by context id", context_id=context_id) result = await self.session.execute( select(Edge).where( Edge.context_id == context_id, # type: ignore @@ -133,7 +133,7 @@ async def get_all_by_context_id(self, context_id: ContextId) -> list[Edge]: ) result_edges = list(result.scalars()) - logger.info( + logger.debug( "Repository fetched edges by context id", found_count=len(result_edges), ) diff --git a/src/prodik/infrastructure/repositories/grant/user.py b/src/prodik/infrastructure/repositories/grant/user.py index 7f73464..1188f58 100644 --- a/src/prodik/infrastructure/repositories/grant/user.py +++ b/src/prodik/infrastructure/repositories/grant/user.py @@ -17,7 +17,7 @@ class UserGrantRepositoryImpl(UserGrantRepository): session: AsyncSession async def create(self, grant: UserGrant) -> None: - logger.info("Repository create user grant", grant_id=grant.id) + logger.debug("Repository create user grant", grant_id=grant.id) await self.session.execute( insert(UserGrant).values( id=grant.id, @@ -29,14 +29,14 @@ async def create(self, grant: UserGrant) -> None: ) async def get_all_by_user_id(self, user_id: UserId) -> list[UserGrant]: - logger.info("Repository get user grants by user id", user_id=user_id) + logger.debug("Repository get user grants by user id", user_id=user_id) result = await self.session.execute( select(UserGrant).where( UserGrant.user_id == user_id, # type: ignore ), ) result_grants = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched grants by user id", count=len(result_grants), ) @@ -48,7 +48,7 @@ async def get_by_user_and_role_id( user_id: UserId, role_id: RoleId, ) -> UserGrant | None: - logger.info( + logger.debug( "Repository get user grant by user and role id", user_id=user_id, role_id=role_id, @@ -60,7 +60,7 @@ async def get_by_user_and_role_id( ) grant = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched context by id", found=grant is not None, ) diff --git a/src/prodik/infrastructure/repositories/group.py b/src/prodik/infrastructure/repositories/group.py index 33d6912..8aca04a 100644 --- a/src/prodik/infrastructure/repositories/group.py +++ b/src/prodik/infrastructure/repositories/group.py @@ -17,7 +17,7 @@ class GroupRepositoryImpl(GroupRepository): session: AsyncSession async def create(self, group: Group) -> None: - logger.info("Repository create group", group_id=group.id) + logger.debug("Repository create group", group_id=group.id) await self.session.execute( insert(Group).values( id=group.id, @@ -30,7 +30,7 @@ async def create(self, group: Group) -> None: ) async def get_by_id(self, group_id: GroupId) -> Group: - logger.info("Repository get group by id", group_id=group_id) + logger.debug("Repository get group by id", group_id=group_id) result = await self.session.execute( select(Group).where( Group.id == group_id, # type: ignore @@ -39,7 +39,7 @@ async def get_by_id(self, group_id: GroupId) -> Group: group = result.scalar_one_or_none() - logger.info("Repository fetched group by id", found=group is not None) + logger.debug("Repository fetched group by id", found=group is not None) if group is None: raise GroupNotFoundError( @@ -50,7 +50,7 @@ async def get_by_id(self, group_id: GroupId) -> Group: return group async def delete(self, group: Group) -> None: - logger.info("Repository delete group", group_id=group.id) + logger.debug("Repository delete group", group_id=group.id) await self.session.execute( delete(Group).where( Group.id == group.id, # type: ignore @@ -58,7 +58,7 @@ async def delete(self, group: Group) -> None: ) async def get_all_by_company_id(self, company_id: CompanyId) -> list[Group]: - logger.info("Repository get groups by company id", company_id=company_id) + logger.debug("Repository get groups by company id", company_id=company_id) result = await self.session.execute( delete(Group).where( Group.company_id == company_id, # type: ignore @@ -66,14 +66,14 @@ async def get_all_by_company_id(self, company_id: CompanyId) -> list[Group]: ) result_groups = list(result.scalars()) - logger.info( + logger.debug( "Repository fetched groups by company id", found_count=len(result_groups), ) return result_groups async def get_all_by_ids(self, group_ids: list[GroupId]) -> list[Group]: - logger.info("Repository get groups by ids", request_count=len(group_ids)) + logger.debug("Repository get groups by ids", request_count=len(group_ids)) if not group_ids: return [] @@ -82,5 +82,5 @@ async def get_all_by_ids(self, group_ids: list[GroupId]) -> list[Group]: ) result_groups = list(result.scalars().all()) - logger.info("Repository fetched groups by ids", found_count=len(result_groups)) + logger.debug("Repository fetched groups by ids", found_count=len(result_groups)) return result_groups diff --git a/src/prodik/infrastructure/repositories/node.py b/src/prodik/infrastructure/repositories/node.py index e88def7..89ebd0c 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/prodik/infrastructure/repositories/node.py @@ -20,7 +20,7 @@ class NodeRepositoryImpl(NodeRepository): session: AsyncSession async def create(self, node: Node) -> None: - logger.info("Repository create node", node_id=node.id) + logger.debug("Repository create node", node_id=node.id) await self.session.execute( insert(Node).values( id=node.id, @@ -33,7 +33,7 @@ async def create(self, node: Node) -> None: ) async def get_all_by_ids(self, nodes_ids: list[NodeId]) -> list[Node]: - logger.info("Repository get nodes by ids", request_count=len(nodes_ids)) + logger.debug("Repository get nodes by ids", request_count=len(nodes_ids)) if not nodes_ids: return [] @@ -44,11 +44,11 @@ async def get_all_by_ids(self, nodes_ids: list[NodeId]) -> list[Node]: ) result_nodes = list(result.scalars().all()) - logger.info("Repository fetched nodes by ids", found_count=len(result_nodes)) + logger.debug("Repository fetched nodes by ids", found_count=len(result_nodes)) return result_nodes async def delete(self, node: Node) -> None: - logger.info("Repository delete node", node_id=node.id) + logger.debug("Repository delete node", node_id=node.id) await self.session.execute( delete(Node).where( Node.id == node.id, # type: ignore @@ -56,7 +56,7 @@ async def delete(self, node: Node) -> None: ) async def get_by_id(self, node_id: NodeId) -> Node: - logger.info("Repository get node by id", node_id=node_id) + logger.debug("Repository get node by id", node_id=node_id) result = await self.session.execute( select(Node).where( Node.id == node_id, # type: ignore @@ -65,7 +65,7 @@ async def get_by_id(self, node_id: NodeId) -> Node: node = result.scalar_one_or_none() - logger.info("Repository fetched node by id", found=node is not None) + logger.debug("Repository fetched node by id", found=node is not None) if node is None: raise NodeNotFoundError( @@ -76,7 +76,7 @@ async def get_by_id(self, node_id: NodeId) -> Node: return node async def update(self, node: Node) -> None: - logger.info("Repository update node", node_id=node.id) + logger.debug("Repository update node", node_id=node.id) await self.session.execute( update(Node) .where( @@ -91,7 +91,7 @@ async def update(self, node: Node) -> None: async def get_all_by_associations( self, associations: list[NodeAssociation] ) -> list[Node]: - logger.info( + logger.debug( "Repository get nodes by associations", request_count=len(associations), ) @@ -106,7 +106,7 @@ async def get_all_by_associations( ) result_nodes = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched nodes by associations", found_count=len(result_nodes), ) @@ -118,7 +118,7 @@ class NodeAssociationRepositoryImpl(NodeAssociationRepository): session: AsyncSession async def create(self, node_association: NodeAssociation) -> None: - logger.info( + logger.debug( "Repository create node association", association_id=node_association.id, ) @@ -133,7 +133,7 @@ async def create(self, node_association: NodeAssociation) -> None: ) async def delete(self, node_association: NodeAssociation) -> None: - logger.info( + logger.debug( "Repository delete node association", association_id=node_association.id, ) @@ -147,7 +147,7 @@ async def get_by_id( self, node_association_id: NodeAssociationId, ) -> NodeAssociation: - logger.info( + logger.debug( "Repository get node association by id", association_id=node_association_id, ) @@ -159,7 +159,7 @@ async def get_by_id( association = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched node association by id", found=association is not None, ) @@ -173,7 +173,7 @@ async def get_by_id( return association async def create_all(self, node_association: list[NodeAssociation]) -> None: - logger.info( + logger.debug( "Repository create node associations batch", request_count=len(node_association), ) @@ -196,7 +196,7 @@ async def create_all(self, node_association: list[NodeAssociation]) -> None: ) async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: - logger.info("Repository get node associations by group id", group_id=group_id) + logger.debug("Repository get node associations by group id", group_id=group_id) result = await self.session.execute( select(NodeAssociation).where( NodeAssociation.group_id == group_id, # type: ignore @@ -204,7 +204,7 @@ async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: ) result_associations = list(result.scalars()) - logger.info( + logger.debug( "Repository fetched node associations by group id", count=len(result_associations), ) @@ -214,7 +214,7 @@ async def get_all_by_node_id( self, node_id: NodeId, ) -> list[NodeAssociation]: - logger.info( + logger.debug( "Repository get node associations by node id", node_id=node_id, ) @@ -226,7 +226,7 @@ async def get_all_by_node_id( ) result_associations = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched all node associations by node id", found_count=len(result_associations), ) diff --git a/src/prodik/infrastructure/repositories/offer.py b/src/prodik/infrastructure/repositories/offer.py index beed191..e9a0eeb 100644 --- a/src/prodik/infrastructure/repositories/offer.py +++ b/src/prodik/infrastructure/repositories/offer.py @@ -27,7 +27,7 @@ class OfferRepositoryImpl(OfferRepository): session: AsyncSession async def create(self, offer: Offer) -> None: - logger.info("Repository create offer", offer_id=offer.id) + logger.debug("Repository create offer", offer_id=offer.id) await self.session.execute( insert(Offer).values( id=offer.id, @@ -45,7 +45,7 @@ async def create(self, offer: Offer) -> None: ) async def update(self, offer: Offer) -> None: - logger.info("Repository update offer", offer_id=offer.id) + logger.debug("Repository update offer", offer_id=offer.id) await self.session.execute( update(Offer) .where( @@ -63,7 +63,7 @@ async def update(self, offer: Offer) -> None: ) async def get_by_id(self, offer_id: OfferId) -> Offer: - logger.info("Repository get offer by id", offer_id=offer_id) + logger.debug("Repository get offer by id", offer_id=offer_id) result = await self.session.execute( select(Offer).where( Offer.id == offer_id, # type: ignore @@ -72,7 +72,7 @@ async def get_by_id(self, offer_id: OfferId) -> Offer: offer = result.scalar_one_or_none() - logger.info("Repository fetched offer by id", found=offer is not None) + logger.debug("Repository fetched offer by id", found=offer is not None) if offer is None: raise OfferNotFoundError( @@ -88,7 +88,7 @@ class OfferLinkRepositoryImpl(OfferLinkRepository): session: AsyncSession async def create(self, offer_link: OfferLink) -> None: - logger.info("Repository create offer link", offer_link_id=offer_link.id) + logger.debug("Repository create offer link", offer_link_id=offer_link.id) await self.session.execute( insert(OfferLink).values( id=offer_link.id, @@ -101,7 +101,7 @@ async def create(self, offer_link: OfferLink) -> None: ) async def update(self, offer_link: OfferLink) -> None: - logger.info("Repository update offer link", offer_link_id=offer_link.id) + logger.debug("Repository update offer link", offer_link_id=offer_link.id) await self.session.execute( update(OfferLink) .where( @@ -119,7 +119,7 @@ async def get_by_offers_ids( from_offer_id: OfferId, to_offer_id: OfferId, ) -> OfferLink | None: - logger.info( + logger.debug( "Repository get offer link by offer ids", from_offer_id=from_offer_id, to_offer_id=to_offer_id, @@ -132,7 +132,7 @@ async def get_by_offers_ids( ), ), ) - logger.info("Repository fetched offer link by id", found=result is not None) + logger.debug("Repository fetched offer link by id", found=result is not None) return result.scalar_one_or_none() @@ -142,7 +142,7 @@ class OfferGroupRepositoryImpl(OfferGroupRepository): session: AsyncSession async def create_all(self, offer_groups: list[OfferGroup]) -> None: - logger.info( + logger.debug( "Repository create offer groups", request_count=len(offer_groups), ) @@ -166,7 +166,7 @@ async def create_all(self, offer_groups: list[OfferGroup]) -> None: ) async def get_by_offer_id(self, offer_id: OfferId) -> list[OfferGroup]: - logger.info("Repository get offer group by offer id") + logger.debug("Repository get offer group by offer id") result = await self.session.execute( select(OfferGroup).where( OfferGroup.offer_id == offer_id, # type: ignore @@ -174,7 +174,7 @@ async def get_by_offer_id(self, offer_id: OfferId) -> list[OfferGroup]: ) result_offer_groups = list(result.scalars().all()) - logger.info( + logger.debug( "Repository offer groups by offer id", found_count=len(result_offer_groups), ) @@ -187,7 +187,7 @@ class OfferContextRepositoryImpl(OfferContextRepository): session: AsyncSession async def create_all(self, offer_contexts: list[OfferContext]) -> None: - logger.info( + logger.debug( "Repository create offer contexts", request_count=len(offer_contexts), ) @@ -212,7 +212,7 @@ async def create_all(self, offer_contexts: list[OfferContext]) -> None: ) async def get_by_offer_id(self, offer_id: OfferId) -> list[OfferContext]: - logger.info("Repository get offer group by offer id") + logger.debug("Repository get offer group by offer id") result = await self.session.execute( select(OfferContext).where( OfferContext.offer_id == offer_id, # type: ignore @@ -220,7 +220,7 @@ async def get_by_offer_id(self, offer_id: OfferId) -> list[OfferContext]: ) result_offer_contexts = list(result.scalars().all()) - logger.info( + logger.debug( "Repository offer contexts by offer id", found_count=len(result_offer_contexts), ) diff --git a/src/prodik/infrastructure/repositories/permission.py b/src/prodik/infrastructure/repositories/permission.py index 743ce2d..f64c872 100644 --- a/src/prodik/infrastructure/repositories/permission.py +++ b/src/prodik/infrastructure/repositories/permission.py @@ -21,7 +21,7 @@ async def create_all( self, permissions: list[RolePermission], ) -> None: - logger.info( + logger.debug( "Repository create role permissions", request_count=len(permissions), ) @@ -49,7 +49,7 @@ async def get_all_by_role_id( self, role_id: RoleId, ) -> list[RolePermission]: - logger.info( + logger.debug( "Repository get permissions by role id", role_id=role_id, ) @@ -61,7 +61,7 @@ async def get_all_by_role_id( ) result_permissions = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched permissions by role id", found_count=len(result_permissions), ) @@ -72,7 +72,7 @@ async def get_all_by_role_ids(self, role_ids: list[RoleId]) -> list[RolePermissi if not role_ids: return [] - logger.info( + logger.debug( "Repository get permissions by role ids", request_count=len(role_ids) ) @@ -83,7 +83,7 @@ async def get_all_by_role_ids(self, role_ids: list[RoleId]) -> list[RolePermissi ) result_permissions = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched permissions by role ids", found_count=len(result_permissions), ) @@ -93,7 +93,7 @@ async def update_all(self, permissions: list[RolePermission]) -> None: if not permissions: return - logger.info( + logger.debug( "Repository update role permissions", request_count=len(permissions), ) @@ -116,7 +116,7 @@ async def get_all_by_ids( self, permission_ids: list[RolePermissionId], ) -> list[RolePermission]: - logger.info("Repository get nodes by ids", request_count=len(permission_ids)) + logger.debug("Repository get nodes by ids", request_count=len(permission_ids)) if not permission_ids: return [] @@ -127,7 +127,7 @@ async def get_all_by_ids( ) result_permissions = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched permissions by ids", found_count=len(result_permissions), ) diff --git a/src/prodik/infrastructure/repositories/role.py b/src/prodik/infrastructure/repositories/role.py index ca88777..9dc48f5 100644 --- a/src/prodik/infrastructure/repositories/role.py +++ b/src/prodik/infrastructure/repositories/role.py @@ -18,7 +18,7 @@ class RoleRepositoryImpl(RoleRepository): session: AsyncSession async def create(self, role: Role) -> None: - logger.info("Repository create role", role_id=role.id) + logger.debug("Repository create role", role_id=role.id) await self.session.execute( insert(Role).values( id=role.id, @@ -30,7 +30,7 @@ async def create(self, role: Role) -> None: ) async def get_all_by_ids(self, roles_ids: list[RoleId]) -> list[Role]: - logger.info( + logger.debug( "Repository get roles by roles id", request_count=len(roles_ids), ) @@ -39,7 +39,7 @@ async def get_all_by_ids(self, roles_ids: list[RoleId]) -> list[Role]: result = await self.session.execute(select(Role).where(Role.id.in_(roles_ids))) # type: ignore roles = list(result.scalars().all()) - logger.info( + logger.debug( "Repository fetched roles by role id", found_count=len(roles), ) @@ -47,7 +47,7 @@ async def get_all_by_ids(self, roles_ids: list[RoleId]) -> list[Role]: return roles async def get_by_name(self, name: RoleName) -> Role | None: - logger.info("Repository get role by name", role_name=name) + logger.debug("Repository get role by name", role_name=name) result = await self.session.execute( select(Role).where( Role.name == name, # type: ignore @@ -55,12 +55,12 @@ async def get_by_name(self, name: RoleName) -> Role | None: ) role = result.scalar_one_or_none() - logger.info("Repository fetched role by name", found=role is not None) + logger.debug("Repository fetched role by name", found=role is not None) return role async def update(self, role: Role) -> None: - logger.info("Repository update role", role_id=role.id) + logger.debug("Repository update role", role_id=role.id) await self.session.execute( update(Role) .where( @@ -72,7 +72,7 @@ async def update(self, role: Role) -> None: ) async def delete(self, role: Role) -> None: - logger.info("Repository delete role", role_id=role.id) + logger.debug("Repository delete role", role_id=role.id) await self.session.execute( delete(Role).where( Role.id == role.id, # type: ignore @@ -80,14 +80,14 @@ async def delete(self, role: Role) -> None: ) async def get_by_id(self, role_id: RoleId) -> Role: - logger.info("Repository get role by id", role_id=role_id) + logger.debug("Repository get role by id", role_id=role_id) result = await self.session.execute( select(Role).where(Role.id == role_id), # type: ignore ) role = result.scalar_one_or_none() - logger.info("Repository fetched role by id", found=role is not None) + logger.debug("Repository fetched role by id", found=role is not None) if role is None: raise RoleNotFoundError( @@ -100,7 +100,7 @@ async def get_by_id(self, role_id: RoleId) -> Role: async def get_by_name_and_company_id( self, name: RoleName, company_id: CompanyId ) -> Role | None: - logger.info( + logger.debug( "Repository get role by name and company id", company_id=company_id, name=name, @@ -115,7 +115,7 @@ async def get_by_name_and_company_id( ) role = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched role by name and company id", found=role is not None ) return role diff --git a/src/prodik/infrastructure/repositories/user.py b/src/prodik/infrastructure/repositories/user.py index 9ad6ec0..198805c 100644 --- a/src/prodik/infrastructure/repositories/user.py +++ b/src/prodik/infrastructure/repositories/user.py @@ -16,7 +16,7 @@ class UserRepositoryImpl(UserRepository): session: AsyncSession async def create(self, user: User) -> None: - logger.info("Repository create user", user_id=user.id) + logger.debug("Repository create user", user_id=user.id) await self.session.execute( insert(User).values( id=user.id, @@ -33,7 +33,7 @@ async def create(self, user: User) -> None: ) async def update(self, user: User) -> None: - logger.info("Repository update user", user_id=user.id) + logger.debug("Repository update user", user_id=user.id) await self.session.execute( update(User) .where( @@ -55,7 +55,7 @@ async def get_by_username_or_email( username: Username, email: Email, ) -> User | None: - logger.info( + logger.debug( "Repository get user by username or email", username=username, email=email, @@ -71,13 +71,13 @@ async def get_by_username_or_email( .limit(1), ) user = result.scalar_one_or_none() - logger.info( + logger.debug( "Repository fetched user by username or email", ) return user async def get_by_username(self, username: Username) -> User | None: - logger.info( + logger.debug( "Repository get user by username", username=username, ) @@ -87,18 +87,18 @@ async def get_by_username(self, username: Username) -> User | None: ), ) user = result.scalar_one_or_none() - logger.info("Repository fetched user by username", found=user is not None) + logger.debug("Repository fetched user by username", found=user is not None) return user async def get_by_id(self, user_id: UserId) -> User: - logger.info("Repository get user by id", user_id=user_id) + logger.debug("Repository get user by id", user_id=user_id) result = await self.session.execute( select(User).where(User.id == user_id), # type: ignore ) user = result.scalar_one_or_none() - logger.info("Repository fetched user by id", found=user is not None) + logger.debug("Repository fetched user by id", found=user is not None) if user is None: raise UserNotFoundError( @@ -109,11 +109,11 @@ async def get_by_id(self, user_id: UserId) -> User: return user async def get_by_email(self, email: Email) -> User | None: - logger.info("Repository get user by email", email=email) + logger.debug("Repository get user by email", email=email) result = await self.session.execute( select(User).where(User.email == email), # type: ignore ) user = result.scalar_one_or_none() - logger.info("Repository fetched user by email", found=user is not None) + logger.debug("Repository fetched user by email", found=user is not None) return user From d9e1aaedc30118aed00d41719a868184cb19999c Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 23:01:33 +0300 Subject: [PATCH 02/55] test(): add test covers negative edge weight --- tests/edge/test_decrement_edge.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/edge/test_decrement_edge.py b/tests/edge/test_decrement_edge.py index 8e310f8..195c98a 100644 --- a/tests/edge/test_decrement_edge.py +++ b/tests/edge/test_decrement_edge.py @@ -1,6 +1,8 @@ +import random from http import HTTPStatus import pytest +from dirty_equals import IsPartialDict from httpx import AsyncClient from tests.factories import ( @@ -30,6 +32,7 @@ async def test_decrement_edge_ok( node_b=nodes[1], company=company, context=context, + weight=random.randint(1, 100), ) response = await test_client.patch( @@ -38,3 +41,31 @@ async def test_decrement_edge_ok( ) assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_decrement_edge_weight_cannot_be_negative( + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + edge_factory: EdgeFactory, + node_factory: NodeFactory, + test_client: AsyncClient, +) -> None: + user_factory_response = await user_factory.create_user(admin=False) + company = await company_factory.create_company(user=user_factory_response.user) + nodes = [await node_factory.create_node(company=company) for _ in range(2)] + context = await context_factory.create_context(company=company) + edge = await edge_factory.create_edge( + node_a=nodes[0], node_b=nodes[1], company=company, context=context, weight=0 + ) + + response = await test_client.patch( + f"/edges/{edge.id}/weight/decrement", + headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, + ) + + assert response.status_code == HTTPStatus.UNPROCESSABLE_CONTENT + assert response.json() == IsPartialDict( + detail="Edge weight cannot be negative", meta=None + ) From 2a4d96a1311f1a29a5997091c093b56b8fd9d20e Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 23:02:03 +0300 Subject: [PATCH 03/55] test(): add tests for neighbour graph traversal --- tests/node/test_get_neighbours.py | 95 +++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/tests/node/test_get_neighbours.py b/tests/node/test_get_neighbours.py index 0270462..c0e90c3 100644 --- a/tests/node/test_get_neighbours.py +++ b/tests/node/test_get_neighbours.py @@ -1,6 +1,8 @@ from http import HTTPStatus +from typing import Final import pytest +from dirty_equals import IsPartialDict from httpx import AsyncClient from tests.factories import ( @@ -12,9 +14,11 @@ UserFactory, ) +DEPTH_TRIGGER: Final = 5 + @pytest.mark.asyncio -async def test_get_neighbours_ok( +async def test_get_direct_neighbours_ok( test_client: AsyncClient, user_factory: UserFactory, edge_factory: EdgeFactory, @@ -23,26 +27,109 @@ async def test_get_neighbours_ok( context_factory: ContextFactory, company_factory: CompanyFactory, ) -> None: - user_factory_response = await user_factory.create_user() + user_factory_response = await user_factory.create_user(admin=False) + company = await company_factory.create_company(user_factory_response.user) group = await group_factory.create_group(company) context = await context_factory.create_context(company) + nodes = [ - await node_factory.create_node(group=group, company=company) for _ in range(4) + await node_factory.create_node( + company=company, group=group, user=user_factory_response.user + ) + for _ in range(10) ] for i in range(1, len(nodes)): await edge_factory.create_edge( - node_a=nodes[0], node_b=nodes[i], company=company, context=context + nodes[0], nodes[i], company=company, context=context ) response = await test_client.get( f"/nodes/{nodes[0].id}/neighbours", params={ "context_id": str(context.id), + "depth": 1, }, headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, ) assert response.status_code == HTTPStatus.OK assert len(response.json()) == len(nodes) - 1 + + +@pytest.mark.asyncio +async def test_get_deep_neighbours_ok( + test_client: AsyncClient, + user_factory: UserFactory, + edge_factory: EdgeFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + context_factory: ContextFactory, + company_factory: CompanyFactory, +) -> None: + user_factory_response = await user_factory.create_user(admin=False) + + company = await company_factory.create_company(user_factory_response.user) + group = await group_factory.create_group(company) + context = await context_factory.create_context(company) + + nodes = [ + await node_factory.create_node( + company=company, group=group, user=user_factory_response.user + ) + for _ in range(10) + ] + + cursor = 0 + for i in range(1, len(nodes)): + if i > DEPTH_TRIGGER: + cursor += 1 + + await edge_factory.create_edge( + nodes[cursor], nodes[i], company=company, context=context + ) + + response = await test_client.get( + f"/nodes/{nodes[0].id}/neighbours", + params={ + "context_id": str(context.id), + "depth": 2, + }, + headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, + ) + + assert response.status_code == HTTPStatus.OK + assert len(response.json()) == len(nodes) + + +@pytest.mark.asyncio +async def test_get_neighbours_depth_cannot_be_negative( + test_client: AsyncClient, + user_factory: UserFactory, + node_factory: NodeFactory, + context_factory: ContextFactory, + company_factory: CompanyFactory, +) -> None: + user_factory_response = await user_factory.create_user(admin=False) + + company = await company_factory.create_company(user_factory_response.user) + context = await context_factory.create_context(company) + + node = await node_factory.create_node( + company=company, user=user_factory_response.user + ) + + response = await test_client.get( + f"/nodes/{node.id}/neighbours", + params={ + "context_id": str(context.id), + "depth": -1, + }, + headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, + ) + + assert response.status_code == HTTPStatus.UNPROCESSABLE_CONTENT + assert response.json() == IsPartialDict( + detail="Depth cannot be negative", meta=[{"key": "depth", "value": -1}] + ) From 10dffefd62674ca561b9f9d61b512bd0428d512d Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 23:02:38 +0300 Subject: [PATCH 04/55] feat(application): interactor for neighbour graph traversal --- .../receive_graph_statistics/neighbours.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/prodik/application/receive_graph_statistics/neighbours.py b/src/prodik/application/receive_graph_statistics/neighbours.py index 4ed6213..79cb147 100644 --- a/src/prodik/application/receive_graph_statistics/neighbours.py +++ b/src/prodik/application/receive_graph_statistics/neighbours.py @@ -1,5 +1,7 @@ +from collections import deque from dataclasses import dataclass +from prodik.application.errors import DepthCannotBeNegativeError from prodik.application.interfaces.repositories import ( ContextRepository, EdgeRepository, @@ -15,6 +17,7 @@ class GetNodeNeighboursRequestDTO: node_id: NodeId context_id: ContextId + depth: int @dataclass @@ -34,18 +37,35 @@ async def execute( await self.access_control_service.ensure_user_can_view_context(user, context) node = await self.node_repository.get_by_id(request.node_id) - neighbour_edges = await self.edge_repository.get_neighbours_by_node_id(node.id) + edges = await self.edge_repository.get_all_by_context_id(context.id) + + if request.depth < 0: + raise DepthCannotBeNegativeError( + "Depth cannot be negative", [{"key": "depth", "value": request.depth}] + ) + + queue: deque[tuple[NodeId, int]] = deque([(node.id, 0)]) + neighbours: dict[NodeId, Edge] = {} + + queue.append((node.id, 0)) + + while queue: + node_id, depth = queue.popleft() + + if depth >= request.depth: + continue + + for edge in edges: + other = edge.other_end(node_id) + + if ( + node_id in {edge.node_a_id, edge.node_b_id} + ) and other not in neighbours: + neighbours[other] = edge + queue.append((other, depth + 1)) - edge_by_node_id = { - (edge.node_b_id if edge.node_a_id == node.id else edge.node_a_id): edge - for edge in neighbour_edges - } - neighbour_nodes = await self.node_repository.get_all_by_ids( - list(edge_by_node_id.keys()) + list(neighbours.keys()) ) - return [ - (neighbour_node, edge_by_node_id[neighbour_node.id]) - for neighbour_node in neighbour_nodes - ] + return [(node, neighbours[node.id]) for node in neighbour_nodes] From 548ae877f13ce0b59e04c7c0c48d62476bf91876 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 23:02:53 +0300 Subject: [PATCH 05/55] feat(application): interactor for neighbour graph traversal --- src/prodik/application/errors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/prodik/application/errors.py b/src/prodik/application/errors.py index 6c7ac9d..7b483a8 100644 --- a/src/prodik/application/errors.py +++ b/src/prodik/application/errors.py @@ -89,3 +89,6 @@ class OfferNotFoundError(ApplicationError): ... class OfferLinkNotFoundError(ApplicationError): ... + + +class DepthCannotBeNegativeError(ApplicationError): ... From a396ac293f5de8e372835ee6237a87f8047244cc Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 14 Jun 2026 23:03:12 +0300 Subject: [PATCH 06/55] feat(presentation): view for neighbour graph traversal --- src/prodik/presentation/exceptions/handlers.py | 2 ++ src/prodik/presentation/views/node.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/prodik/presentation/exceptions/handlers.py b/src/prodik/presentation/exceptions/handlers.py index 7c5bccf..5b13020 100644 --- a/src/prodik/presentation/exceptions/handlers.py +++ b/src/prodik/presentation/exceptions/handlers.py @@ -13,6 +13,7 @@ CompanyAlreadyExistsError, CompanyNotFoundError, ContextNotFoundError, + DepthCannotBeNegativeError, EdgeAlreadyExistsError, EdgeNotFoundError, FailedToReadClientError, @@ -46,6 +47,7 @@ EXCEPTION_HANDLERS: Final[dict[type[ApplicationError], HTTPStatus]] = { UserAlreadyExistsError: HTTPStatus.CONFLICT, + DepthCannotBeNegativeError: HTTPStatus.UNPROCESSABLE_CONTENT, CompanyAlreadyExistsError: HTTPStatus.CONFLICT, EdgeAlreadyExistsError: HTTPStatus.CONFLICT, InvalidTokenError: HTTPStatus.UNAUTHORIZED, diff --git a/src/prodik/presentation/views/node.py b/src/prodik/presentation/views/node.py index 5753d86..4777e08 100644 --- a/src/prodik/presentation/views/node.py +++ b/src/prodik/presentation/views/node.py @@ -62,12 +62,14 @@ async def create_node( async def get_neighbours( node_id: NodeId, context_id: ContextId, + depth: int, interactor: FromDishka[GetNodeNeighboursInteractor], ) -> list[tuple[NodeSchema, EdgeSchema]]: result = await interactor.execute( GetNodeNeighboursRequestDTO( node_id=node_id, context_id=context_id, + depth=depth, ) ) From 5ced6c859a6f92ea4198b01a3e8ca1f7bdf04f74 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 22 Jun 2026 16:07:42 +0300 Subject: [PATCH 07/55] tests: main environment & basic tests --- pyproject.toml | 8 +- pytest.ini | 2 +- tests/__init__.py | 0 tests/auth/__init__.py | 0 tests/auth/test_login.py | 72 ---- tests/auth/test_refresh.py | 68 ---- tests/auth/test_register.py | 52 --- tests/conftest.py | 334 +----------------- tests/context/__init__.py | 0 tests/context/test_create_context.py | 89 ----- tests/context/test_delete_context.py | 52 --- tests/e2e/general/test_docs.py | 11 + tests/e2e/general/test_status.py | 13 + tests/edge/__init__.py | 0 tests/edge/test_create_edge.py | 126 ------- tests/edge/test_decrement_edge.py | 71 ---- tests/edge/test_get_shortest_path.py | 50 --- tests/edge/test_increment_edge.py | 40 --- tests/edge/test_update_edge_weight.py | 45 --- tests/factories/__init__.py | 53 --- tests/factories/common.py | 6 - tests/factories/models/__init__.py | 24 -- tests/factories/models/company_factory.py | 77 ---- tests/factories/models/context_factory.py | 26 -- tests/factories/models/edge_factory.py | 61 ---- tests/factories/models/group_factory.py | 29 -- .../models/node_association_factory.py | 20 -- tests/factories/models/node_factory.py | 55 --- tests/factories/models/offer_factory.py | 172 --------- tests/factories/models/offer_link_factory.py | 26 -- tests/factories/models/role_factory.py | 68 ---- tests/factories/models/user_factory.py | 88 ----- tests/factories/requests/__init__.py | 35 -- .../requests/auth_request_factory.py | 16 - .../requests/company_request_factory.py | 13 - .../requests/context_request_factory.py | 14 - .../requests/edge_request_factory.py | 23 -- .../requests/node_request_factory.py | 33 -- .../requests/role_request_factory.py | 57 --- tests/node/__init__.py | 0 tests/node/test_attach_node.py | 109 ------ tests/node/test_create_node.py | 91 ----- tests/node/test_delete_node.py | 54 --- tests/node/test_get_neighbours.py | 135 ------- tests/node/test_update_node.py | 65 ---- tests/offer/__init__.py | 0 tests/offer/test_accept_offer_from_company.py | 133 ------- .../offer/test_decline_offer_from_company.py | 170 --------- tests/offer/test_send_offer_to_company.py | 308 ---------------- tests/role/__init__.py | 0 tests/role/test_create_role.py | 38 -- tests/role/test_delete_role.py | 29 -- tests/role/test_update_role.py | 155 -------- tests/root/__init__.py | 0 tests/root/test_ping.py | 11 - tests/services.py | 17 - tests/user/__init__.py | 0 tests/user/test_get_current_user.py | 30 -- tests/user/test_get_user_by_username.py | 50 --- tests/user/test_give_role_to_user.py | 141 -------- tests/user/test_revoke_role_from_user.py | 149 -------- .../user/test_update_current_user_password.py | 78 ---- uv.lock | 143 ++++++++ 63 files changed, 184 insertions(+), 3651 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/auth/__init__.py delete mode 100644 tests/auth/test_login.py delete mode 100644 tests/auth/test_refresh.py delete mode 100644 tests/auth/test_register.py delete mode 100644 tests/context/__init__.py delete mode 100644 tests/context/test_create_context.py delete mode 100644 tests/context/test_delete_context.py create mode 100644 tests/e2e/general/test_docs.py create mode 100644 tests/e2e/general/test_status.py delete mode 100644 tests/edge/__init__.py delete mode 100644 tests/edge/test_create_edge.py delete mode 100644 tests/edge/test_decrement_edge.py delete mode 100644 tests/edge/test_get_shortest_path.py delete mode 100644 tests/edge/test_increment_edge.py delete mode 100644 tests/edge/test_update_edge_weight.py delete mode 100644 tests/factories/__init__.py delete mode 100644 tests/factories/common.py delete mode 100644 tests/factories/models/__init__.py delete mode 100644 tests/factories/models/company_factory.py delete mode 100644 tests/factories/models/context_factory.py delete mode 100644 tests/factories/models/edge_factory.py delete mode 100644 tests/factories/models/group_factory.py delete mode 100644 tests/factories/models/node_association_factory.py delete mode 100644 tests/factories/models/node_factory.py delete mode 100644 tests/factories/models/offer_factory.py delete mode 100644 tests/factories/models/offer_link_factory.py delete mode 100644 tests/factories/models/role_factory.py delete mode 100644 tests/factories/models/user_factory.py delete mode 100644 tests/factories/requests/__init__.py delete mode 100644 tests/factories/requests/auth_request_factory.py delete mode 100644 tests/factories/requests/company_request_factory.py delete mode 100644 tests/factories/requests/context_request_factory.py delete mode 100644 tests/factories/requests/edge_request_factory.py delete mode 100644 tests/factories/requests/node_request_factory.py delete mode 100644 tests/factories/requests/role_request_factory.py delete mode 100644 tests/node/__init__.py delete mode 100644 tests/node/test_attach_node.py delete mode 100644 tests/node/test_create_node.py delete mode 100644 tests/node/test_delete_node.py delete mode 100644 tests/node/test_get_neighbours.py delete mode 100644 tests/node/test_update_node.py delete mode 100644 tests/offer/__init__.py delete mode 100644 tests/offer/test_accept_offer_from_company.py delete mode 100644 tests/offer/test_decline_offer_from_company.py delete mode 100644 tests/offer/test_send_offer_to_company.py delete mode 100644 tests/role/__init__.py delete mode 100644 tests/role/test_create_role.py delete mode 100644 tests/role/test_delete_role.py delete mode 100644 tests/role/test_update_role.py delete mode 100644 tests/root/__init__.py delete mode 100644 tests/root/test_ping.py delete mode 100644 tests/services.py delete mode 100644 tests/user/__init__.py delete mode 100644 tests/user/test_get_current_user.py delete mode 100644 tests/user/test_get_user_by_username.py delete mode 100644 tests/user/test_give_role_to_user.py delete mode 100644 tests/user/test_revoke_role_from_user.py delete mode 100644 tests/user/test_update_current_user_password.py diff --git a/pyproject.toml b/pyproject.toml index 933372e..a17f98f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,8 @@ dependencies = [ "argon2-cffi==25.1.0", "asyncpg==0.31.0", "cryptography==46.0.5", - "dirty-equals==0.11", "dishka==1.7.2", "fastapi==0.128.0", - "httpx==0.28.1", "polyfactory==3.3.0", "pydantic[email]==2.13.2", "pyjwt==2.11.0", @@ -24,10 +22,14 @@ dependencies = [ [project.optional-dependencies] dev = [ + "dirty-equals==0.11", + "httpx==0.28.1", "pytest-asyncio==1.3.0", "pytest", "mypy", "ruff", + "pytest-cov>=7.1.0", + "pytest-xdist>=3.8.0", "datamodel-code-generator==0.54.0", ] @@ -66,7 +68,7 @@ testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] -addopts = "-v --tb=short" +addopts = "--cov-report=term-missing" [tool.mypy] diff --git a/pytest.ini b/pytest.ini index d280de0..2f4c80e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -asyncio_mode = auto \ No newline at end of file +asyncio_mode = auto diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/auth/__init__.py b/tests/auth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/auth/test_login.py b/tests/auth/test_login.py deleted file mode 100644 index c8e21e4..0000000 --- a/tests/auth/test_login.py +++ /dev/null @@ -1,72 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import UserFactory - - -@pytest.mark.asyncio -async def test_login_ok( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.post( - "/auth/login", - json={ - "email": factory_result.user.email.value, - "password": factory_result.password, - }, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - access_token=IsStr(), - refresh_token=IsStr(), - expires_in=IsInt(), - ) - - -@pytest.mark.asyncio -async def test_login_invalid_credentials( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.post( - "/auth/login", - json={ - "email": factory_result.user.email.value, - "password": "TotallyInvalidPassword123456789", - }, - ) - - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Invalid email or password", - meta=[ - {"key": "email", "value": factory_result.user.email.value}, - {"key": "password", "value": "TotallyInvalidPassword123456789"}, - ], - ) - - -@pytest.mark.asyncio -async def test_login_user_not_found(test_client: AsyncClient) -> None: - response = await test_client.post( - "/auth/login", - json={ - "email": "unknown-user@conode.team", - "password": "TotallyInvalidPassword123456789", - }, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="User not found", - meta=[{"key": "email", "value": "unknown-user@conode.team"}], - ) diff --git a/tests/auth/test_refresh.py b/tests/auth/test_refresh.py deleted file mode 100644 index 444aad6..0000000 --- a/tests/auth/test_refresh.py +++ /dev/null @@ -1,68 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import UserFactory - - -@pytest.mark.asyncio -async def test_refresh_ok( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.post( - "/auth/refresh", - json={"refresh_token": factory_result.refresh_token}, - headers={"Authorization": f"Bearer {factory_result.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - access_token=IsStr(), - refresh_token=IsStr(), - expires_in=IsInt(), - ) - - -@pytest.mark.asyncio -async def test_refresh_session_not_found( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - invalid_refresh_token = "totally-invalid-refresh-token" # noqa: S105 - - response = await test_client.post( - "/auth/refresh", - json={"refresh_token": invalid_refresh_token}, - headers={"Authorization": f"Bearer {factory_result.access_token}"}, - ) - - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Session not found", - meta=[{"key": "refresh_token", "value": invalid_refresh_token}], - ) - - -@pytest.mark.asyncio -async def test_refresh_invalid_access_token( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.post( - "/auth/refresh", - json={"refresh_token": factory_result.refresh_token}, - headers={"Authorization": "Bearer totally-invalid-access-token"}, - ) - - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Invalid authorization token", - ) diff --git a/tests/auth/test_register.py b/tests/auth/test_register.py deleted file mode 100644 index 547e5a3..0000000 --- a/tests/auth/test_register.py +++ /dev/null @@ -1,52 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr -from httpx import AsyncClient - -from prodik.application.interfaces.repositories import UserRepository -from prodik.domain.user import Email -from tests.factories import RegisterRequestFactory, UserFactory -from tests.services import EntityExistenceService - - -@pytest.mark.asyncio -async def test_register_ok( - test_client: AsyncClient, - user_repository: UserRepository, - entity_existence_service: EntityExistenceService, -) -> None: - request = RegisterRequestFactory.build().model_dump() - response = await test_client.post("/auth/register", json=request) - - assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( - access_token=IsStr(), - refresh_token=IsStr(), - expires_in=IsInt(), - ) - user = await user_repository.get_by_email(Email(request["email"])) - assert user is not None - assert await entity_existence_service.exists(user) is True - - -@pytest.mark.asyncio -async def test_register_user_already_exists( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - request = RegisterRequestFactory.build().model_dump() - - request.update({"email": factory_result.user.email.value}) - - response = await test_client.post("/auth/register", json=request) - - assert response.status_code == HTTPStatus.CONFLICT - assert response.json() == IsPartialDict( - detail="User with this username or email already exists", - meta=[ - {"key": "email", "value": request["email"]}, - {"key": "username", "value": request["username"]}, - ], - ) diff --git a/tests/conftest.py b/tests/conftest.py index 741b17c..7e544fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,332 +1,20 @@ -from collections.abc import AsyncGenerator -from typing import cast -from unittest.mock import AsyncMock +from typing import AsyncGenerator import pytest -from dishka import AsyncContainer, Provider, Scope, make_async_container, provide -from dishka.integrations.fastapi import FastapiProvider, setup_dishka -from httpx import ASGITransport, AsyncClient -from redis.asyncio import Redis -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine - -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - CompanyRepository, - ContextRepository, - EdgeRepository, - GroupRepository, - LocalAuthorizationRepository, - NodeAssociationRepository, - NodeRepository, - OfferContextRepository, - OfferGroupRepository, - OfferLinkRepository, - OfferRepository, - RolePermissionsRepository, - RoleRepository, - SessionRepository, - UserGrantRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.bootstrap.api import create_app -from prodik.bootstrap.di.providers import ( - ApplicationProvider, - CacheConnectionProvider, - InfrastructureProvider, -) -from prodik.infrastructure.config import ( - APIConfig, - CacheConfig, - Config, - DatabaseConfig, - SecretsConfig, - load_config, -) -from prodik.infrastructure.persistence import start_mapper -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - GroupFactory, - NodeAssociationFactory, - NodeFactory, - OfferFactory, - OfferLinkFactory, - RoleFactory, - UserFactory, -) -from tests.services import EntityExistenceService +from httpx import AsyncClient, ASGITransport +from prodik.bootstrap.api.run import create_app +from prodik.infrastructure.config import Config, load_config @pytest.fixture(scope="session") -def test_config() -> Config: +def config() -> Config: return load_config("test.config.toml") - -@pytest.fixture(scope="session", autouse=True) -def startup() -> None: - start_mapper() - - -@pytest.fixture(autouse=True) -async def clean_redis(test_container: AsyncContainer) -> AsyncGenerator[None]: - yield - redis = await test_container.get(Redis) - await redis.flushdb() - - -@pytest.fixture -async def user_factory(test_container: AsyncContainer) -> UserFactory: - async with test_container() as container: - return UserFactory( - password_hasher=await container.get(PasswordHasher), - access_token_manager=await container.get(AccessTokenManager), - refresh_token_manager=await container.get(RefreshTokenManager), - user_repository=await container.get(UserRepository), - session_repository=await container.get(SessionRepository), - authorization_repository=await container.get(LocalAuthorizationRepository), - ) - - -@pytest.fixture -async def company_factory( - test_container: AsyncContainer, - user_factory: UserFactory, -) -> CompanyFactory: - async with test_container() as container: - return CompanyFactory( - role_repository=await container.get(RoleRepository), - role_permissions_repository=await container.get(RolePermissionsRepository), - user_grant_repository=await container.get(UserGrantRepository), - company_repository=await container.get(CompanyRepository), - user_factory=user_factory, - ) - - -@pytest.fixture -async def role_factory( - test_container: AsyncContainer, -) -> RoleFactory: - async with test_container() as container: - return RoleFactory( - role_repository=await container.get(RoleRepository), - role_permissions_repository=await container.get(RolePermissionsRepository), - ) - - -@pytest.fixture -async def user_repository(test_container: AsyncContainer) -> UserRepository: - async with test_container() as container: - return cast("UserRepository", await container.get(UserRepository)) - - -@pytest.fixture -async def user_grant_repository(test_container: AsyncContainer) -> UserGrantRepository: - async with test_container() as container: - return cast("UserGrantRepository", await container.get(UserGrantRepository)) - - -@pytest.fixture -async def company_repository(test_container: AsyncContainer) -> CompanyRepository: - async with test_container() as container: - return cast("CompanyRepository", await container.get(CompanyRepository)) - - -@pytest.fixture -async def role_permissions_repository( - test_container: AsyncContainer, -) -> RolePermissionsRepository: - async with test_container() as container: - return cast( - "RolePermissionsRepository", - await container.get(RolePermissionsRepository), - ) - - -@pytest.fixture -async def context_repository(test_container: AsyncContainer) -> ContextRepository: - async with test_container() as container: - return cast("ContextRepository", await container.get(ContextRepository)) - - -@pytest.fixture -async def edge_repository(test_container: AsyncContainer) -> EdgeRepository: - async with test_container() as container: - return cast("EdgeRepository", await container.get(EdgeRepository)) - - -@pytest.fixture -async def group_repository(test_container: AsyncContainer) -> GroupRepository: - async with test_container() as container: - return cast("GroupRepository", await container.get(GroupRepository)) - - -@pytest.fixture -async def offer_link_repository(test_container: AsyncContainer) -> OfferLinkRepository: - async with test_container() as container: - return cast("OfferLinkRepository", await container.get(OfferLinkRepository)) - - -@pytest.fixture -async def group_factory( - test_container: AsyncContainer, - company_factory: CompanyFactory, -) -> GroupFactory: - async with test_container() as container: - return GroupFactory( - group_repository=await container.get(GroupRepository), - company_factory=company_factory, - ) - - -@pytest.fixture -async def offer_link_factory( - test_container: AsyncContainer, -) -> OfferLinkFactory: - async with test_container() as container: - return OfferLinkFactory( - offer_link_repository=await container.get(OfferLinkRepository), - ) - - -@pytest.fixture -async def offer_factory( - test_container: AsyncContainer, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_link_factory: OfferLinkFactory, -) -> OfferFactory: - async with test_container() as container: - return OfferFactory( - offer_link_factory=offer_link_factory, - offer_repository=await container.get(OfferRepository), - offer_group_repository=await container.get(OfferGroupRepository), - offer_context_repository=await container.get(OfferContextRepository), - company_factory=company_factory, - group_factory=group_factory, - context_factory=context_factory, - ) - - -@pytest.fixture -async def context_factory( - test_container: AsyncContainer, - company_factory: CompanyFactory, -) -> ContextFactory: - async with test_container() as container: - return ContextFactory( - context_repository=await container.get(ContextRepository), - company_factory=company_factory, - ) - - -@pytest.fixture -async def edge_factory(test_container: AsyncContainer) -> EdgeFactory: - async with test_container() as container: - return EdgeFactory( - edge_repository=await container.get(EdgeRepository), - ) - - -@pytest.fixture -async def node_factory( - test_container: AsyncContainer, - company_factory: CompanyFactory, - user_factory: UserFactory, - group_factory: GroupFactory, - node_association_factory: NodeAssociationFactory, -) -> NodeFactory: - async with test_container() as container: - return NodeFactory( - node_repository=await container.get(NodeRepository), - user_factory=user_factory, - group_factory=group_factory, - company_factory=company_factory, - node_association_factory=node_association_factory, - ) - - -@pytest.fixture -async def node_association_factory( - test_container: AsyncContainer, -) -> NodeAssociationFactory: - async with test_container() as container: - return NodeAssociationFactory( - node_association_repository=await container.get(NodeAssociationRepository), - ) - - -@pytest.fixture -async def node_association_repository( - test_container: AsyncContainer, -) -> NodeAssociationRepository: - async with test_container() as container: - return cast( - "NodeAssociationRepository", - await container.get(NodeAssociationRepository), - ) - - -@pytest.fixture -async def entity_existence_service( - test_session: AsyncSession, -) -> EntityExistenceService: - return EntityExistenceService(session=test_session) - - -@pytest.fixture -async def test_session(test_config: Config) -> AsyncGenerator[AsyncSession]: - engine = create_async_engine(test_config.database.url) - async with engine.begin() as conn, AsyncSession(conn) as session: - session.commit = AsyncMock() # type: ignore - yield session - await session.rollback() - - -@pytest.fixture -async def test_container( - test_session: AsyncSession, - test_config: Config, -) -> AsyncGenerator[AsyncContainer]: - class TestConnectionProvider(Provider): - @provide(scope=Scope.REQUEST) - async def session(self) -> AsyncSession: - return test_session - - container = make_async_container( - FastapiProvider(), - InfrastructureProvider(), - CacheConnectionProvider(), - ApplicationProvider(), - TestConnectionProvider(), - context={ - APIConfig: test_config.api, - DatabaseConfig: test_config.database, - SecretsConfig: test_config.secrets, - CacheConfig: test_config.cache, - }, - ) - - yield container - - await container.close() - - @pytest.fixture -async def test_client( - test_config: Config, - test_container: AsyncContainer, -) -> AsyncGenerator[AsyncClient]: - app = create_app(test_config) - setup_dishka(app=app, container=test_container) +async def transport(config: Config) -> AsyncClient: + app = create_app(config) - async with AsyncClient( - base_url="http://test.localhost.com", - transport=ASGITransport(app), - ) as client: - yield client + return AsyncClient( + base_url="http://test.environment.org", + transport=ASGITransport(app) + ) \ No newline at end of file diff --git a/tests/context/__init__.py b/tests/context/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/context/test_create_context.py b/tests/context/test_create_context.py deleted file mode 100644 index e4db7e6..0000000 --- a/tests/context/test_create_context.py +++ /dev/null @@ -1,89 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - CreateContextRequestFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_create_context_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - - request = CreateContextRequestFactory.build( - company_id=company.id, - ) - - response = await test_client.post( - "/contexts/", - json=request.model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( - id=IsStr(), - name=request.name, - description=request.description, - company_id=str(company.id), - ) - - -@pytest.mark.asyncio -async def test_create_context_company_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - fake_company_id = uuid4() - - response = await test_client.post( - "/contexts/", - json=CreateContextRequestFactory.build( - company_id=fake_company_id, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Company not found", - meta=[{"key": "company_id", "value": str(fake_company_id)}], - ) - - -@pytest.mark.asyncio -async def test_create_context_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - company = await company_factory.create_company() - - user_factory_response = await user_factory.create_user() - - response = await test_client.post( - "/contexts/", - json=CreateContextRequestFactory.build( - name="abcde", - company_id=company.id, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/context/test_delete_context.py b/tests/context/test_delete_context.py deleted file mode 100644 index fec19ca..0000000 --- a/tests/context/test_delete_context.py +++ /dev/null @@ -1,52 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_delete_context_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - context = await context_factory.create_context(company) - - response = await test_client.delete( - f"/contexts/{context.id}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_delete_context_forbidden( - user_factory: UserFactory, - test_client: AsyncClient, - context_factory: ContextFactory, -) -> None: - context = await context_factory.create_context() - - user_factory_response = await user_factory.create_user() - - response = await test_client.delete( - f"/contexts/{context.id}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/e2e/general/test_docs.py b/tests/e2e/general/test_docs.py new file mode 100644 index 0000000..9538d6b --- /dev/null +++ b/tests/e2e/general/test_docs.py @@ -0,0 +1,11 @@ +from http import HTTPStatus + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_docs_availability(transport: AsyncClient) -> None: + response = await transport.get("/docs") + + assert response.status_code == HTTPStatus.OK \ No newline at end of file diff --git a/tests/e2e/general/test_status.py b/tests/e2e/general/test_status.py new file mode 100644 index 0000000..bc8d827 --- /dev/null +++ b/tests/e2e/general/test_status.py @@ -0,0 +1,13 @@ +from http import HTTPStatus + +from httpx import AsyncClient +from dirty_equals import IsPartialDict + + +async def test_api_status(transport: AsyncClient) -> None: + response = await transport.get("/") + + assert response.status_code == HTTPStatus.OK + assert response.json() == IsPartialDict( + status=HTTPStatus.OK + ) \ No newline at end of file diff --git a/tests/edge/__init__.py b/tests/edge/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/edge/test_create_edge.py b/tests/edge/test_create_edge.py deleted file mode 100644 index 68fa1f5..0000000 --- a/tests/edge/test_create_edge.py +++ /dev/null @@ -1,126 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - CreateEdgeRequestFactory, - GroupFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_create_edge_ok( - user_factory: UserFactory, - context_factory: ContextFactory, - company_factory: CompanyFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - context = await context_factory.create_context(company=company) - nodes = [await node_factory.create_node(company=company) for _ in range(2)] - - response = await test_client.post( - "/edges/", - json=CreateEdgeRequestFactory.build( - node_a_id=nodes[0].id, - node_b_id=nodes[1].id, - context_id=context.id, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( - id=IsStr(), - node_a_id=str(nodes[0].id), - node_b_id=str(nodes[1].id), - context_id=str(context.id), - company_id=str(company.id), - ) - - -@pytest.mark.asyncio -async def test_create_edge_context_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - group = await group_factory.create_group(company=company) - nodes = [ - await node_factory.create_node( - user=user_factory_response.user, - company=company, - group=group, - ) - for _ in range(2) - ] - fake_context_id = uuid4() - - response = await test_client.post( - "/edges/", - json=CreateEdgeRequestFactory.build( - node_a_id=nodes[0].id, - node_b_id=nodes[1].id, - context_id=fake_context_id, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Context not found", - meta=[{"key": "context_id", "value": str(fake_context_id)}], - ) - - -@pytest.mark.asyncio -async def test_create_edge_context_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - node_factory: NodeFactory, - context_factory: ContextFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - another_company = await company_factory.create_company() - context = await context_factory.create_context(company=another_company) - group = await group_factory.create_group(company=company) - nodes = [ - await node_factory.create_node( - user=user_factory_response.user, - group=group, - company=company, - ) - for _ in range(2) - ] - - response = await test_client.post( - "/edges/", - json=CreateEdgeRequestFactory.build( - node_a_id=nodes[0].id, - node_b_id=nodes[1].id, - context_id=context.id, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/edge/test_decrement_edge.py b/tests/edge/test_decrement_edge.py deleted file mode 100644 index 195c98a..0000000 --- a/tests/edge/test_decrement_edge.py +++ /dev/null @@ -1,71 +0,0 @@ -import random -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_decrement_edge_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - nodes = [await node_factory.create_node(company=company) for _ in range(2)] - context = await context_factory.create_context(company=company) - edge = await edge_factory.create_edge( - node_a=nodes[0], - node_b=nodes[1], - company=company, - context=context, - weight=random.randint(1, 100), - ) - - response = await test_client.patch( - f"/edges/{edge.id}/weight/decrement", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_decrement_edge_weight_cannot_be_negative( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - nodes = [await node_factory.create_node(company=company) for _ in range(2)] - context = await context_factory.create_context(company=company) - edge = await edge_factory.create_edge( - node_a=nodes[0], node_b=nodes[1], company=company, context=context, weight=0 - ) - - response = await test_client.patch( - f"/edges/{edge.id}/weight/decrement", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.UNPROCESSABLE_CONTENT - assert response.json() == IsPartialDict( - detail="Edge weight cannot be negative", meta=None - ) diff --git a/tests/edge/test_get_shortest_path.py b/tests/edge/test_get_shortest_path.py deleted file mode 100644 index 74e36a8..0000000 --- a/tests/edge/test_get_shortest_path.py +++ /dev/null @@ -1,50 +0,0 @@ -from http import HTTPStatus -from random import choice - -import pytest -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - GroupFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_get_shortest_path_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - group_factory: GroupFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - group = await group_factory.create_group(company) - nodes = [ - await node_factory.create_node( - company=company, group=group, user=user_factory_response.user - ) - for _ in range(4) - ] - context = await context_factory.create_context(company=company) - - await edge_factory.generate_random_graph(nodes, company, context, 1.0) - - response = await test_client.get( - "/edges/shortest", - params={ - "from_node_id": str(choice(nodes).id), - "to_node_id": str(choice(nodes).id), - "context_id": str(context.id), - }, - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK diff --git a/tests/edge/test_increment_edge.py b/tests/edge/test_increment_edge.py deleted file mode 100644 index 6415cc9..0000000 --- a/tests/edge/test_increment_edge.py +++ /dev/null @@ -1,40 +0,0 @@ -from http import HTTPStatus - -import pytest -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_increment_edge_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - nodes = [await node_factory.create_node(company=company) for _ in range(2)] - context = await context_factory.create_context(company=company) - edge = await edge_factory.create_edge( - node_a=nodes[0], - node_b=nodes[1], - company=company, - context=context, - ) - - response = await test_client.patch( - f"/edges/{edge.id}/weight/increment", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT diff --git a/tests/edge/test_update_edge_weight.py b/tests/edge/test_update_edge_weight.py deleted file mode 100644 index 3651707..0000000 --- a/tests/edge/test_update_edge_weight.py +++ /dev/null @@ -1,45 +0,0 @@ -import random -from http import HTTPStatus - -import pytest -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - NodeFactory, - UpdateEdgeWeightRequestFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_update_edge_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - context_factory: ContextFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - nodes = [await node_factory.create_node(company=company) for _ in range(2)] - context = await context_factory.create_context(company=company) - edge = await edge_factory.create_edge( - node_a=nodes[0], - node_b=nodes[1], - company=company, - context=context, - ) - - response = await test_client.patch( - f"/edges/{edge.id}/weight", - json=UpdateEdgeWeightRequestFactory.build( - weight=float(random.randint(10, 100)), - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT diff --git a/tests/factories/__init__.py b/tests/factories/__init__.py deleted file mode 100644 index 596fa99..0000000 --- a/tests/factories/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from tests.factories.common import generate_random_string -from tests.factories.models import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - GroupFactory, - NodeAssociationFactory, - NodeFactory, - OfferFactory, - OfferLinkFactory, - RoleFactory, - UserFactory, - UserFactoryResponse, -) -from tests.factories.requests import ( - AttachNodeRequestFactory, - CreateContextRequestFactory, - CreateEdgeRequestFactory, - CreateNodeRequestFactory, - CreateRoleRequestFactory, - RegisterCompanyRequestFactory, - RegisterRequestFactory, - UpdateEdgeWeightRequestFactory, - UpdateNodeRequestFactory, - UpdatePermissionRequestFactory, - UpdateRoleRequestFactory, -) - -__all__ = [ - "AttachNodeRequestFactory", - "CompanyFactory", - "ContextFactory", - "CreateContextRequestFactory", - "CreateEdgeRequestFactory", - "CreateNodeRequestFactory", - "CreateRoleRequestFactory", - "EdgeFactory", - "GroupFactory", - "NodeAssociationFactory", - "NodeFactory", - "OfferFactory", - "OfferLinkFactory", - "RegisterCompanyRequestFactory", - "RegisterRequestFactory", - "RoleFactory", - "UpdateEdgeWeightRequestFactory", - "UpdateNodeRequestFactory", - "UpdatePermissionRequestFactory", - "UpdateRoleRequestFactory", - "UserFactory", - "UserFactoryResponse", - "generate_random_string", -] diff --git a/tests/factories/common.py b/tests/factories/common.py deleted file mode 100644 index 4c5a14d..0000000 --- a/tests/factories/common.py +++ /dev/null @@ -1,6 +0,0 @@ -import random -import string - - -def generate_random_string(length: int = 5) -> str: - return "".join(random.choices(string.ascii_lowercase, k=length)) diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py deleted file mode 100644 index f852cf1..0000000 --- a/tests/factories/models/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from tests.factories.models.company_factory import CompanyFactory -from tests.factories.models.context_factory import ContextFactory -from tests.factories.models.edge_factory import EdgeFactory -from tests.factories.models.group_factory import GroupFactory -from tests.factories.models.node_association_factory import NodeAssociationFactory -from tests.factories.models.node_factory import NodeFactory -from tests.factories.models.offer_factory import OfferFactory -from tests.factories.models.offer_link_factory import OfferLinkFactory -from tests.factories.models.role_factory import RoleFactory -from tests.factories.models.user_factory import UserFactory, UserFactoryResponse - -__all__ = [ - "CompanyFactory", - "ContextFactory", - "EdgeFactory", - "GroupFactory", - "NodeAssociationFactory", - "NodeFactory", - "OfferFactory", - "OfferLinkFactory", - "RoleFactory", - "UserFactory", - "UserFactoryResponse", -] diff --git a/tests/factories/models/company_factory.py b/tests/factories/models/company_factory.py deleted file mode 100644 index 72e6345..0000000 --- a/tests/factories/models/company_factory.py +++ /dev/null @@ -1,77 +0,0 @@ -import asyncio -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import ( - CompanyRepository, - RolePermissionsRepository, - RoleRepository, - UserGrantRepository, -) -from prodik.domain.company import Company, CompanyId -from prodik.domain.grant import UserGrant, UserGrantId -from prodik.domain.role import ( - EntityType, - PermissionType, - Role, - RoleId, - RolePermission, - RolePermissionId, -) -from prodik.domain.user import User -from tests.factories.common import generate_random_string -from tests.factories.models.user_factory import UserFactory - - -@dataclass(slots=True) -class CompanyFactory: - role_repository: RoleRepository - role_permissions_repository: RolePermissionsRepository - user_grant_repository: UserGrantRepository - company_repository: CompanyRepository - user_factory: UserFactory - - async def create_company(self, user: User | None = None) -> Company: - if user is None: - user = (await self.user_factory.create_user(admin=False)).user - - company = Company.new( - company_id=CompanyId(uuid4()), - name=generate_random_string(10), - description=generate_random_string(300), - owner=user, - ) - - role = Role.new(role_id=RoleId(uuid4()), name="owner", company=company) - - permissions = [ - RolePermission.new( - role_permission_id=RolePermissionId(uuid4()), - role=role, - permission=PermissionType.READ, - entity_type=EntityType.COMPANY, - entity_id=company.id, - ), - RolePermission.new( - role_permission_id=RolePermissionId(uuid4()), - role=role, - permission=PermissionType.MODIFY, - entity_type=EntityType.COMPANY, - entity_id=company.id, - ), - ] - - grant = UserGrant.new( - user_grant_id=UserGrantId(uuid4()), - role=role, - user=user, - ) - - await self.company_repository.create(company) - await self.role_repository.create(role) - await asyncio.gather( - self.role_permissions_repository.create_all(permissions), - self.user_grant_repository.create(grant), - ) - - return company diff --git a/tests/factories/models/context_factory.py b/tests/factories/models/context_factory.py deleted file mode 100644 index 63bd67a..0000000 --- a/tests/factories/models/context_factory.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import ContextRepository -from prodik.domain.company import Company -from prodik.domain.context import Context, ContextId -from tests.factories.common import generate_random_string -from tests.factories.models.company_factory import CompanyFactory - - -@dataclass(slots=True) -class ContextFactory: - context_repository: ContextRepository - company_factory: CompanyFactory - - async def create_context(self, company: Company | None = None) -> Context: - context = Context.new( - context_id=ContextId(uuid4()), - name=generate_random_string(10), - description=generate_random_string(30), - company=company or await self.company_factory.create_company(), - ) - - await self.context_repository.create(context) - - return context diff --git a/tests/factories/models/edge_factory.py b/tests/factories/models/edge_factory.py deleted file mode 100644 index e1e7895..0000000 --- a/tests/factories/models/edge_factory.py +++ /dev/null @@ -1,61 +0,0 @@ -from dataclasses import dataclass -from math import ceil -from random import choice, randint -from uuid import uuid4 - -from prodik.application.interfaces.repositories import EdgeRepository -from prodik.domain.company import Company -from prodik.domain.context import Context -from prodik.domain.edge import Edge, EdgeId -from prodik.domain.node import Node - - -@dataclass(slots=True) -class EdgeFactory: - edge_repository: EdgeRepository - - async def create_edge( - self, - node_a: Node, - node_b: Node, - company: Company, - context: Context, - weight: float = 0, - ) -> Edge: - edge = Edge.new( - edge_id=EdgeId(uuid4()), - node_a=node_a, - node_b=node_b, - company=company, - context=context, - weight=weight, - ) - await self.edge_repository.create(edge) - return edge - - async def generate_random_graph( - self, - nodes: list[Node], - company: Company, - context: Context, - coupling: float = 1.0, - ) -> list[Edge]: - edges = [] - - for _ in range(ceil(len(nodes) * 2 * coupling)): - while True: - node_a, node_b = choice(nodes), choice(nodes) - if node_a != node_b: - break - - edges.append( - await self.create_edge( - node_a, - node_b, - company=company, - context=context, - weight=randint(1, 100), - ) - ) - - return edges diff --git a/tests/factories/models/group_factory.py b/tests/factories/models/group_factory.py deleted file mode 100644 index 6cdbd02..0000000 --- a/tests/factories/models/group_factory.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import GroupRepository -from prodik.domain.company import Company -from prodik.domain.group import Group, GroupId -from tests.factories.common import generate_random_string -from tests.factories.models.company_factory import CompanyFactory - - -@dataclass(slots=True) -class GroupFactory: - group_repository: GroupRepository - company_factory: CompanyFactory - - async def create_group( - self, - company: Company | None = None, - parent_group: Group | None = None, - ) -> Group: - group = Group.new( - group_id=GroupId(uuid4()), - name=generate_random_string(10), - description=generate_random_string(30), - company=company or await self.company_factory.create_company(), - parent_group=parent_group, - ) - await self.group_repository.create(group) - return group diff --git a/tests/factories/models/node_association_factory.py b/tests/factories/models/node_association_factory.py deleted file mode 100644 index 185baa6..0000000 --- a/tests/factories/models/node_association_factory.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import NodeAssociationRepository -from prodik.domain.group import Group -from prodik.domain.node import Node, NodeAssociation, NodeAssociationId - - -@dataclass(slots=True) -class NodeAssociationFactory: - node_association_repository: NodeAssociationRepository - - async def create_association(self, node: Node, group: Group) -> NodeAssociation: - association = NodeAssociation.new( - node_association_id=NodeAssociationId(uuid4()), - node=node, - group=group, - ) - await self.node_association_repository.create(association) - return association diff --git a/tests/factories/models/node_factory.py b/tests/factories/models/node_factory.py deleted file mode 100644 index 55c987f..0000000 --- a/tests/factories/models/node_factory.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import NodeRepository -from prodik.domain.company import Company -from prodik.domain.group import Group -from prodik.domain.node import Node, NodeAssociation, NodeId -from prodik.domain.user import User -from tests.factories.common import generate_random_string -from tests.factories.models.company_factory import CompanyFactory -from tests.factories.models.group_factory import GroupFactory -from tests.factories.models.node_association_factory import NodeAssociationFactory -from tests.factories.models.user_factory import UserFactory - - -@dataclass(slots=True) -class NodeFactory: - group_factory: GroupFactory - node_association_factory: NodeAssociationFactory - node_repository: NodeRepository - user_factory: UserFactory - company_factory: CompanyFactory - - async def create_node( - self, - association: NodeAssociation | None = None, - company: Company | None = None, - group: Group | None = None, - user: User | None = None, - ) -> Node: - if company is None: - if user is None: - user = (await self.user_factory.create_user(admin=False)).user - - company = await self.company_factory.create_company(user) - - node = Node.new( - node_id=NodeId(uuid4()), - name=generate_random_string(10), - description=generate_random_string(30), - company=company, - ) - - if group is None: - group = await self.group_factory.create_group(company) - - await self.node_repository.create(node) - - if association is None: - association = await self.node_association_factory.create_association( - node, - group, - ) - - return node diff --git a/tests/factories/models/offer_factory.py b/tests/factories/models/offer_factory.py deleted file mode 100644 index a8cb54d..0000000 --- a/tests/factories/models/offer_factory.py +++ /dev/null @@ -1,172 +0,0 @@ -import asyncio -from dataclasses import dataclass -from datetime import UTC, datetime, timedelta -from uuid import uuid4 - -from prodik.application.interfaces.repositories import ( - OfferContextRepository, - OfferGroupRepository, - OfferRepository, -) -from prodik.domain.company import Company -from prodik.domain.context import ContextId -from prodik.domain.group import GroupId -from prodik.domain.offer import ( - Offer, - OfferContext, - OfferContextId, - OfferGroup, - OfferGroupId, - OfferId, -) -from prodik.domain.role import PermissionType -from tests.factories.common import generate_random_string -from tests.factories.models.company_factory import CompanyFactory -from tests.factories.models.context_factory import ContextFactory -from tests.factories.models.group_factory import GroupFactory -from tests.factories.models.offer_link_factory import OfferLinkFactory - - -@dataclass(slots=True) -class OfferFactory: - offer_repository: OfferRepository - offer_group_repository: OfferGroupRepository - offer_context_repository: OfferContextRepository - - offer_link_factory: OfferLinkFactory - - company_factory: CompanyFactory - group_factory: GroupFactory - context_factory: ContextFactory - - async def create_offer( - self, - *, - from_company: Company | None = None, - to_company: Company | None = None, - from_offer: Offer | None = None, - requires_counteroffer: bool = False, - groups: dict[GroupId, PermissionType | str] | None = None, - contexts: dict[ContextId, PermissionType | str] | None = None, - ) -> Offer: - if from_company is None: - from_company = await self.company_factory.create_company() - - if to_company is None: - to_company = await self.company_factory.create_company() - - offer = Offer.new( - offer_id=OfferId(uuid4()), - title=generate_random_string(15), - description=generate_random_string(100), - from_company=from_company, - to_company=to_company, - from_offer=from_offer, - expires_in=datetime.now(UTC) + timedelta(days=7), - requires_counteroffer=requires_counteroffer, - ) - - await self.offer_repository.create(offer) - - offer_groups: list[OfferGroup] = [] - offer_contexts: list[OfferContext] = [] - - if groups is None: - default_group = await self.group_factory.create_group(company=from_company) - groups = { - default_group.id: PermissionType.READ, - } - - if contexts is None: - default_context = await self.context_factory.create_context( - company=from_company, - ) - contexts = { - default_context.id: PermissionType.READ, - } - - for group_id, permission in groups.items(): - permission_type = ( - PermissionType(permission) - if isinstance(permission, str) - else permission - ) - - offer_groups.append( - OfferGroup( - id=OfferGroupId(uuid4()), - offer_id=offer.id, - group_id=group_id, - permission_type=permission_type, - created_at=datetime.now(UTC), - updated_at=datetime.now(UTC), - ), - ) - - for context_id, permission in contexts.items(): - permission_type = ( - PermissionType(permission) - if isinstance(permission, str) - else permission - ) - - offer_contexts.append( - OfferContext( - id=OfferContextId(uuid4()), - offer_id=offer.id, - context_id=context_id, - permission_type=permission_type, - created_at=datetime.now(UTC), - updated_at=datetime.now(UTC), - ), - ) - - await asyncio.gather( - self.offer_group_repository.create_all(offer_groups), - self.offer_context_repository.create_all(offer_contexts), - ) - - return offer - - async def create_counter_offer( - self, - *, - from_company: Company, - to_company: Company, - from_offer: Offer, - groups: dict[GroupId, PermissionType | str] | None = None, - contexts: dict[ContextId, PermissionType | str] | None = None, - ) -> Offer: - counter_offer = await self.create_offer( - from_company=from_company, - to_company=to_company, - from_offer=from_offer, - requires_counteroffer=True, - groups=groups, - contexts=contexts, - ) - - await self.offer_link_factory.create_link( - request_offer=from_offer, - response_offer=counter_offer, - ) - - return counter_offer - - async def create_counter_offer_without_link( - self, - *, - from_company: Company, - to_company: Company, - from_offer: Offer, - groups: dict[GroupId, PermissionType | str] | None = None, - contexts: dict[ContextId, PermissionType | str] | None = None, - ) -> Offer: - return await self.create_offer( - from_company=from_company, - to_company=to_company, - from_offer=from_offer, - requires_counteroffer=True, - groups=groups, - contexts=contexts, - ) diff --git a/tests/factories/models/offer_link_factory.py b/tests/factories/models/offer_link_factory.py deleted file mode 100644 index 3c4ddce..0000000 --- a/tests/factories/models/offer_link_factory.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import OfferLinkRepository -from prodik.domain.offer import Offer, OfferLink, OfferLinkId - - -@dataclass(slots=True) -class OfferLinkFactory: - offer_link_repository: OfferLinkRepository - - async def create_link( - self, - *, - request_offer: Offer, - response_offer: Offer, - ) -> OfferLink: - offer_link = OfferLink.new( - offer_link_id=OfferLinkId(uuid4()), - request_offer=request_offer, - response_offer=response_offer, - ) - - await self.offer_link_repository.create(offer_link) - - return offer_link diff --git a/tests/factories/models/role_factory.py b/tests/factories/models/role_factory.py deleted file mode 100644 index 7685d0f..0000000 --- a/tests/factories/models/role_factory.py +++ /dev/null @@ -1,68 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.repositories import ( - RolePermissionsRepository, - RoleRepository, -) -from prodik.domain.company import Company -from prodik.domain.role import ( - EntityType, - PermissionType, - Role, - RoleId, - RolePermission, - RolePermissionEntityId, - RolePermissionId, -) -from tests.factories.common import generate_random_string - - -@dataclass(slots=True) -class RoleFactory: - role_repository: RoleRepository - role_permissions_repository: RolePermissionsRepository - - async def create_role( - self, - *, - company: Company, - name: str | None = None, - with_permissions: bool = True, - permission_entity_id: RolePermissionEntityId | None = None, - ) -> Role: - - role = Role.new( - role_id=RoleId(uuid4()), - name=name or generate_random_string(10), - company=company, - ) - - permissions: list[RolePermission] = [] - - if with_permissions: - entity_id = permission_entity_id or company.id - - permissions = [ - RolePermission.new( - role_permission_id=RolePermissionId(uuid4()), - role=role, - permission=PermissionType.READ, - entity_type=EntityType.COMPANY, - entity_id=entity_id, - ), - RolePermission.new( - role_permission_id=RolePermissionId(uuid4()), - role=role, - permission=PermissionType.MODIFY, - entity_type=EntityType.COMPANY, - entity_id=entity_id, - ), - ] - - await self.role_repository.create(role) - - if permissions: - await self.role_permissions_repository.create_all(permissions) - - return role diff --git a/tests/factories/models/user_factory.py b/tests/factories/models/user_factory.py deleted file mode 100644 index 4fc46af..0000000 --- a/tests/factories/models/user_factory.py +++ /dev/null @@ -1,88 +0,0 @@ -import asyncio -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.domain.authorization import ( - LocalAuthorization, - LocalAuthorizationId, - Session, - SessionId, -) -from prodik.domain.user import User, UserId, UserSystemRole -from tests.factories.common import generate_random_string - - -@dataclass -class UserFactoryResponse: - user: User - session: Session - authorization: LocalAuthorization - access_token: str - refresh_token: str - password: str - - -@dataclass(slots=True) -class UserFactory: - password_hasher: PasswordHasher - access_token_manager: AccessTokenManager - refresh_token_manager: RefreshTokenManager - user_repository: UserRepository - session_repository: SessionRepository - authorization_repository: LocalAuthorizationRepository - - async def create_user(self, *, admin: bool = False) -> UserFactoryResponse: - user = User.new( - user_id=UserId(uuid4()), - username=generate_random_string(10), - first_name=generate_random_string(10), - last_name=generate_random_string(10), - email=generate_random_string(10) + "@conode.team", - bio=generate_random_string(100), - ) - - if admin: - user.system_role = UserSystemRole.ADMIN - - password = generate_random_string(10) - hashed_password = self.password_hasher.hash(password) - access_token, _ = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - - authorization = LocalAuthorization.new( - local_authorization_id=LocalAuthorizationId(uuid4()), - password=hashed_password, - user=user, - ) - - session = Session.new( - session_id=SessionId(uuid4()), - user=user, - host="127.0.0.1", - token=refresh_token, - ) - - await self.user_repository.create(user) - await asyncio.gather( - self.session_repository.create(session), - self.authorization_repository.create(authorization), - ) - - return UserFactoryResponse( - user=user, - session=session, - authorization=authorization, - access_token=access_token, - refresh_token=refresh_token, - password=password, - ) diff --git a/tests/factories/requests/__init__.py b/tests/factories/requests/__init__.py deleted file mode 100644 index 63d0947..0000000 --- a/tests/factories/requests/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from tests.factories.requests.auth_request_factory import RegisterRequestFactory -from tests.factories.requests.company_request_factory import ( - RegisterCompanyRequestFactory, -) -from tests.factories.requests.context_request_factory import ( - CreateContextRequestFactory, -) -from tests.factories.requests.edge_request_factory import ( - CreateEdgeRequestFactory, - UpdateEdgeWeightRequestFactory, -) -from tests.factories.requests.node_request_factory import ( - AttachNodeRequestFactory, - CreateNodeRequestFactory, - UpdateNodeRequestFactory, -) -from tests.factories.requests.role_request_factory import ( - CreateRoleRequestFactory, - UpdatePermissionRequestFactory, - UpdateRoleRequestFactory, -) - -__all__ = [ - "AttachNodeRequestFactory", - "CreateContextRequestFactory", - "CreateEdgeRequestFactory", - "CreateNodeRequestFactory", - "CreateRoleRequestFactory", - "RegisterCompanyRequestFactory", - "RegisterRequestFactory", - "UpdateEdgeWeightRequestFactory", - "UpdateNodeRequestFactory", - "UpdatePermissionRequestFactory", - "UpdateRoleRequestFactory", -] diff --git a/tests/factories/requests/auth_request_factory.py b/tests/factories/requests/auth_request_factory.py deleted file mode 100644 index a34b22a..0000000 --- a/tests/factories/requests/auth_request_factory.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel, EmailStr, Field - - -class RegisterRequest(BaseModel): - username: Annotated[str, Field(min_length=5, max_length=30)] - first_name: Annotated[str, Field(min_length=1, max_length=30)] - last_name: Annotated[str, Field(min_length=1, max_length=30)] - password: Annotated[str, Field(min_length=7, max_length=100)] - email: EmailStr - - -class RegisterRequestFactory(ModelFactory[RegisterRequest]): - __model__ = RegisterRequest diff --git a/tests/factories/requests/company_request_factory.py b/tests/factories/requests/company_request_factory.py deleted file mode 100644 index da6ddc4..0000000 --- a/tests/factories/requests/company_request_factory.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel, Field - - -class RegisterCompanyRequest(BaseModel): - name: Annotated[str, Field(min_length=1, max_length=50)] - description: Annotated[str, Field(min_length=20, max_length=3000)] - - -class RegisterCompanyRequestFactory(ModelFactory[RegisterCompanyRequest]): - __model__ = RegisterCompanyRequest diff --git a/tests/factories/requests/context_request_factory.py b/tests/factories/requests/context_request_factory.py deleted file mode 100644 index 02475af..0000000 --- a/tests/factories/requests/context_request_factory.py +++ /dev/null @@ -1,14 +0,0 @@ -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel - -from prodik.domain.company import CompanyId - - -class CreateContextRequest(BaseModel): - name: str - description: str - company_id: CompanyId - - -class CreateContextRequestFactory(ModelFactory[CreateContextRequest]): - __model__ = CreateContextRequest diff --git a/tests/factories/requests/edge_request_factory.py b/tests/factories/requests/edge_request_factory.py deleted file mode 100644 index 63ef58d..0000000 --- a/tests/factories/requests/edge_request_factory.py +++ /dev/null @@ -1,23 +0,0 @@ -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel - -from prodik.domain.context import ContextId -from prodik.domain.node import NodeId - - -class CreateEdgeRequest(BaseModel): - node_a_id: NodeId - node_b_id: NodeId - context_id: ContextId - - -class UpdateEdgeWeightRequest(BaseModel): - weight: float - - -class CreateEdgeRequestFactory(ModelFactory[CreateEdgeRequest]): - __model__ = CreateEdgeRequest - - -class UpdateEdgeWeightRequestFactory(ModelFactory[UpdateEdgeWeightRequest]): - __model__ = UpdateEdgeWeightRequest diff --git a/tests/factories/requests/node_request_factory.py b/tests/factories/requests/node_request_factory.py deleted file mode 100644 index f9cf683..0000000 --- a/tests/factories/requests/node_request_factory.py +++ /dev/null @@ -1,33 +0,0 @@ -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel - -from prodik.domain.group import GroupId -from prodik.domain.node import NodeId - - -class CreateNodeRequest(BaseModel): - name: str - description: str - group_id: GroupId - - -class UpdateNodeRequest(BaseModel): - name: str - description: str - - -class AttachNodeRequest(BaseModel): - group_id: GroupId - nodes: list[NodeId] - - -class CreateNodeRequestFactory(ModelFactory[CreateNodeRequest]): - __model__ = CreateNodeRequest - - -class UpdateNodeRequestFactory(ModelFactory[UpdateNodeRequest]): - __model__ = UpdateNodeRequest - - -class AttachNodeRequestFactory(ModelFactory[AttachNodeRequest]): - __model__ = AttachNodeRequest diff --git a/tests/factories/requests/role_request_factory.py b/tests/factories/requests/role_request_factory.py deleted file mode 100644 index ce7da8c..0000000 --- a/tests/factories/requests/role_request_factory.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Annotated - -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import BaseModel, Field - -from prodik.domain.company import CompanyId -from prodik.domain.role import ( - EntityType, - PermissionType, - RolePermissionEntityId, - RolePermissionId, -) -from prodik.domain.role.model import ( - MAX_ALLOWED_ROLE_NAME_LENGTH, - MIN_ALLOWED_ROLE_NAME_LENGTH, -) - - -class CreatePermissionRequest(BaseModel): - permission: PermissionType - entity_type: EntityType - entity_id: RolePermissionEntityId - - -class CreateRoleRequest(BaseModel): - name: Annotated[ - str, - Field( - min_length=MIN_ALLOWED_ROLE_NAME_LENGTH, - max_length=MAX_ALLOWED_ROLE_NAME_LENGTH, - ), - ] - company_id: CompanyId - permissions: list[CreatePermissionRequest] - - -class UpdatePermissionRequest(BaseModel): - permission: PermissionType - entity_type: EntityType - entity_id: RolePermissionEntityId - - -class UpdateRoleRequest(BaseModel): - name: str - permissions: dict[RolePermissionId, UpdatePermissionRequest] - - -class CreateRoleRequestFactory(ModelFactory[CreateRoleRequest]): - __model__ = CreateRoleRequest - - -class UpdatePermissionRequestFactory(ModelFactory[UpdatePermissionRequest]): - __model__ = UpdatePermissionRequest - - -class UpdateRoleRequestFactory(ModelFactory[UpdateRoleRequest]): - __model__ = UpdateRoleRequest diff --git a/tests/node/__init__.py b/tests/node/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/node/test_attach_node.py b/tests/node/test_attach_node.py deleted file mode 100644 index b935c81..0000000 --- a/tests/node/test_attach_node.py +++ /dev/null @@ -1,109 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - AttachNodeRequestFactory, - CompanyFactory, - GroupFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_attach_node_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - group = await group_factory.create_group(company=company) - - nodes = [ - str((await node_factory.create_node(company=company)).id) for _ in range(10) - ] - - response = await test_client.post( - "/nodes/attach", - json=AttachNodeRequestFactory.build( - group_id=group.id, - nodes=nodes, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CREATED - assert len(response.json()) == len(nodes) - - -@pytest.mark.asyncio -async def test_attach_node_group_not_found( - user_factory: UserFactory, - test_client: AsyncClient, - company_factory: CompanyFactory, - node_factory: NodeFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - - nodes = [ - str((await node_factory.create_node(company=company)).id) for _ in range(10) - ] - fake_group_id = uuid4() - - response = await test_client.post( - "/nodes/attach", - json=AttachNodeRequestFactory.build( - group_id=fake_group_id, - nodes=nodes, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Group not found", - meta=[{"key": "group_id", "value": str(fake_group_id)}], - ) - - -@pytest.mark.asyncio -async def test_attach_node_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - user_company = await company_factory.create_company(user_factory_response.user) - - another_company = await company_factory.create_company() - another_group = await group_factory.create_group(company=another_company) - - nodes = [ - str((await node_factory.create_node(company=user_company)).id) - for _ in range(10) - ] - - response = await test_client.post( - "/nodes/attach", - json=AttachNodeRequestFactory.build( - group_id=another_group.id, - nodes=nodes, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/node/test_create_node.py b/tests/node/test_create_node.py deleted file mode 100644 index 26c68c2..0000000 --- a/tests/node/test_create_node.py +++ /dev/null @@ -1,91 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - CreateNodeRequestFactory, - GroupFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_create_node_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - group = await group_factory.create_group(company=company) - - request = CreateNodeRequestFactory.build(group_id=group.id) - - response = await test_client.post( - "/nodes/", - json=request.model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( - id=IsStr(), - name=request.name, - description=request.description, - company_id=str(company.id), - ) - - -@pytest.mark.asyncio -async def test_create_node_group_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - fake_group_id = uuid4() - - response = await test_client.post( - "/nodes/", - json=CreateNodeRequestFactory.build(group_id=fake_group_id).model_dump( - mode="json", - ), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Group not found", - meta=[{"key": "group_id", "value": str(fake_group_id)}], - ) - - -@pytest.mark.asyncio -async def test_create_node_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - test_client: AsyncClient, -) -> None: - company = await company_factory.create_company() - group = await group_factory.create_group(company) - - user_factory_response = await user_factory.create_user() - - response = await test_client.post( - "/nodes/", - json=CreateNodeRequestFactory.build(name="abcde", group_id=group.id).model_dump( - mode="json", - ), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/node/test_delete_node.py b/tests/node/test_delete_node.py deleted file mode 100644 index 71a566b..0000000 --- a/tests/node/test_delete_node.py +++ /dev/null @@ -1,54 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - NodeFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_delete_node_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - node = await node_factory.create_node(company=company) - - response = await test_client.delete( - f"/nodes/{node.id}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_delete_node_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - company = await company_factory.create_company() - node = await node_factory.create_node(company=company) - - user_factory_response = await user_factory.create_user(admin=False) - - response = await test_client.delete( - f"/nodes/{node.id}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/node/test_get_neighbours.py b/tests/node/test_get_neighbours.py deleted file mode 100644 index c0e90c3..0000000 --- a/tests/node/test_get_neighbours.py +++ /dev/null @@ -1,135 +0,0 @@ -from http import HTTPStatus -from typing import Final - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - EdgeFactory, - GroupFactory, - NodeFactory, - UserFactory, -) - -DEPTH_TRIGGER: Final = 5 - - -@pytest.mark.asyncio -async def test_get_direct_neighbours_ok( - test_client: AsyncClient, - user_factory: UserFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - company_factory: CompanyFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user_factory_response.user) - group = await group_factory.create_group(company) - context = await context_factory.create_context(company) - - nodes = [ - await node_factory.create_node( - company=company, group=group, user=user_factory_response.user - ) - for _ in range(10) - ] - - for i in range(1, len(nodes)): - await edge_factory.create_edge( - nodes[0], nodes[i], company=company, context=context - ) - - response = await test_client.get( - f"/nodes/{nodes[0].id}/neighbours", - params={ - "context_id": str(context.id), - "depth": 1, - }, - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert len(response.json()) == len(nodes) - 1 - - -@pytest.mark.asyncio -async def test_get_deep_neighbours_ok( - test_client: AsyncClient, - user_factory: UserFactory, - edge_factory: EdgeFactory, - node_factory: NodeFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - company_factory: CompanyFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user_factory_response.user) - group = await group_factory.create_group(company) - context = await context_factory.create_context(company) - - nodes = [ - await node_factory.create_node( - company=company, group=group, user=user_factory_response.user - ) - for _ in range(10) - ] - - cursor = 0 - for i in range(1, len(nodes)): - if i > DEPTH_TRIGGER: - cursor += 1 - - await edge_factory.create_edge( - nodes[cursor], nodes[i], company=company, context=context - ) - - response = await test_client.get( - f"/nodes/{nodes[0].id}/neighbours", - params={ - "context_id": str(context.id), - "depth": 2, - }, - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert len(response.json()) == len(nodes) - - -@pytest.mark.asyncio -async def test_get_neighbours_depth_cannot_be_negative( - test_client: AsyncClient, - user_factory: UserFactory, - node_factory: NodeFactory, - context_factory: ContextFactory, - company_factory: CompanyFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user_factory_response.user) - context = await context_factory.create_context(company) - - node = await node_factory.create_node( - company=company, user=user_factory_response.user - ) - - response = await test_client.get( - f"/nodes/{node.id}/neighbours", - params={ - "context_id": str(context.id), - "depth": -1, - }, - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.UNPROCESSABLE_CONTENT - assert response.json() == IsPartialDict( - detail="Depth cannot be negative", meta=[{"key": "depth", "value": -1}] - ) diff --git a/tests/node/test_update_node.py b/tests/node/test_update_node.py deleted file mode 100644 index 1856058..0000000 --- a/tests/node/test_update_node.py +++ /dev/null @@ -1,65 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - NodeFactory, - UpdateNodeRequestFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_update_node_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user=user_factory_response.user) - node = await node_factory.create_node(company=company) - - request = UpdateNodeRequestFactory.build() - - response = await test_client.put( - f"/nodes/{node.id}", - json=request.model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - id=IsStr(), - name=request.name, - description=request.description, - company_id=str(company.id), - ) - - -@pytest.mark.asyncio -async def test_update_node_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - node_factory: NodeFactory, - test_client: AsyncClient, -) -> None: - company = await company_factory.create_company() - node = await node_factory.create_node(company=company) - - user_factory_response = await user_factory.create_user(admin=False) - - response = await test_client.put( - f"/nodes/{node.id}", - json=UpdateNodeRequestFactory.build(name="abcde").model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/offer/__init__.py b/tests/offer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/offer/test_accept_offer_from_company.py b/tests/offer/test_accept_offer_from_company.py deleted file mode 100644 index bfa2444..0000000 --- a/tests/offer/test_accept_offer_from_company.py +++ /dev/null @@ -1,133 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - GroupFactory, - OfferFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_accept_offer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - recipient_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company(user=recipient_response.user) - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - offer = await offer_factory.create_offer( - from_company=from_company, - to_company=to_company, - groups={group.id: "READ"}, - contexts={context.id: "READ"}, - ) - - response = await test_client.post( - f"/offers/{offer.id}/accept", - headers={"Authorization": f"Bearer {recipient_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_accept_counter_offer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - recipient_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company(user=recipient_response.user) - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - original_offer = await offer_factory.create_offer( - from_company=to_company, - to_company=from_company, - groups={group.id: "READ"}, - contexts={context.id: "READ"}, - ) - counter_offer = await offer_factory.create_counter_offer( - from_company=from_company, - to_company=to_company, - from_offer=original_offer, - groups={group.id: "MODIFY"}, - contexts={context.id: "MODIFY"}, - ) - - response = await test_client.post( - f"/offers/{counter_offer.id}/accept", - headers={"Authorization": f"Bearer {recipient_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_accept_offer_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - user_response = await user_factory.create_user(admin=False) - fake_offer_id = uuid4() - - response = await test_client.post( - f"/offers/{fake_offer_id}/accept", - headers={"Authorization": f"Bearer {user_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Offer not found", - meta=[{"key": "offer_id", "value": str(fake_offer_id)}], - ) - - -@pytest.mark.asyncio -async def test_accept_offer_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - intruder_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - await company_factory.create_company(user=intruder_response.user) - - offer = await offer_factory.create_offer( - from_company=from_company, - to_company=to_company, - ) - - response = await test_client.post( - f"/offers/{offer.id}/accept", - headers={"Authorization": f"Bearer {intruder_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) diff --git a/tests/offer/test_decline_offer_from_company.py b/tests/offer/test_decline_offer_from_company.py deleted file mode 100644 index d996a93..0000000 --- a/tests/offer/test_decline_offer_from_company.py +++ /dev/null @@ -1,170 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - GroupFactory, - OfferFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_decline_offer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - recipient_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company(user=recipient_response.user) - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - offer = await offer_factory.create_offer( - from_company=from_company, - to_company=to_company, - groups={group.id: "READ"}, - contexts={context.id: "READ"}, - ) - - response = await test_client.post( - f"/offers/{offer.id}/decline", - headers={"Authorization": f"Bearer {recipient_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_decline_counter_offer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - recipient_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company(user=recipient_response.user) - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - original_offer = await offer_factory.create_offer( - from_company=to_company, - to_company=from_company, - groups={group.id: "READ"}, - contexts={context.id: "READ"}, - ) - counter_offer = await offer_factory.create_counter_offer( - from_company=from_company, - to_company=to_company, - from_offer=original_offer, - groups={group.id: "MODIFY"}, - contexts={context.id: "MODIFY"}, - ) - - response = await test_client.post( - f"/offers/{counter_offer.id}/decline", - headers={"Authorization": f"Bearer {recipient_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT - - -@pytest.mark.asyncio -async def test_decline_offer_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - user_response = await user_factory.create_user(admin=False) - fake_offer_id = uuid4() - - response = await test_client.post( - f"/offers/{fake_offer_id}/decline", - headers={"Authorization": f"Bearer {user_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Offer not found", - meta=[{"key": "offer_id", "value": str(fake_offer_id)}], - ) - - -@pytest.mark.asyncio -async def test_decline_offer_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - intruder_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - await company_factory.create_company(user=intruder_response.user) - - offer = await offer_factory.create_offer( - from_company=from_company, - to_company=to_company, - ) - - response = await test_client.post( - f"/offers/{offer.id}/decline", - headers={"Authorization": f"Bearer {intruder_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) - - -@pytest.mark.asyncio -async def test_decline_counter_offer_link_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - recipient_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company(user=recipient_response.user) - - original_offer = await offer_factory.create_offer( - from_company=to_company, - to_company=from_company, - ) - counter_offer = await offer_factory.create_counter_offer_without_link( - from_company=from_company, - to_company=to_company, - from_offer=original_offer, - ) - - response = await test_client.post( - f"/offers/{counter_offer.id}/decline", - headers={"Authorization": f"Bearer {recipient_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Offer link not found", - meta=[ - {"key": "to_offer_id", "value": str(counter_offer.id)}, - {"key": "from_offer_id", "value": str(original_offer.id)}, - ], - ) diff --git a/tests/offer/test_send_offer_to_company.py b/tests/offer/test_send_offer_to_company.py deleted file mode 100644 index 02ed0ad..0000000 --- a/tests/offer/test_send_offer_to_company.py +++ /dev/null @@ -1,308 +0,0 @@ -from datetime import UTC, datetime, timedelta -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - ContextFactory, - GroupFactory, - OfferFactory, - UserFactory, -) - - -def _expires_in() -> str: - return (datetime.now(UTC) + timedelta(days=7)).isoformat() - - -@pytest.mark.asyncio -async def test_send_offer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(from_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {str(group.id): "READ"}, - "contexts": {str(context.id): "READ"}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - id=IsStr(), - title="Test Offer", - description="Test description", - status="PENDING", - from_company_id=str(from_company.id), - to_company_id=str(to_company.id), - from_offer=None, - requires_counteroffer=False, - ) - - -@pytest.mark.asyncio -async def test_send_offer_as_counteroffer_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - group_factory: GroupFactory, - context_factory: ContextFactory, - offer_factory: OfferFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - group = await group_factory.create_group(company=from_company) - context = await context_factory.create_context(company=from_company) - - original_offer = await offer_factory.create_offer( - from_company=to_company, - to_company=from_company, - ) - - response = await test_client.post( - "/offers/", - json={ - "title": "Counter Offer", - "description": "Counter description", - "from_company_id": str(from_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": str(original_offer.id), - "groups": {str(group.id): "READ"}, - "contexts": {str(context.id): "MODIFY"}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - status="PENDING", - from_company_id=str(from_company.id), - to_company_id=str(to_company.id), - from_offer=str(original_offer.id), - ) - - -@pytest.mark.asyncio -async def test_send_offer_from_company_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - to_company = await company_factory.create_company() - fake_company_id = uuid4() - - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(fake_company_id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {}, - "contexts": {}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Company not found", - meta=[{"key": "company_id", "value": str(fake_company_id)}], - ) - - -@pytest.mark.asyncio -async def test_send_offer_to_company_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - fake_company_id = uuid4() - - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(from_company.id), - "to_company_id": str(fake_company_id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {}, - "contexts": {}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Company not found", - meta=[{"key": "company_id", "value": str(fake_company_id)}], - ) - - -@pytest.mark.asyncio -async def test_send_offer_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - another_company = await company_factory.create_company() - to_company = await company_factory.create_company() - - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(another_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {}, - "contexts": {}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) - - -@pytest.mark.asyncio -async def test_send_offer_group_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - fake_group_id = uuid4() - - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(from_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {str(fake_group_id): "READ"}, - "contexts": {}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Some of groups not found", - meta=[{"key": "group_ids", "value": [str(fake_group_id)]}], - ) - - -@pytest.mark.asyncio -async def test_send_offer_context_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - - fake_context_id = uuid4() - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(from_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": None, - "groups": {}, - "contexts": {str(fake_context_id): "READ"}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Some of contexts not found", - meta=[{"key": "context_ids", "value": [str(fake_context_id)]}], - ) - - -@pytest.mark.asyncio -async def test_send_offer_from_offer_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - sender_response = await user_factory.create_user(admin=False) - from_company = await company_factory.create_company(user=sender_response.user) - to_company = await company_factory.create_company() - - fake_offer_id = uuid4() - response = await test_client.post( - "/offers/", - json={ - "title": "Test Offer", - "description": "Test description", - "from_company_id": str(from_company.id), - "to_company_id": str(to_company.id), - "requires_counteroffer": False, - "from_offer_id": str(fake_offer_id), - "groups": {}, - "contexts": {}, - "expires_in": _expires_in(), - }, - headers={"Authorization": f"Bearer {sender_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="Offer not found", - meta=[{"key": "offer_id", "value": str(fake_offer_id)}], - ) diff --git a/tests/role/__init__.py b/tests/role/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/role/test_create_role.py b/tests/role/test_create_role.py deleted file mode 100644 index fec28e5..0000000 --- a/tests/role/test_create_role.py +++ /dev/null @@ -1,38 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - CreateRoleRequestFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_create_role_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=True) - company = await company_factory.create_company(user=user_factory_response.user) - - request = CreateRoleRequestFactory.build( - company_id=company.id, - ) - - response = await test_client.post( - "/roles/", - json=request.model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( - id=IsStr(), - owner_company_id=str(company.id), - name=request.name, - ) diff --git a/tests/role/test_delete_role.py b/tests/role/test_delete_role.py deleted file mode 100644 index 7978f99..0000000 --- a/tests/role/test_delete_role.py +++ /dev/null @@ -1,29 +0,0 @@ -from http import HTTPStatus - -import pytest -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - RoleFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_delete_role_ok( - role_factory: RoleFactory, - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - company = await company_factory.create_company(user_factory_response.user) - role = await role_factory.create_role(company=company) - - response = await test_client.delete( - f"/roles/{role.id}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NO_CONTENT diff --git a/tests/role/test_update_role.py b/tests/role/test_update_role.py deleted file mode 100644 index 87facb9..0000000 --- a/tests/role/test_update_role.py +++ /dev/null @@ -1,155 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from prodik.application.interfaces.repositories import RolePermissionsRepository -from prodik.domain.role import EntityType, PermissionType -from tests.factories import ( - CompanyFactory, - RoleFactory, - UpdatePermissionRequestFactory, - UpdateRoleRequestFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_update_role_ok( - role_factory: RoleFactory, - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=user_factory_response.user) - - role = await role_factory.create_role(company=company) - - request = UpdateRoleRequestFactory.build(permissions={}) - - response = await test_client.put( - f"/roles/{role.id}", - json=request.model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - - assert response.json() == IsPartialDict( - role=IsPartialDict( - id=IsStr(), - owner_company_id=str(company.id), - name=request.name, - ), - permissions=[], - ) - - -@pytest.mark.asyncio -async def test_update_role_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - user_factory_response = await user_factory.create_user(admin=True) - fake_role_id = uuid4() - - response = await test_client.put( - f"/roles/{fake_role_id}", - json=UpdateRoleRequestFactory.build(permissions={}).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="Role not found", - meta=[{"key": "role_id", "value": str(fake_role_id)}], - ) - - -@pytest.mark.asyncio -async def test_update_role_forbidden( - role_factory: RoleFactory, - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, -) -> None: - another_company = await company_factory.create_company() - - role = await role_factory.create_role(company=another_company) - - user_factory_response = await user_factory.create_user(admin=False) - - response = await test_client.put( - f"/roles/{role.id}", - json=UpdateRoleRequestFactory.build(permissions={}).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) - - -@pytest.mark.asyncio -async def test_update_role_permissions_ok( - role_factory: RoleFactory, - user_factory: UserFactory, - company_factory: CompanyFactory, - test_client: AsyncClient, - role_permissions_repository: RolePermissionsRepository, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=user_factory_response.user) - - role = await role_factory.create_role(company=company) - - permissions = await role_permissions_repository.get_all_by_role_id(role.id) - - permission = permissions[0] - - response = await test_client.put( - f"/roles/{role.id}", - json=UpdateRoleRequestFactory.build( - name=role.name.value, - permissions={ - permission.id: UpdatePermissionRequestFactory.build( - permission=PermissionType.MODIFY, - entity_type=EntityType.COMPANY, - entity_id=company.id, - ), - }, - ).model_dump(mode="json"), - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - - body = response.json() - - assert body == IsPartialDict( - role=IsPartialDict( - id=str(role.id), - owner_company_id=str(company.id), - name=role.name.value, - ), - ) - - assert len(body["permissions"]) > 0 - - updated_permission = body["permissions"][0] - - assert updated_permission == IsPartialDict( - role_id=str(role.id), - permission=PermissionType.MODIFY, - entity_type=EntityType.COMPANY, - entity_id=str(company.id), - ) diff --git a/tests/root/__init__.py b/tests/root/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/root/test_ping.py b/tests/root/test_ping.py deleted file mode 100644 index 7d3d42a..0000000 --- a/tests/root/test_ping.py +++ /dev/null @@ -1,11 +0,0 @@ -from http import HTTPStatus - -import pytest -from httpx import AsyncClient - - -@pytest.mark.asyncio -async def test_ping(test_client: AsyncClient) -> None: - result = await test_client.get("/") - - assert result.status_code == HTTPStatus.OK diff --git a/tests/services.py b/tests/services.py deleted file mode 100644 index 4d7cb83..0000000 --- a/tests/services.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass -from typing import Any - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - - -@dataclass -class EntityExistenceService: - session: AsyncSession - - async def exists(self, entity: Any) -> bool: - result = await self.session.execute( - select(entity.__class__).where(entity.__class__.id == entity.id), - ) - - return result.scalar_one_or_none() is not None diff --git a/tests/user/__init__.py b/tests/user/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/user/test_get_current_user.py b/tests/user/test_get_current_user.py deleted file mode 100644 index 54c63e1..0000000 --- a/tests/user/test_get_current_user.py +++ /dev/null @@ -1,30 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import UserFactory - - -@pytest.mark.asyncio -async def test_get_current_user_ok( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - - response = await test_client.get( - "/users/me/profile", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - id=IsStr(), - first_name=user_factory_response.user.first_name.value, - last_name=user_factory_response.user.last_name.value, - username=user_factory_response.user.username.value, - email=user_factory_response.user.email.value, - bio=user_factory_response.user.bio.value, - ) diff --git a/tests/user/test_get_user_by_username.py b/tests/user/test_get_user_by_username.py deleted file mode 100644 index ee0284d..0000000 --- a/tests/user/test_get_user_by_username.py +++ /dev/null @@ -1,50 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import UserFactory, generate_random_string - - -@pytest.mark.asyncio -async def test_get_user_by_username_ok( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - users = [await user_factory.create_user(admin=False) for _ in range(2)] - - response = await test_client.get( - f"/users/{users[0].user.username.value}", - headers={"Authorization": f"Bearer {users[1].access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - id=IsStr(), - first_name=users[0].user.first_name.value, - last_name=users[0].user.last_name.value, - username=users[0].user.username.value, - email=users[0].user.email.value, - bio=users[0].user.bio.value, - ) - - -@pytest.mark.asyncio -async def test_get_user_by_username_user_not_found( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - user_factory_response = await user_factory.create_user(admin=False) - unknown_username = generate_random_string(10) - - response = await test_client.get( - f"/users/{unknown_username}", - headers={"Authorization": f"Bearer {user_factory_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.json() == IsPartialDict( - detail="User not found", - meta=[{"key": "username", "value": unknown_username}], - ) diff --git a/tests/user/test_give_role_to_user.py b/tests/user/test_give_role_to_user.py deleted file mode 100644 index a1ea51a..0000000 --- a/tests/user/test_give_role_to_user.py +++ /dev/null @@ -1,141 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - RoleFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_give_role_to_user_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - - target_user = (await user_factory.create_user()).user - - response = await test_client.post( - f"/users/{target_user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - - -@pytest.mark.asyncio -async def test_give_role_to_user_role_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=True) - - target_user = (await user_factory.create_user()).user - fake_role_id = uuid4() - - response = await test_client.post( - f"/users/{target_user.id}/roles/{fake_role_id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="Role not found", - meta=[{"key": "role_id", "value": str(fake_role_id)}], - ) - - -@pytest.mark.asyncio -async def test_give_role_to_user_user_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - fake_user_id = uuid4() - - response = await test_client.post( - f"/users/{fake_user_id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="User not found", - meta=[{"key": "user_id", "value": str(fake_user_id)}], - ) - - -@pytest.mark.asyncio -async def test_give_role_to_user_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - another_company = await company_factory.create_company() - - role = await role_factory.create_role(company=another_company) - - executor_response = await user_factory.create_user(admin=False) - - target_user = (await user_factory.create_user()).user - - response = await test_client.post( - f"/users/{target_user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) - - -@pytest.mark.asyncio -async def test_give_role_to_user_grant_already_exists( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - - response = await test_client.post( - f"/users/{executor_response.user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.CONFLICT - assert response.json() == IsPartialDict( - detail="Grant already exists", - meta=[ - {"key": "user_id", "value": str(executor_response.user.id)}, - {"key": "role_id", "value": str(role.id)}, - ], - ) diff --git a/tests/user/test_revoke_role_from_user.py b/tests/user/test_revoke_role_from_user.py deleted file mode 100644 index 0390c2a..0000000 --- a/tests/user/test_revoke_role_from_user.py +++ /dev/null @@ -1,149 +0,0 @@ -from http import HTTPStatus -from uuid import uuid4 - -import pytest -from dirty_equals import IsPartialDict -from httpx import AsyncClient - -from tests.factories import ( - CompanyFactory, - RoleFactory, - UserFactory, -) - - -@pytest.mark.asyncio -async def test_revoke_role_from_user_ok( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - - target_user_response = await user_factory.create_user() - - await test_client.post( - f"/users/{target_user_response.user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - response = await test_client.delete( - f"/users/{target_user_response.user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - - -@pytest.mark.asyncio -async def test_revoke_role_from_user_role_not_found( - user_factory: UserFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=True) - - target_user = (await user_factory.create_user()).user - fake_role_id = uuid4() - - response = await test_client.delete( - f"/users/{target_user.id}/roles/{fake_role_id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="Role not found", - meta=[{"key": "role_id", "value": str(fake_role_id)}], - ) - - -@pytest.mark.asyncio -async def test_revoke_role_from_user_user_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - fake_user_id = uuid4() - - response = await test_client.delete( - f"/users/{fake_user_id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="User not found", - meta=[{"key": "user_id", "value": str(fake_user_id)}], - ) - - -@pytest.mark.asyncio -async def test_revoke_role_from_user_forbidden( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - another_company = await company_factory.create_company() - - role = await role_factory.create_role(company=another_company) - - executor_response = await user_factory.create_user(admin=False) - - target_user = (await user_factory.create_user()).user - - response = await test_client.delete( - f"/users/{target_user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - - assert response.json() == IsPartialDict( - detail="Not enough rights to perform operation", - meta=None, - ) - - -@pytest.mark.asyncio -async def test_revoke_role_from_user_grant_not_found( - user_factory: UserFactory, - company_factory: CompanyFactory, - role_factory: RoleFactory, - test_client: AsyncClient, -) -> None: - executor_response = await user_factory.create_user(admin=False) - - company = await company_factory.create_company(user=executor_response.user) - - role = await role_factory.create_role(company=company) - - target_user = (await user_factory.create_user()).user - - response = await test_client.delete( - f"/users/{target_user.id}/roles/{role.id}", - headers={"Authorization": f"Bearer {executor_response.access_token}"}, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - assert response.json() == IsPartialDict( - detail="Grant by user and role id not found", - meta=[ - {"key": "user_id", "value": str(target_user.id)}, - {"key": "role_id", "value": str(role.id)}, - ], - ) diff --git a/tests/user/test_update_current_user_password.py b/tests/user/test_update_current_user_password.py deleted file mode 100644 index ae5433a..0000000 --- a/tests/user/test_update_current_user_password.py +++ /dev/null @@ -1,78 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr -from httpx import AsyncClient - -from tests.factories import UserFactory - - -@pytest.mark.asyncio -async def test_update_current_user_password_ok( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.put( - "/users/me/password", - json={ - "old_password": factory_result.password, - "new_password": "NewSuperSecretPassword123456", - }, - headers={"Authorization": f"Bearer {factory_result.access_token}"}, - ) - - assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - access_token=IsStr(), - refresh_token=IsStr(), - expires_in=IsInt(), - ) - - -@pytest.mark.asyncio -async def test_update_current_user_password_invalid_old_password( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.put( - "/users/me/password", - json={ - "old_password": "TOTALYINVALIDPASSWORD12348904567890", - "new_password": "NewSuperSecretPassword123456", - }, - headers={"Authorization": f"Bearer {factory_result.access_token}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( - detail="Invalid old password", - meta=[ - {"key": "old_password", "value": "TOTALYINVALIDPASSWORD12348904567890"}, - ], - ) - - -@pytest.mark.asyncio -async def test_update_current_user_password_invalid_access_token( - test_client: AsyncClient, - user_factory: UserFactory, -) -> None: - factory_result = await user_factory.create_user(admin=False) - - response = await test_client.put( - "/users/me/password", - json={ - "old_password": factory_result.password, - "new_password": "NewSuperSecretPassword123456", - }, - headers={"Authorization": "Bearer totally-invalid-access-token"}, - ) - - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Invalid authorization token", - ) diff --git a/uv.lock b/uv.lock index d36a378..1940900 100644 --- a/uv.lock +++ b/uv.lock @@ -382,6 +382,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/a3/3834a5564fe8f32154cd7032400d3c2f9c565b2a373fa671f2bbdad6f634/coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230", size = 923982, upload-time = "2026-06-20T14:49:30.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7f/551ebe25fa3de95ebbd3528b01ffd672b418e9c521b8555f85fb8aca21f8/coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925", size = 220230, upload-time = "2026-06-20T14:47:09.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ec/a444a1a21b46e54298357977d8ab6c388e5755bc79effaf587808fdb405f/coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2", size = 220750, upload-time = "2026-06-20T14:47:11.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/e5/0dce79f914e31fa0810ab770b06cc638fe5137af259649fafd4daeb2c8e5/coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43", size = 247487, upload-time = "2026-06-20T14:47:12.564Z" }, + { url = "https://files.pythonhosted.org/packages/52/bb/aaca2c75ca6a5da71c3f413ac5920fe9f6e1aad387dae52a3315adf313e0/coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c", size = 249316, upload-time = "2026-06-20T14:47:13.938Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/b45f5edd19ab9f79f7c6a4da2b4d1bd5969e0d7605fe197b60405c7129da/coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8", size = 251182, upload-time = "2026-06-20T14:47:15.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/7c/ad5fd04da4565e7c9ad35080a5fb4762ed2d0f4893ac7eff6a1e7364e79f/coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d", size = 253095, upload-time = "2026-06-20T14:47:16.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/41279b303e8773e1fd9a621a80159c7ed7b643dd9c7e85fd7fc3c88ebdaa/coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99", size = 248203, upload-time = "2026-06-20T14:47:17.758Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/205dddd96954fcf7c7f0b509af614c0edc2a547115ad2fb52a8fc9cbaa41/coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c", size = 249223, upload-time = "2026-06-20T14:47:19.276Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/46c437176338ece41effbbd07d2941378db04b3e618dce68197d59a870b4/coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33", size = 247226, upload-time = "2026-06-20T14:47:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a7/42dfcb471dedb51d366762528e53f232427e8d897b92a9106b4aacec74fc/coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5", size = 251039, upload-time = "2026-06-20T14:47:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/3c/29/987df1a9f8843d3b1f50cb47cca0b10c983e84a3b1b9f6965796f07efa4e/coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044", size = 247497, upload-time = "2026-06-20T14:47:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1c/919b6624c35161d183c43f57fca52ebcc5a59a7b3fa52fe0d0c3067469f5/coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294", size = 248100, upload-time = "2026-06-20T14:47:24.621Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/acbc7b5a6184f92d4875b820477f0bbb3a87371408f5ef73a575bdd3b8e1/coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3", size = 222282, upload-time = "2026-06-20T14:47:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3f/944e24fdb2e88549b58bfd5a51a3a66481cf21154c7aa1a494597c870125/coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6", size = 222910, upload-time = "2026-06-20T14:47:27.331Z" }, + { url = "https://files.pythonhosted.org/packages/04/d5/d0e511247f84fa88ae7da68403cbd3bf9d2a5fc48f5d6618a6846b275632/coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58", size = 220352, upload-time = "2026-06-20T14:47:28.61Z" }, + { url = "https://files.pythonhosted.org/packages/03/4a/ecaff6db72e6c1782ca51336e391393f1e9cc6e4412d6c3da8b7d5075adf/coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924", size = 220855, upload-time = "2026-06-20T14:47:29.972Z" }, + { url = "https://files.pythonhosted.org/packages/34/9a/cf950cd8e8df06ee5941276e69f81647005360421be523d5ca18f658e143/coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5", size = 251276, upload-time = "2026-06-20T14:47:31.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/08/f973be32c9a095e4bb2d3a7bdcb2f9c117e39d4062471ffffae3623f6c51/coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768", size = 253189, upload-time = "2026-06-20T14:47:32.727Z" }, + { url = "https://files.pythonhosted.org/packages/96/aa/f3a50952ba553d442d94b793e5dede25d426b02e5e011e9a9dd225c002d3/coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254", size = 255299, upload-time = "2026-06-20T14:47:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/e0/29/9a4c491986f4d637ed64961ae56721661fc21b6b767d280848d0c708756a/coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93", size = 257255, upload-time = "2026-06-20T14:47:35.397Z" }, + { url = "https://files.pythonhosted.org/packages/dd/61/d2a5b48007f6a212f321c36cf5486feb80505d2d00dfb1163aad2da71197/coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b", size = 251417, upload-time = "2026-06-20T14:47:36.677Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/8df66ae25b401d4529e1d0617af20d9695d171ea4ffec4ca9dffc5dc37b7/coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d", size = 252991, upload-time = "2026-06-20T14:47:38.027Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/16bdc9116dd8bf412a421a7227daa65ad9f12bef0685b13c1bd1c12e6d4c/coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad", size = 251051, upload-time = "2026-06-20T14:47:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f8/b7dbed84274dcc69ddb9c0fe72ec1260830473e0d6c299dcf087a0567f7c/coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8", size = 254817, upload-time = "2026-06-20T14:47:40.995Z" }, + { url = "https://files.pythonhosted.org/packages/c6/07/4659e6bed01a25a0effb4952e8e75fd157038fe5f2829b0f69c6811c2033/coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830", size = 250772, upload-time = "2026-06-20T14:47:42.306Z" }, + { url = "https://files.pythonhosted.org/packages/26/f4/45019da4cd6cd1df3042476447449d62a76a201f6b3556aa40ac31bce20b/coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0", size = 251679, upload-time = "2026-06-20T14:47:43.703Z" }, + { url = "https://files.pythonhosted.org/packages/92/e5/76d75fa2ffe0285d3f2608d1bb241fc245cf98fe614d52118427dd6ccdaa/coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82", size = 222445, upload-time = "2026-06-20T14:47:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/59/696c64547e5c8b9ed31532e9c7a5f9b6474054da93f8ab07f8baf7365c57/coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc", size = 222922, upload-time = "2026-06-20T14:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/646a28100462996c11b98e27d6786cd61f48100d1479804846a3e1e5bf9b/coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a", size = 222468, upload-time = "2026-06-20T14:47:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/bdd141aa2c605096a8ef63b8435fd4f5fec78946a3cb7b9145840ec78291/coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6", size = 220528, upload-time = "2026-06-20T14:47:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/02/97/d24ae7d2afc62c54a36313d4dedb655c9afbba3003f0f7f1ae81e97af31f/coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b", size = 220883, upload-time = "2026-06-20T14:47:51.036Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0e/d8f00efd3df0d63e6843ebcbade9e4119d60f5376753c9705d84b014c775/coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46", size = 252395, upload-time = "2026-06-20T14:47:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1c/ab9510dfe1a16a35a10f90efad0d9a9cf61b9876973752968f2ba882f73f/coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240", size = 255131, upload-time = "2026-06-20T14:47:54.235Z" }, + { url = "https://files.pythonhosted.org/packages/ba/dd/70171e9371003b33dc6b20f527ac216ff91bbe5c1088e754eb8950d79193/coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff", size = 256246, upload-time = "2026-06-20T14:47:55.61Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/a68b1dd81d5c011e17fd6ab0d707d33297df1d0c618114b9b750a2219c80/coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d", size = 258504, upload-time = "2026-06-20T14:47:56.979Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7b/40baaa946189f5317cd77d484e39b9b0727d02ebada0a12162374f2faee2/coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26", size = 252808, upload-time = "2026-06-20T14:47:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/d5/05/b19517b09c43d1e8591de6c13178b0c03166c31e1adbebda378e64c66b9a/coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5", size = 254166, upload-time = "2026-06-20T14:48:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f5/6e65da5957e041d2094a9b97736628dd80160f1cc007a50790bbb2668c1a/coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8", size = 252310, upload-time = "2026-06-20T14:48:01.458Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/01b5274f0db63175b04d9354eff68d2d268b8b57a1b2db7d3dcb1f2c9dbb/coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1", size = 256379, upload-time = "2026-06-20T14:48:02.981Z" }, + { url = "https://files.pythonhosted.org/packages/71/d6/9a2ffbca41e2f8f86f61e8b78b86afa433ec8cdeac4908ace93a28fe3ff0/coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73", size = 251880, upload-time = "2026-06-20T14:48:04.463Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/20bd54a43c88c08f474e6cb355a97e024e38412873ef0a581629abe1e26f/coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e", size = 253753, upload-time = "2026-06-20T14:48:05.99Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/2b3482c30d8344f301d8df6ff232a321f2ab87d5ac97ba21891a68638131/coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7", size = 222584, upload-time = "2026-06-20T14:48:07.361Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5e/83934ffff147edd313fe925db426e8f7ccad9e4663262eb5c4db4e345658/coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5", size = 223118, upload-time = "2026-06-20T14:48:08.837Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ee/616b4f38a34f076f3045d3eedfa764d34d82e6a6cc6b300acb0f1ff22a98/coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5", size = 222504, upload-time = "2026-06-20T14:48:10.436Z" }, + { url = "https://files.pythonhosted.org/packages/6d/09/b5b334c27960e7aac0003b96491bada7838dc641099fa64a1a598abf33cd/coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca", size = 220552, upload-time = "2026-06-20T14:48:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/79/20/879a000c319b4df7b50e4d688c0f7c0f6b5ac9d7b18848cbc00eabf26efe/coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b", size = 220919, upload-time = "2026-06-20T14:48:13.397Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/326dded4371bab60f42215797944a356e4d81a3cee106121c7f7dd531604/coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b", size = 251917, upload-time = "2026-06-20T14:48:14.931Z" }, + { url = "https://files.pythonhosted.org/packages/eb/14/b3232ba218a0d1a70883d2675f18ff465de9e8e5e3346e81dc2b079838bd/coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e", size = 254515, upload-time = "2026-06-20T14:48:16.545Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/d77bcbee1cad71b42776574114b462225cc9125b4982f43da1b66adc850f/coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d", size = 255749, upload-time = "2026-06-20T14:48:18.214Z" }, + { url = "https://files.pythonhosted.org/packages/86/86/97377937b29e9e44a1529bb20cb74dbcf80ed9006d87d7e742ff69e44b67/coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f", size = 257882, upload-time = "2026-06-20T14:48:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a4/0fc8fe68bc505450bb068a2823ac7797bd8495240ccb8b4a5a1da1ee7e62/coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea", size = 252144, upload-time = "2026-06-20T14:48:21.176Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4a/450094ddc41ab0d2eb4a0457b3856400ea3329568d1303696e85de099ae6/coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796", size = 253882, upload-time = "2026-06-20T14:48:22.701Z" }, + { url = "https://files.pythonhosted.org/packages/d0/28/2f6ae6d98265d9aa6bac311c4a93403675905b03aca95dc4373080279d75/coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b", size = 251846, upload-time = "2026-06-20T14:48:24.295Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6e/707281468400794d52874e8fb5e38ff7578a0ff32ed49fe4fe85f192d0fc/coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8", size = 256002, upload-time = "2026-06-20T14:48:26.015Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/5e963120de4011257a950ce4cfb7fc833ddf3fee19db495268d3dec28154/coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb", size = 251665, upload-time = "2026-06-20T14:48:27.654Z" }, + { url = "https://files.pythonhosted.org/packages/e9/78/66b482cd525083bcc0bc894c16db79dabac37490065b53b07d6e8ab77202/coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc", size = 253435, upload-time = "2026-06-20T14:48:29.354Z" }, + { url = "https://files.pythonhosted.org/packages/e6/61/0663fb8cb530c8b11819b920109694eee95a3b22960a9495be0200f657f1/coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548", size = 222591, upload-time = "2026-06-20T14:48:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/1536d2b009c2848c3682500f497053f4645e70911afe02f594000997831a/coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31", size = 223134, upload-time = "2026-06-20T14:48:32.696Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/33ba4f335dd60bb34350318283d784f46018070e67b7d4df7c910ec9d9a0/coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a", size = 222529, upload-time = "2026-06-20T14:48:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bc/120390669817ede714ab141ae0a2a73240fd7354aac992c41dc0bd19570f/coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126", size = 220593, upload-time = "2026-06-20T14:48:35.755Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a3/7f1cfacd76af91e585f7ad689d7168002b444ed2a8ce59f2daaff10089b5/coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428", size = 220925, upload-time = "2026-06-20T14:48:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/e7/10/6514b2525bb672eb8b43703e46d061d694111db21efe7609db722df2233f/coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6", size = 251974, upload-time = "2026-06-20T14:48:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/4533091541c6620ecd68115bbfa1c61265b775618adef3a5fd137f4582e9/coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64", size = 254479, upload-time = "2026-06-20T14:48:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/06/af/e251a143d5d106385dbca696c553afab6b69f7f6bc376a34e089cc0b8b32/coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c", size = 255824, upload-time = "2026-06-20T14:48:42.608Z" }, + { url = "https://files.pythonhosted.org/packages/9c/53/9e5876e60efbaa79d743d1948a5015ddc05b808db1cd62228acf83e87d43/coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499", size = 258139, upload-time = "2026-06-20T14:48:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/85/5a/d35a4f431fb594e46b81cad4a13b470b017e918f347c1c0b260f7494fa1e/coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213", size = 252002, upload-time = "2026-06-20T14:48:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e2/f5b304c8139c606c4f1b230d3a257d0c88edfbbdf06c58364f07625dc45c/coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5", size = 253832, upload-time = "2026-06-20T14:48:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/86/bc/bbbd283daa6be4f68aad4ad4066fd39ae98e4174db8c03ab26c5803d6234/coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4", size = 251799, upload-time = "2026-06-20T14:48:49.42Z" }, + { url = "https://files.pythonhosted.org/packages/69/8d/0745fceb89c9e5f7dd8ed243d97dc8561b7a95545741e2409d2b34654824/coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948", size = 256075, upload-time = "2026-06-20T14:48:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a0/441d9a5255cf021ab41ee00c014a4607d1c72d5e5bef0a4fdaa5be86a907/coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d", size = 251612, upload-time = "2026-06-20T14:48:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/50/37/3d19c5e32d4a529c068eb296abfa3e455bd2c0f9311ecf26280f408ff8e0/coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba", size = 253270, upload-time = "2026-06-20T14:48:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b0/54dd13937297518da6d092cc2c39d9340ec2194bdfa92e0a64694d643e23/coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1", size = 222796, upload-time = "2026-06-20T14:48:56.084Z" }, + { url = "https://files.pythonhosted.org/packages/51/45/7a10e0909919686e335fdd95869cfb222d55243ebff27dc5cf59ca259a1f/coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8", size = 223285, upload-time = "2026-06-20T14:48:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/2e/03/9cb197eb4b3d1a2eccb2537c226a93c80522c5b8afc5dd93e1993d7bb021/coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c", size = 222712, upload-time = "2026-06-20T14:48:59.413Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3c/e59f498511080d20bf866b0af9eeab820feb91547dae2084cb9bb7fb0e58/coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101", size = 221325, upload-time = "2026-06-20T14:49:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/37/8d7955f7e701e69198bd0a0132ea76518c078a635b930a4924e2ccfa70f0/coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0", size = 221594, upload-time = "2026-06-20T14:49:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/34/7a/6738e1e1533ce8ec4e2e472696eefdd4723864d7efaa140e433053bf576a/coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea", size = 262957, upload-time = "2026-06-20T14:49:04.829Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/d1be863cd39e0955904315fece67c5c23e046563f5eea0ceac16c547a759/coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea", size = 265081, upload-time = "2026-06-20T14:49:07.018Z" }, + { url = "https://files.pythonhosted.org/packages/72/7f/412df3c3c251284a11834287fd6f7e3bb98c528c53e030589e9344a3ef80/coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b", size = 267500, upload-time = "2026-06-20T14:49:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/7d0764e83459455384d5c04179ce2d2a837bef01b9ba463079c6e8b31361/coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac", size = 268619, upload-time = "2026-06-20T14:49:10.405Z" }, + { url = "https://files.pythonhosted.org/packages/14/68/1292164ac70cbcc86ac3982da31a6fbb42bb4bcebf6e5cf73c99cfcfd50d/coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28", size = 262066, upload-time = "2026-06-20T14:49:12.257Z" }, + { url = "https://files.pythonhosted.org/packages/20/44/fd6fdf3f63b6e00a1a9230022d072ded5189576001685706aa6524187c65/coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2", size = 264953, upload-time = "2026-06-20T14:49:14.13Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/e803fea3da89eaeb5b6b41b3ccd039fe9f3300a900e3803baac1a998529f/coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512", size = 262555, upload-time = "2026-06-20T14:49:15.803Z" }, + { url = "https://files.pythonhosted.org/packages/32/3c/b360e48ac68e3236c04cb83658382e7f5be7efbbec2e1faae3dcca432783/coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4", size = 266289, upload-time = "2026-06-20T14:49:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/59/12/1ed6d9274d599c586e2d1aa9818765dcdae6bb52aa88afa2fcd868398191/coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1", size = 261402, upload-time = "2026-06-20T14:49:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/44/17/eb6cf12a4538cda937aefbeabb15377a8a30b377b484e63d31c9da790966/coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7", size = 263715, upload-time = "2026-06-20T14:49:21.427Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ca/4bafdb9d372ab05d6ed3a63e7f00d3195d169d0afea00f617c026e386c19/coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a", size = 223103, upload-time = "2026-06-20T14:49:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/35/cb/0765dbd9011d2e47315f1da31e62c5fe231f04a6ec8da213e64c4505896d/coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169", size = 223934, upload-time = "2026-06-20T14:49:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ce/373dde027ecd0ae54511430fe7569f838d3a0376b70333ba9fd20c76b836/coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345", size = 223249, upload-time = "2026-06-20T14:49:27.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/a8ba14ceb014f39bd5e3f7077150718c7de61c01ce326bfe7e8eae9b19b2/coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5", size = 212325, upload-time = "2026-06-20T14:49:28.991Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -518,6 +621,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "faker" version = "40.18.0" @@ -1107,6 +1219,8 @@ dependencies = [ { name = "polyfactory" }, { name = "pydantic", extra = ["email"] }, { name = "pyjwt" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, { name = "redis", extra = ["hiredis"] }, { name = "sqlalchemy" }, { name = "structlog" }, @@ -1139,6 +1253,8 @@ requires-dist = [ { name = "pyjwt", specifier = "==2.11.0" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "redis", extras = ["hiredis"], specifier = "==7.4.0" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "sqlalchemy", specifier = "==2.0.46" }, @@ -1342,6 +1458,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" From bc005cf6606a8481995d3bbeeeea05437b9a4321 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 22 Jun 2026 18:20:34 +0300 Subject: [PATCH 08/55] tests: simple factories and register test --- pyproject.toml | 2 +- tests/__init__.py | 0 tests/conftest.py | 113 ++++++++++++++++++++-- tests/e2e/__init__.py | 0 tests/e2e/authentication/__init__.py | 0 tests/e2e/authentication/test_register.py | 40 ++++++++ tests/e2e/general/__init__.py | 0 tests/e2e/general/test_docs.py | 2 +- tests/e2e/general/test_status.py | 6 +- tests/factories/__init__.py | 0 tests/factories/common.py | 28 ++++++ tests/factories/models/__init__.py | 3 + tests/factories/models/user.py | 85 ++++++++++++++++ tests/factories/schemas/__init__.py | 3 + tests/factories/schemas/register.py | 7 ++ uv.lock | 20 ++-- 16 files changed, 286 insertions(+), 23 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/authentication/__init__.py create mode 100644 tests/e2e/authentication/test_register.py create mode 100644 tests/e2e/general/__init__.py create mode 100644 tests/factories/__init__.py create mode 100644 tests/factories/common.py create mode 100644 tests/factories/models/__init__.py create mode 100644 tests/factories/models/user.py create mode 100644 tests/factories/schemas/__init__.py create mode 100644 tests/factories/schemas/register.py diff --git a/pyproject.toml b/pyproject.toml index a17f98f..f060acc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "cryptography==46.0.5", "dishka==1.7.2", "fastapi==0.128.0", - "polyfactory==3.3.0", "pydantic[email]==2.13.2", "pyjwt==2.11.0", "redis[hiredis]==7.4.0", @@ -22,6 +21,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + "polyfactory==3.3.0", "dirty-equals==0.11", "httpx==0.28.1", "pytest-asyncio==1.3.0", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 7e544fd..b531ff5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,119 @@ -from typing import AsyncGenerator +from collections.abc import AsyncIterator import pytest -from httpx import AsyncClient, ASGITransport +from dishka import AsyncContainer, Provider, Scope, make_async_container, provide +from dishka.integrations.fastapi import FastapiProvider, setup_dishka +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from prodik.application.interfaces.repositories import ( + LocalAuthorizationRepository, + SessionRepository, + UserRepository, +) +from prodik.application.interfaces.token_managers import ( + AccessTokenManager, + RefreshTokenManager, +) +from prodik.application.interfaces.transaction_manager import TransactionManager from prodik.bootstrap.api.run import create_app -from prodik.infrastructure.config import Config, load_config +from prodik.bootstrap.di.providers import ( + ApplicationProvider, + CacheConnectionProvider, + InfrastructureProvider, +) +from prodik.infrastructure.config import ( + APIConfig, + CacheConfig, + Config, + DatabaseConfig, + SecretsConfig, + load_config, +) +from prodik.infrastructure.persistence import start_mapper +from tests.factories.models import UserFactory + @pytest.fixture(scope="session") def config() -> Config: return load_config("test.config.toml") + +@pytest.fixture(scope="session", autouse=True) +def startup() -> None: + start_mapper() + + +@pytest.fixture +async def user_factory(container: AsyncContainer) -> UserFactory: + async with container() as test_container: + return UserFactory( + local_authorization_repository=await test_container.get( + LocalAuthorizationRepository + ), + refresh_token_manager=await test_container.get(RefreshTokenManager), + access_token_manager=await test_container.get(AccessTokenManager), + transaction_manager=await test_container.get(TransactionManager), + session_repository=await test_container.get(SessionRepository), + user_repository=await test_container.get(UserRepository), + ) + + +@pytest.fixture +async def container(config: Config) -> AsyncIterator[AsyncContainer]: + class TestConnectionProvider(Provider): + @provide(scope=Scope.APP) + async def provide_async_engine(self) -> AsyncIterator[AsyncEngine]: + engine = create_async_engine(config.database.url) + yield engine + await engine.dispose() + + @provide(scope=Scope.APP) + def provide_async_sessionmaker( + self, + engine: AsyncEngine, + ) -> async_sessionmaker[AsyncSession]: + return async_sessionmaker( + engine, + expire_on_commit=False, + ) + + @provide(scope=Scope.REQUEST) + async def provide_async_session( + self, + session_factory: async_sessionmaker[AsyncSession], + ) -> AsyncIterator[AsyncSession]: + async with session_factory() as session: + yield session + + container = make_async_container( + FastapiProvider(), + ApplicationProvider(), + TestConnectionProvider(), + InfrastructureProvider(), + CacheConnectionProvider(), + context={ + APIConfig: config.api, + DatabaseConfig: config.database, + SecretsConfig: config.secrets, + CacheConfig: config.cache, + }, + ) + + yield container + await container.close() + + @pytest.fixture -async def transport(config: Config) -> AsyncClient: +async def transport(container: AsyncContainer, config: Config) -> AsyncClient: app = create_app(config) + setup_dishka(container, app) return AsyncClient( - base_url="http://test.environment.org", - transport=ASGITransport(app) - ) \ No newline at end of file + base_url="http://test.environment.org", transport=ASGITransport(app) + ) diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/authentication/__init__.py b/tests/e2e/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py new file mode 100644 index 0000000..e92e4d1 --- /dev/null +++ b/tests/e2e/authentication/test_register.py @@ -0,0 +1,40 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.models import UserFactory +from tests.factories.schemas import RegisterRequestFactory + + +@pytest.mark.asyncio +async def test_register_ok(transport: AsyncClient) -> None: + request = RegisterRequestFactory.build() + + response = await transport.post("/auth/register", json=request.model_dump()) + + assert response.status_code == HTTPStatus.CREATED + + +@pytest.mark.asyncio +async def test_register_when_user_already_exists( + transport: AsyncClient, user_factory: UserFactory +) -> None: + user_factory_response = await user_factory.build() + + request = RegisterRequestFactory.build( + email=user_factory_response.user.email.value, + username=user_factory_response.user.username.value, + ) + + response = await transport.post("/auth/register", json=request.model_dump()) + + assert response.status_code == HTTPStatus.CONFLICT + assert response.json() == IsPartialDict( + detail="User with this username or email already exists", + meta=[ + {"key": "email", "value": user_factory_response.user.email.value}, + {"key": "username", "value": user_factory_response.user.username.value}, + ], + ) diff --git a/tests/e2e/general/__init__.py b/tests/e2e/general/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/general/test_docs.py b/tests/e2e/general/test_docs.py index 9538d6b..04f9717 100644 --- a/tests/e2e/general/test_docs.py +++ b/tests/e2e/general/test_docs.py @@ -8,4 +8,4 @@ async def test_docs_availability(transport: AsyncClient) -> None: response = await transport.get("/docs") - assert response.status_code == HTTPStatus.OK \ No newline at end of file + assert response.status_code == HTTPStatus.OK diff --git a/tests/e2e/general/test_status.py b/tests/e2e/general/test_status.py index bc8d827..33ca480 100644 --- a/tests/e2e/general/test_status.py +++ b/tests/e2e/general/test_status.py @@ -1,13 +1,11 @@ from http import HTTPStatus -from httpx import AsyncClient from dirty_equals import IsPartialDict +from httpx import AsyncClient async def test_api_status(transport: AsyncClient) -> None: response = await transport.get("/") assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( - status=HTTPStatus.OK - ) \ No newline at end of file + assert response.json() == IsPartialDict(status=HTTPStatus.OK) diff --git a/tests/factories/__init__.py b/tests/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/factories/common.py b/tests/factories/common.py new file mode 100644 index 0000000..6409af9 --- /dev/null +++ b/tests/factories/common.py @@ -0,0 +1,28 @@ +import random +import string + + +def generate_random_string( + length: int = 10, + *, + include_lowercase: bool = True, + include_uppercase: bool = True, + include_digits: bool = True, + include_special_symbols: bool = True, +) -> str: + include_range: list[str] = [] + + if include_lowercase: + include_range.extend(string.ascii_lowercase) + if include_uppercase: + include_range.extend(string.ascii_uppercase) + if include_digits: + include_range.extend(string.digits) + if include_special_symbols: + include_range.extend(string.punctuation) + + return "".join([random.choice(include_range) for _ in range(length)]) + + +def generate_random_ip() -> str: + return ".".join([str(random.randint(1, 255)) for _ in range(4)]) diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py new file mode 100644 index 0000000..40e5c5f --- /dev/null +++ b/tests/factories/models/__init__.py @@ -0,0 +1,3 @@ +from .user import UserFactory + +__all__ = ("UserFactory",) diff --git a/tests/factories/models/user.py b/tests/factories/models/user.py new file mode 100644 index 0000000..5da7099 --- /dev/null +++ b/tests/factories/models/user.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import ( + LocalAuthorizationRepository, + SessionRepository, + UserRepository, +) +from prodik.application.interfaces.token_managers import ( + AccessTokenManager, + RefreshTokenManager, +) +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.domain.authorization import ( + LocalAuthorization, + LocalAuthorizationId, + Session, + SessionId, +) +from prodik.domain.user import User, UserId +from tests.factories.common import generate_random_ip, generate_random_string + + +@dataclass +class UserFactoryResponse: + user: User + access_token: str + refresh_token: str + password: str + + +@dataclass +class UserFactory: + local_authorization_repository: LocalAuthorizationRepository + refresh_token_manager: RefreshTokenManager + access_token_manager: AccessTokenManager + transaction_manager: TransactionManager + session_repository: SessionRepository + user_repository: UserRepository + + async def build(self) -> UserFactoryResponse: + async with self.transaction_manager: + user = User.new( + user_id=UserId(uuid4()), + first_name=generate_random_string(), + last_name=generate_random_string(), + username=generate_random_string( + include_digits=False, include_special_symbols=False + ), + email=generate_random_string( + include_digits=False, + include_special_symbols=False, + include_uppercase=False, + ) + + "@testing.org", + bio=generate_random_string(100), + ) + + access_token = self.access_token_manager.encode(user) + refresh_token = self.refresh_token_manager.encode() + password = generate_random_string() + + authorization = LocalAuthorization.new( + local_authorization_id=LocalAuthorizationId(uuid4()), + password=password, + user=user, + ) + + session = Session.new( + session_id=SessionId(uuid4()), + host=generate_random_ip(), + token=refresh_token, + user=user, + ) + + await self.user_repository.create(user) + await self.local_authorization_repository.create(authorization) + await self.session_repository.create(session) + + return UserFactoryResponse( + user=user, + access_token=access_token.token, + refresh_token=refresh_token, + password=password, + ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py new file mode 100644 index 0000000..0f84cda --- /dev/null +++ b/tests/factories/schemas/__init__.py @@ -0,0 +1,3 @@ +from .register import RegisterRequestFactory + +__all__ = ("RegisterRequestFactory",) diff --git a/tests/factories/schemas/register.py b/tests/factories/schemas/register.py new file mode 100644 index 0000000..972624a --- /dev/null +++ b/tests/factories/schemas/register.py @@ -0,0 +1,7 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.auth import RegisterRequest + + +class RegisterRequestFactory(ModelFactory[RegisterRequest]): + __use_examples__ = True diff --git a/uv.lock b/uv.lock index 1940900..010b06b 100644 --- a/uv.lock +++ b/uv.lock @@ -1212,15 +1212,10 @@ dependencies = [ { name = "argon2-cffi" }, { name = "asyncpg" }, { name = "cryptography" }, - { name = "dirty-equals" }, { name = "dishka" }, { name = "fastapi" }, - { name = "httpx" }, - { name = "polyfactory" }, { name = "pydantic", extra = ["email"] }, { name = "pyjwt" }, - { name = "pytest-cov" }, - { name = "pytest-xdist" }, { name = "redis", extra = ["hiredis"] }, { name = "sqlalchemy" }, { name = "structlog" }, @@ -1230,9 +1225,14 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "datamodel-code-generator" }, + { name = "dirty-equals" }, + { name = "httpx" }, { name = "mypy" }, + { name = "polyfactory" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, { name = "ruff" }, ] @@ -1243,18 +1243,18 @@ requires-dist = [ { name = "asyncpg", specifier = "==0.31.0" }, { name = "cryptography", specifier = "==46.0.5" }, { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.54.0" }, - { name = "dirty-equals", specifier = "==0.11" }, + { name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.11" }, { name = "dishka", specifier = "==1.7.2" }, { name = "fastapi", specifier = "==0.128.0" }, - { name = "httpx", specifier = "==0.28.1" }, + { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, { name = "mypy", marker = "extra == 'dev'" }, - { name = "polyfactory", specifier = "==3.3.0" }, + { name = "polyfactory", marker = "extra == 'dev'", specifier = "==3.3.0" }, { name = "pydantic", extras = ["email"], specifier = "==2.13.2" }, { name = "pyjwt", specifier = "==2.11.0" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, - { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, { name = "redis", extras = ["hiredis"], specifier = "==7.4.0" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "sqlalchemy", specifier = "==2.0.46" }, From afe0c1ab66a25f821b0b939662bd7fef5cd072e3 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 22 Jun 2026 18:22:31 +0300 Subject: [PATCH 09/55] tests: simple factories and register test --- tests/e2e/authentication/test_register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py index e92e4d1..53e9469 100644 --- a/tests/e2e/authentication/test_register.py +++ b/tests/e2e/authentication/test_register.py @@ -37,4 +37,4 @@ async def test_register_when_user_already_exists( {"key": "email", "value": user_factory_response.user.email.value}, {"key": "username", "value": user_factory_response.user.username.value}, ], - ) + ) \ No newline at end of file From 0643644acafd275a9029edf587c899b0cc229bfd Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 22 Jun 2026 18:50:44 +0300 Subject: [PATCH 10/55] test: login flow --- .../application/authorization/register.py | 1 + src/prodik/infrastructure/password_hasher.py | 4 +- tests/conftest.py | 2 + tests/e2e/authentication/test_login.py | 50 +++++++++++++++++++ tests/e2e/authentication/test_register.py | 9 +++- tests/factories/models/user.py | 5 +- tests/factories/schemas/__init__.py | 4 +- .../{register.py => authentication.py} | 5 +- 8 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 tests/e2e/authentication/test_login.py rename tests/factories/schemas/{register.py => authentication.py} (53%) diff --git a/src/prodik/application/authorization/register.py b/src/prodik/application/authorization/register.py index 6ae9314..7ed66bd 100644 --- a/src/prodik/application/authorization/register.py +++ b/src/prodik/application/authorization/register.py @@ -57,6 +57,7 @@ async def execute(self, request: RegisterRequestDTO) -> RegisterResponseDTO: username=Username(request.username), email=Email(request.email), ) + if user is not None: raise UserAlreadyExistsError( "User with this username or email already exists", diff --git a/src/prodik/infrastructure/password_hasher.py b/src/prodik/infrastructure/password_hasher.py index da2ec13..22a55cd 100644 --- a/src/prodik/infrastructure/password_hasher.py +++ b/src/prodik/infrastructure/password_hasher.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from argon2 import PasswordHasher as Argon2Hasher -from argon2.exceptions import Argon2Error +from argon2.exceptions import Argon2Error, InvalidHashError from prodik.application.interfaces.password_hasher import PasswordHasher @@ -16,5 +16,5 @@ def hash(self, password: str) -> str: def verify(self, hashed_password: str, password: str) -> bool: try: return self._hasher.verify(hashed_password, password) - except Argon2Error: + except (InvalidHashError, Argon2Error): return False diff --git a/tests/conftest.py b/tests/conftest.py index b531ff5..bbb7e60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ create_async_engine, ) +from prodik.application.interfaces.password_hasher import PasswordHasher from prodik.application.interfaces.repositories import ( LocalAuthorizationRepository, SessionRepository, @@ -60,6 +61,7 @@ async def user_factory(container: AsyncContainer) -> UserFactory: access_token_manager=await test_container.get(AccessTokenManager), transaction_manager=await test_container.get(TransactionManager), session_repository=await test_container.get(SessionRepository), + password_hasher=await test_container.get(PasswordHasher), user_repository=await test_container.get(UserRepository), ) diff --git a/tests/e2e/authentication/test_login.py b/tests/e2e/authentication/test_login.py new file mode 100644 index 0000000..060faac --- /dev/null +++ b/tests/e2e/authentication/test_login.py @@ -0,0 +1,50 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsInt, IsPartialDict, IsStr +from httpx import AsyncClient + +from tests.factories.models import UserFactory +from tests.factories.schemas import LoginRequestFactory + + +@pytest.mark.asyncio +async def test_login_ok(transport: AsyncClient, user_factory: UserFactory) -> None: + user_factory_response = await user_factory.build() + + request = LoginRequestFactory.build( + email=user_factory_response.user.email.value, + password=user_factory_response.password, + ) + + response = await transport.post("/auth/login", json=request.model_dump()) + + assert response.status_code == HTTPStatus.OK + assert response.json() == IsPartialDict( + access_token=IsStr, + refresh_token=IsStr, + expires_in=IsInt, + ) + + +@pytest.mark.asyncio +async def test_login_invalid_password( + transport: AsyncClient, user_factory: UserFactory +) -> None: + user_factory_response = await user_factory.build() + + request = LoginRequestFactory.build( + email=user_factory_response.user.email.value, + password="", + ) + + response = await transport.post("/auth/login", json=request.model_dump()) + + assert response.status_code == HTTPStatus.UNAUTHORIZED + assert response.json() == IsPartialDict( + detail="Invalid email or password", + meta=[ + {"key": "email", "value": request.email}, + {"key": "password", "value": request.password}, + ], + ) diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py index 53e9469..ba343f1 100644 --- a/tests/e2e/authentication/test_register.py +++ b/tests/e2e/authentication/test_register.py @@ -1,7 +1,7 @@ from http import HTTPStatus import pytest -from dirty_equals import IsPartialDict +from dirty_equals import IsInt, IsPartialDict, IsStr from httpx import AsyncClient from tests.factories.models import UserFactory @@ -15,6 +15,11 @@ async def test_register_ok(transport: AsyncClient) -> None: response = await transport.post("/auth/register", json=request.model_dump()) assert response.status_code == HTTPStatus.CREATED + assert response.json() == IsPartialDict( + access_token=IsStr, + refresh_token=IsStr, + expires_in=IsInt, + ) @pytest.mark.asyncio @@ -37,4 +42,4 @@ async def test_register_when_user_already_exists( {"key": "email", "value": user_factory_response.user.email.value}, {"key": "username", "value": user_factory_response.user.username.value}, ], - ) \ No newline at end of file + ) diff --git a/tests/factories/models/user.py b/tests/factories/models/user.py index 5da7099..79a3306 100644 --- a/tests/factories/models/user.py +++ b/tests/factories/models/user.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from uuid import uuid4 +from prodik.application.interfaces.password_hasher import PasswordHasher from prodik.application.interfaces.repositories import ( LocalAuthorizationRepository, SessionRepository, @@ -37,6 +38,7 @@ class UserFactory: transaction_manager: TransactionManager session_repository: SessionRepository user_repository: UserRepository + password_hasher: PasswordHasher async def build(self) -> UserFactoryResponse: async with self.transaction_manager: @@ -59,10 +61,11 @@ async def build(self) -> UserFactoryResponse: access_token = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() password = generate_random_string() + hashed_password = self.password_hasher.hash(password) authorization = LocalAuthorization.new( local_authorization_id=LocalAuthorizationId(uuid4()), - password=password, + password=hashed_password, user=user, ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index 0f84cda..5a7d027 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -1,3 +1,3 @@ -from .register import RegisterRequestFactory +from .authentication import LoginRequestFactory, RegisterRequestFactory -__all__ = ("RegisterRequestFactory",) +__all__ = ("LoginRequestFactory", "RegisterRequestFactory") diff --git a/tests/factories/schemas/register.py b/tests/factories/schemas/authentication.py similarity index 53% rename from tests/factories/schemas/register.py rename to tests/factories/schemas/authentication.py index 972624a..e68cb7c 100644 --- a/tests/factories/schemas/register.py +++ b/tests/factories/schemas/authentication.py @@ -1,7 +1,10 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.auth import RegisterRequest +from prodik.presentation.schemas.auth import LoginRequest, RegisterRequest class RegisterRequestFactory(ModelFactory[RegisterRequest]): __use_examples__ = True + + +class LoginRequestFactory(ModelFactory[LoginRequest]): ... From 35de0cb1c64df76d6d3f5fe76127f08fe529110b Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 22 Jun 2026 19:09:41 +0300 Subject: [PATCH 11/55] tests: refresh token flow --- .../application/authorization/refresh.py | 7 --- .../repositories/authorization/session.py | 2 +- .../repositories/authorization/session.py | 8 ++- .../e2e/authentication/test_refresh_token.py | 55 +++++++++++++++++++ tests/factories/common.py | 4 ++ tests/factories/schemas/__init__.py | 12 +++- tests/factories/schemas/authentication.py | 9 ++- 7 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 tests/e2e/authentication/test_refresh_token.py diff --git a/src/prodik/application/authorization/refresh.py b/src/prodik/application/authorization/refresh.py index fc56070..b3ffacf 100644 --- a/src/prodik/application/authorization/refresh.py +++ b/src/prodik/application/authorization/refresh.py @@ -1,6 +1,5 @@ from dataclasses import dataclass -from prodik.application.errors import SessionNotFoundError from prodik.application.interfaces.identity_provider import IdentityProvider from prodik.application.interfaces.repositories import SessionRepository, UserRepository from prodik.application.interfaces.token_managers import ( @@ -31,13 +30,7 @@ async def execute(self, token: str) -> RefreshTokenResponse: user_meta = self.identity_provider.get_current_user_meta() user = await self.user_repository.get_by_id(user_meta["user_id"]) - session = await self.session_repository.get_by_token(token) - if session is None: - raise SessionNotFoundError( - "Session not found", - [{"key": "refresh_token", "value": token}], - ) access_token, expires_in = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() diff --git a/src/prodik/application/interfaces/repositories/authorization/session.py b/src/prodik/application/interfaces/repositories/authorization/session.py index 364b0f3..3e7ff15 100644 --- a/src/prodik/application/interfaces/repositories/authorization/session.py +++ b/src/prodik/application/interfaces/repositories/authorization/session.py @@ -7,4 +7,4 @@ class SessionRepository(Protocol): async def create(self, session: Session) -> None: ... async def update(self, session: Session) -> None: ... async def get_by_host(self, host: str) -> Session | None: ... - async def get_by_token(self, token: str) -> Session | None: ... + async def get_by_token(self, token: str) -> Session: ... diff --git a/src/prodik/infrastructure/repositories/authorization/session.py b/src/prodik/infrastructure/repositories/authorization/session.py index cf35f7f..c0879c0 100644 --- a/src/prodik/infrastructure/repositories/authorization/session.py +++ b/src/prodik/infrastructure/repositories/authorization/session.py @@ -4,6 +4,7 @@ import structlog from redis.asyncio import Redis +from prodik.application.errors import SessionNotFoundError from prodik.application.interfaces.repositories import SessionRepository from prodik.domain.authorization import Session, SessionId from prodik.domain.user import UserId @@ -49,12 +50,15 @@ async def update(self, session: Session) -> None: }, ) - async def get_by_token(self, token: str) -> Session | None: + async def get_by_token(self, token: str) -> Session: logger.debug("Repository get session by token") data = await self.client.hgetall(f"session:{token}") # type: ignore if not data: - return None + raise SessionNotFoundError( + "Session not found", + [{"key": "refresh_token", "value": token}], + ) session = Session( id=SessionId(data["id"]), diff --git a/tests/e2e/authentication/test_refresh_token.py b/tests/e2e/authentication/test_refresh_token.py new file mode 100644 index 0000000..eb05960 --- /dev/null +++ b/tests/e2e/authentication/test_refresh_token.py @@ -0,0 +1,55 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsInt, IsPartialDict, IsStr +from httpx import AsyncClient + +from tests.factories.common import authorization_header, generate_random_string +from tests.factories.models import UserFactory +from tests.factories.schemas import RefreshTokenRequestFactory + + +@pytest.mark.asyncio +async def test_refresh_token_ok( + transport: AsyncClient, user_factory: UserFactory +) -> None: + user_factory_response = await user_factory.build() + + request = RefreshTokenRequestFactory.build( + refresh_token=user_factory_response.refresh_token + ) + + response = await transport.post( + "/auth/refresh", + headers=authorization_header(user_factory_response.access_token), + json=request.model_dump(), + ) + + assert response.status_code == HTTPStatus.OK + assert response.json() == IsPartialDict( + access_token=IsStr, + refresh_token=IsStr, + expires_in=IsInt, + ) + + +@pytest.mark.asyncio +async def test_refresh_token_without_active_session( + transport: AsyncClient, user_factory: UserFactory +) -> None: + user_factory_response = await user_factory.build() + + refresh_token = generate_random_string() + request = RefreshTokenRequestFactory.build(refresh_token=refresh_token) + + response = await transport.post( + "/auth/refresh", + headers=authorization_header(user_factory_response.access_token), + json=request.model_dump(), + ) + + assert response.status_code == HTTPStatus.UNAUTHORIZED + assert response.json() == IsPartialDict( + detail="Session not found", + meta=[{"key": "refresh_token", "value": refresh_token}], + ) diff --git a/tests/factories/common.py b/tests/factories/common.py index 6409af9..8eb884b 100644 --- a/tests/factories/common.py +++ b/tests/factories/common.py @@ -26,3 +26,7 @@ def generate_random_string( def generate_random_ip() -> str: return ".".join([str(random.randint(1, 255)) for _ in range(4)]) + + +def authorization_header(access_token: str) -> dict[str, str]: + return {"Authorization": f"Bearer {access_token}"} diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index 5a7d027..8cc046a 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -1,3 +1,11 @@ -from .authentication import LoginRequestFactory, RegisterRequestFactory +from .authentication import ( + LoginRequestFactory, + RefreshTokenRequestFactory, + RegisterRequestFactory, +) -__all__ = ("LoginRequestFactory", "RegisterRequestFactory") +__all__ = ( + "LoginRequestFactory", + "RefreshTokenRequestFactory", + "RegisterRequestFactory", +) diff --git a/tests/factories/schemas/authentication.py b/tests/factories/schemas/authentication.py index e68cb7c..4de7f4c 100644 --- a/tests/factories/schemas/authentication.py +++ b/tests/factories/schemas/authentication.py @@ -1,10 +1,17 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.auth import LoginRequest, RegisterRequest +from prodik.presentation.schemas.auth import ( + LoginRequest, + RefreshTokenRequest, + RegisterRequest, +) class RegisterRequestFactory(ModelFactory[RegisterRequest]): __use_examples__ = True +class RefreshTokenRequestFactory(ModelFactory[RefreshTokenRequest]): ... + + class LoginRequestFactory(ModelFactory[LoginRequest]): ... From 961c7d4c4f2e14a9066ba590a030d25fcac21346 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Tue, 23 Jun 2026 15:08:13 +0300 Subject: [PATCH 12/55] tests: register company test --- .../repositories/authorization/session.py | 5 ++ tests/conftest.py | 22 ++++++- .../e2e/authentication/test_refresh_token.py | 6 +- tests/e2e/company/__init__.py | 0 tests/e2e/company/test_register_company.py | 59 +++++++++++++++++ tests/factories/common.py | 2 +- tests/factories/models/__init__.py | 3 +- tests/factories/models/company.py | 63 +++++++++++++++++++ tests/factories/schemas/__init__.py | 2 + tests/factories/schemas/company.py | 7 +++ 10 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/company/__init__.py create mode 100644 tests/e2e/company/test_register_company.py create mode 100644 tests/factories/models/company.py create mode 100644 tests/factories/schemas/company.py diff --git a/src/prodik/infrastructure/repositories/authorization/session.py b/src/prodik/infrastructure/repositories/authorization/session.py index c0879c0..c718744 100644 --- a/src/prodik/infrastructure/repositories/authorization/session.py +++ b/src/prodik/infrastructure/repositories/authorization/session.py @@ -80,6 +80,11 @@ async def get_by_host(self, host: str) -> Session | None: return None data = await self.client.hgetall(f"session:{token}") # type: ignore + if not data: + raise SessionNotFoundError( + "Session not found", + [{"key": "refresh_token", "value": token}], + ) session = Session( id=SessionId(data["id"]), diff --git a/tests/conftest.py b/tests/conftest.py index bbb7e60..9a6d0b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,12 @@ from prodik.application.interfaces.password_hasher import PasswordHasher from prodik.application.interfaces.repositories import ( + CompanyRepository, LocalAuthorizationRepository, + RolePermissionsRepository, + RoleRepository, SessionRepository, + UserGrantRepository, UserRepository, ) from prodik.application.interfaces.token_managers import ( @@ -22,6 +26,7 @@ RefreshTokenManager, ) from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.application.services import RoleManagmentService from prodik.bootstrap.api.run import create_app from prodik.bootstrap.di.providers import ( ApplicationProvider, @@ -37,7 +42,7 @@ load_config, ) from prodik.infrastructure.persistence import start_mapper -from tests.factories.models import UserFactory +from tests.factories.models import CompanyFactory, UserFactory @pytest.fixture(scope="session") @@ -66,6 +71,21 @@ async def user_factory(container: AsyncContainer) -> UserFactory: ) +@pytest.fixture +async def company_factory(container: AsyncContainer) -> CompanyFactory: + async with container() as test_container: + return CompanyFactory( + role_permissions_repository=await test_container.get( + RolePermissionsRepository + ), + role_managment_service=await test_container.get(RoleManagmentService), + user_grant_repository=await test_container.get(UserGrantRepository), + transaction_manager=await test_container.get(TransactionManager), + company_repository=await test_container.get(CompanyRepository), + role_repository=await test_container.get(RoleRepository), + ) + + @pytest.fixture async def container(config: Config) -> AsyncIterator[AsyncContainer]: class TestConnectionProvider(Provider): diff --git a/tests/e2e/authentication/test_refresh_token.py b/tests/e2e/authentication/test_refresh_token.py index eb05960..4df0847 100644 --- a/tests/e2e/authentication/test_refresh_token.py +++ b/tests/e2e/authentication/test_refresh_token.py @@ -4,7 +4,7 @@ from dirty_equals import IsInt, IsPartialDict, IsStr from httpx import AsyncClient -from tests.factories.common import authorization_header, generate_random_string +from tests.factories.common import authorization_headers, generate_random_string from tests.factories.models import UserFactory from tests.factories.schemas import RefreshTokenRequestFactory @@ -21,7 +21,7 @@ async def test_refresh_token_ok( response = await transport.post( "/auth/refresh", - headers=authorization_header(user_factory_response.access_token), + headers=authorization_headers(user_factory_response.access_token), json=request.model_dump(), ) @@ -44,7 +44,7 @@ async def test_refresh_token_without_active_session( response = await transport.post( "/auth/refresh", - headers=authorization_header(user_factory_response.access_token), + headers=authorization_headers(user_factory_response.access_token), json=request.model_dump(), ) diff --git a/tests/e2e/company/__init__.py b/tests/e2e/company/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/company/test_register_company.py b/tests/e2e/company/test_register_company.py new file mode 100644 index 0000000..7392c0d --- /dev/null +++ b/tests/e2e/company/test_register_company.py @@ -0,0 +1,59 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict, IsStr +from httpx import AsyncClient + +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, UserFactory +from tests.factories.schemas import RegisterCompanyRequestFactory + + +@pytest.mark.asyncio +async def test_register_company_ok( + transport: AsyncClient, user_factory: UserFactory +) -> None: + user_factory_response = await user_factory.build() + + request = RegisterCompanyRequestFactory.build() + + response = await transport.post( + "/companies/", + headers=authorization_headers(user_factory_response.access_token), + json=request.model_dump(), + ) + + assert response.status_code == HTTPStatus.CREATED + assert response.json() == IsPartialDict( + id=IsStr, + name=request.name, + owner_id=str(user_factory_response.user.id), + description=request.description, + verified=False, + ) + + +@pytest.mark.asyncio +async def test_register_company_while_same_company_exists( + transport: AsyncClient, user_factory: UserFactory, company_factory: CompanyFactory +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + + request = RegisterCompanyRequestFactory.build( + name=company.name.value, + description=company.description.value, + ) + + response = await transport.post( + "/companies/", + headers=authorization_headers(user_factory_response.access_token), + json=request.model_dump(), + ) + + assert response.status_code == HTTPStatus.CONFLICT + assert response.json() == IsPartialDict( + detail="Company with this name already exists", + meta=[{"key": "name", "value": request.name}], + ) diff --git a/tests/factories/common.py b/tests/factories/common.py index 8eb884b..13e36e0 100644 --- a/tests/factories/common.py +++ b/tests/factories/common.py @@ -28,5 +28,5 @@ def generate_random_ip() -> str: return ".".join([str(random.randint(1, 255)) for _ in range(4)]) -def authorization_header(access_token: str) -> dict[str, str]: +def authorization_headers(access_token: str) -> dict[str, str]: return {"Authorization": f"Bearer {access_token}"} diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py index 40e5c5f..47363d9 100644 --- a/tests/factories/models/__init__.py +++ b/tests/factories/models/__init__.py @@ -1,3 +1,4 @@ +from .company import CompanyFactory from .user import UserFactory -__all__ = ("UserFactory",) +__all__ = ("CompanyFactory", "UserFactory") diff --git a/tests/factories/models/company.py b/tests/factories/models/company.py new file mode 100644 index 0000000..d6584c3 --- /dev/null +++ b/tests/factories/models/company.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import ( + CompanyRepository, + RolePermissionsRepository, + RoleRepository, + UserGrantRepository, +) +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.application.services import RoleManagmentService +from prodik.domain.company import Company, CompanyId +from prodik.domain.role import EntityType, PermissionType +from prodik.domain.user import User +from tests.factories.common import generate_random_string + + +@dataclass +class CompanyFactory: + role_permissions_repository: RolePermissionsRepository + role_managment_service: RoleManagmentService + user_grant_repository: UserGrantRepository + transaction_manager: TransactionManager + company_repository: CompanyRepository + role_repository: RoleRepository + + async def build(self, *, owner: User) -> Company: + async with self.transaction_manager: + company = Company.new( + company_id=CompanyId(uuid4()), + name=generate_random_string( + include_special_symbols=False, include_digits=False + ), + description=generate_random_string(200), + owner=owner, + ) + + role_managment_service_response = ( + self.role_managment_service.create_role_with_permissions( + name="owner", + company=company, + request=[ + ( + company.id, + EntityType.COMPANY, + PermissionType.READ, + ), + ( + company.id, + EntityType.COMPANY, + PermissionType.MODIFY, + ), + ], + ) + ) + + await self.company_repository.create(company) + await self.role_repository.create(role_managment_service_response.role) + await self.role_permissions_repository.create_all( + role_managment_service_response.permissions + ) + + return company diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index 8cc046a..47e212b 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -3,9 +3,11 @@ RefreshTokenRequestFactory, RegisterRequestFactory, ) +from .company import RegisterCompanyRequestFactory __all__ = ( "LoginRequestFactory", "RefreshTokenRequestFactory", + "RegisterCompanyRequestFactory", "RegisterRequestFactory", ) diff --git a/tests/factories/schemas/company.py b/tests/factories/schemas/company.py new file mode 100644 index 0000000..86aa767 --- /dev/null +++ b/tests/factories/schemas/company.py @@ -0,0 +1,7 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.company import RegisterCompanyRequest + + +class RegisterCompanyRequestFactory(ModelFactory[RegisterCompanyRequest]): + __use_examples__ = True From d7138e4db3c1919d08687255cc039e5449ceb2f2 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Wed, 24 Jun 2026 21:55:03 +0300 Subject: [PATCH 13/55] tests: database side effects --- .../authorization/local_authorization.py | 1 + tests/conftest.py | 67 ++++++++++++------- tests/e2e/authentication/test_register.py | 24 ++++++- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/prodik/application/interfaces/repositories/authorization/local_authorization.py b/src/prodik/application/interfaces/repositories/authorization/local_authorization.py index 40ae365..af786f3 100644 --- a/src/prodik/application/interfaces/repositories/authorization/local_authorization.py +++ b/src/prodik/application/interfaces/repositories/authorization/local_authorization.py @@ -8,3 +8,4 @@ class LocalAuthorizationRepository(Protocol): async def create(self, authorization: LocalAuthorization) -> None: ... async def update(self, authorization: LocalAuthorization) -> None: ... async def get_by_user_id(self, user_id: UserId) -> LocalAuthorization | None: ... + async def get_all_by_user_id(self, user_id: UserId) -> list[LocalAuthorization]: ... diff --git a/tests/conftest.py b/tests/conftest.py index 9a6d0b6..aa37248 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,12 @@ from collections.abc import AsyncIterator +from unittest.mock import AsyncMock import pytest from dishka import AsyncContainer, Provider, Scope, make_async_container, provide from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import ( - AsyncEngine, AsyncSession, - async_sessionmaker, create_async_engine, ) @@ -55,6 +54,32 @@ def startup() -> None: start_mapper() +@pytest.fixture +async def session_repository(container: AsyncContainer) -> SessionRepository: + async with container() as test_container: + return await test_container.get(SessionRepository) # type: ignore[no-any-return] + + +@pytest.fixture +async def local_authorization_repository( + container: AsyncContainer, +) -> LocalAuthorizationRepository: + async with container() as test_container: + return await test_container.get(LocalAuthorizationRepository) # type: ignore[no-any-return] + + +@pytest.fixture +async def access_token_manager(container: AsyncContainer) -> AccessTokenManager: + async with container() as test_container: + return await test_container.get(AccessTokenManager) # type: ignore[no-any-return] + + +@pytest.fixture +async def user_repository(container: AsyncContainer) -> UserRepository: + async with container() as test_container: + return await test_container.get(UserRepository) # type: ignore[no-any-return] + + @pytest.fixture async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: @@ -87,31 +112,23 @@ async def company_factory(container: AsyncContainer) -> CompanyFactory: @pytest.fixture -async def container(config: Config) -> AsyncIterator[AsyncContainer]: - class TestConnectionProvider(Provider): - @provide(scope=Scope.APP) - async def provide_async_engine(self) -> AsyncIterator[AsyncEngine]: - engine = create_async_engine(config.database.url) - yield engine - await engine.dispose() - - @provide(scope=Scope.APP) - def provide_async_sessionmaker( - self, - engine: AsyncEngine, - ) -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - engine, - expire_on_commit=False, - ) +async def session(config: Config) -> AsyncIterator[AsyncSession]: + engine = create_async_engine(config.database.url) + + async with AsyncSession(engine) as session: + session.commit = AsyncMock() # type: ignore + yield session + await session.close() + +@pytest.fixture +async def container( + config: Config, session: AsyncSession +) -> AsyncIterator[AsyncContainer]: + class TestConnectionProvider(Provider): @provide(scope=Scope.REQUEST) - async def provide_async_session( - self, - session_factory: async_sessionmaker[AsyncSession], - ) -> AsyncIterator[AsyncSession]: - async with session_factory() as session: - yield session + async def provide_async_session(self) -> AsyncSession: + return session container = make_async_container( FastapiProvider(), diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py index ba343f1..ab97204 100644 --- a/tests/e2e/authentication/test_register.py +++ b/tests/e2e/authentication/test_register.py @@ -1,21 +1,39 @@ from http import HTTPStatus import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr +from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr from httpx import AsyncClient +from prodik.application.interfaces.repositories import ( + SessionRepository, +) +from prodik.application.interfaces.token_managers import AccessTokenManager from tests.factories.models import UserFactory from tests.factories.schemas import RegisterRequestFactory @pytest.mark.asyncio -async def test_register_ok(transport: AsyncClient) -> None: +async def test_register_ok( + transport: AsyncClient, + session_repository: SessionRepository, + access_token_manager: AccessTokenManager, +) -> None: request = RegisterRequestFactory.build() response = await transport.post("/auth/register", json=request.model_dump()) + content = response.json() + + session = await session_repository.get_by_token(content["refresh_token"]) + token_content = access_token_manager.decode(content["access_token"]) assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( + + assert session == IsPartialDataclass( + user_id=token_content["user_id"], + token=content["refresh_token"], + host=IsStr, + ) + assert content == IsPartialDict( access_token=IsStr, refresh_token=IsStr, expires_in=IsInt, From e5c8bf5ae8cce727cdc71a9a452aae3eedfdf65c Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 01:11:38 +0300 Subject: [PATCH 14/55] tests: Partial rollback --- src/prodik/bootstrap/di/container.py | 4 +-- src/prodik/bootstrap/di/providers/__init__.py | 4 +-- .../providers/{database.py => connection.py} | 7 ++-- .../bootstrap/di/providers/infrastructure.py | 2 -- .../authorization/local_authorization.py | 16 ++++++++++ tests/conftest.py | 32 ++++++++++++++++++- tests/e2e/authentication/test_register.py | 10 +++++- 7 files changed, 65 insertions(+), 10 deletions(-) rename src/prodik/bootstrap/di/providers/{database.py => connection.py} (83%) diff --git a/src/prodik/bootstrap/di/container.py b/src/prodik/bootstrap/di/container.py index 0b4122c..c75a4a4 100644 --- a/src/prodik/bootstrap/di/container.py +++ b/src/prodik/bootstrap/di/container.py @@ -4,7 +4,7 @@ from prodik.bootstrap.di.providers import ( ApplicationProvider, CacheConnectionProvider, - DatabaseConnectionProvider, + ConnectionProvider, InfrastructureProvider, ) from prodik.infrastructure.config import ( @@ -22,7 +22,7 @@ def get_async_container(config: Config) -> AsyncContainer: InfrastructureProvider(), ApplicationProvider(), CacheConnectionProvider(), - DatabaseConnectionProvider(), + ConnectionProvider(), context={ APIConfig: config.api, DatabaseConfig: config.database, diff --git a/src/prodik/bootstrap/di/providers/__init__.py b/src/prodik/bootstrap/di/providers/__init__.py index 32ddce0..a91ffb0 100644 --- a/src/prodik/bootstrap/di/providers/__init__.py +++ b/src/prodik/bootstrap/di/providers/__init__.py @@ -1,11 +1,11 @@ from .application import ApplicationProvider from .cache import CacheConnectionProvider -from .database import DatabaseConnectionProvider +from .connection import ConnectionProvider from .infrastructure import InfrastructureProvider __all__ = ( "ApplicationProvider", "CacheConnectionProvider", - "DatabaseConnectionProvider", + "ConnectionProvider", "InfrastructureProvider", ) diff --git a/src/prodik/bootstrap/di/providers/database.py b/src/prodik/bootstrap/di/providers/connection.py similarity index 83% rename from src/prodik/bootstrap/di/providers/database.py rename to src/prodik/bootstrap/di/providers/connection.py index 336d739..bdd639e 100644 --- a/src/prodik/bootstrap/di/providers/database.py +++ b/src/prodik/bootstrap/di/providers/connection.py @@ -1,6 +1,6 @@ from collections.abc import AsyncIterator -from dishka import Provider, Scope, provide +from dishka import Provider, Scope, WithParents, provide, provide_all from redis.asyncio import Redis from sqlalchemy.ext.asyncio import ( AsyncEngine, @@ -10,9 +10,12 @@ ) from prodik.infrastructure.config import CacheConfig, DatabaseConfig +from prodik.infrastructure.transaction_manager import TransactionManagerImpl -class DatabaseConnectionProvider(Provider): +class ConnectionProvider(Provider): + provides = provide_all(WithParents[TransactionManagerImpl], scope=Scope.REQUEST) + @provide(scope=Scope.APP) async def get_engine(self, config: DatabaseConfig) -> AsyncIterator[AsyncEngine]: engine = create_async_engine(config.url, future=True) diff --git a/src/prodik/bootstrap/di/providers/infrastructure.py b/src/prodik/bootstrap/di/providers/infrastructure.py index 1834c63..58d4948 100644 --- a/src/prodik/bootstrap/di/providers/infrastructure.py +++ b/src/prodik/bootstrap/di/providers/infrastructure.py @@ -25,14 +25,12 @@ AccessTokenManagerImpl, RefreshTokenManagerImpl, ) -from prodik.infrastructure.transaction_manager import TransactionManagerImpl class InfrastructureProvider(Provider): provides = provide_all( WithParents[IdentityProviderImpl], WithParents[GroupRepositoryImpl], - WithParents[TransactionManagerImpl], WithParents[PasswordHasherImpl], WithParents[AccessTokenManagerImpl], WithParents[RefreshTokenManagerImpl], diff --git a/src/prodik/infrastructure/repositories/authorization/local_authorization.py b/src/prodik/infrastructure/repositories/authorization/local_authorization.py index 4499a49..b5c5095 100644 --- a/src/prodik/infrastructure/repositories/authorization/local_authorization.py +++ b/src/prodik/infrastructure/repositories/authorization/local_authorization.py @@ -58,3 +58,19 @@ async def get_by_user_id(self, user_id: UserId) -> LocalAuthorization | None: found=authorization is not None, ) return authorization + + async def get_all_by_user_id(self, user_id: UserId) -> list[LocalAuthorization]: + logger.debug("Repository get local authorizations by user id", user_id=user_id) + result = await self.session.execute( + select(LocalAuthorization).where( + LocalAuthorization.user_id == user_id # type: ignore + ) + ) + + result_local_authorizations = list(result.scalars().all()) + + logger.debug( + "Repository fetched edges by context id", + found_count=len(result_local_authorizations), + ) + return result_local_authorizations diff --git a/tests/conftest.py b/tests/conftest.py index aa37248..444768e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,18 @@ from collections.abc import AsyncIterator +from dataclasses import dataclass +from types import TracebackType from unittest.mock import AsyncMock import pytest -from dishka import AsyncContainer, Provider, Scope, make_async_container, provide +from dishka import ( + AsyncContainer, + Provider, + Scope, + WithParents, + make_async_container, + provide, + provide_all, +) from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import ( @@ -121,11 +131,31 @@ async def session(config: Config) -> AsyncIterator[AsyncSession]: await session.close() +@dataclass +class TestTransactionManagerImpl(TransactionManager): + session: AsyncSession + + async def __aenter__(self) -> None: + self.tx = await self.session.begin_nested() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.tx.__aexit__(exc_type, exc, tb) + + @pytest.fixture async def container( config: Config, session: AsyncSession ) -> AsyncIterator[AsyncContainer]: class TestConnectionProvider(Provider): + provides = provide_all( + WithParents[TestTransactionManagerImpl], scope=Scope.REQUEST + ) + @provide(scope=Scope.REQUEST) async def provide_async_session(self) -> AsyncSession: return session diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py index ab97204..e5515be 100644 --- a/tests/e2e/authentication/test_register.py +++ b/tests/e2e/authentication/test_register.py @@ -5,6 +5,7 @@ from httpx import AsyncClient from prodik.application.interfaces.repositories import ( + LocalAuthorizationRepository, SessionRepository, ) from prodik.application.interfaces.token_managers import AccessTokenManager @@ -42,7 +43,9 @@ async def test_register_ok( @pytest.mark.asyncio async def test_register_when_user_already_exists( - transport: AsyncClient, user_factory: UserFactory + transport: AsyncClient, + user_factory: UserFactory, + local_authorization_repository: LocalAuthorizationRepository, ) -> None: user_factory_response = await user_factory.build() @@ -53,7 +56,12 @@ async def test_register_when_user_already_exists( response = await transport.post("/auth/register", json=request.model_dump()) + local_authorizations = await local_authorization_repository.get_all_by_user_id( + user_factory_response.user.id + ) + assert response.status_code == HTTPStatus.CONFLICT + assert len(local_authorizations) == 1 assert response.json() == IsPartialDict( detail="User with this username or email already exists", meta=[ From 7e36ca118e5f8c3d0c51c372e230849723556684 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 18:09:15 +0300 Subject: [PATCH 15/55] tests: fix typo --- tests/factories/models/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/factories/models/user.py b/tests/factories/models/user.py index 79a3306..096dd3d 100644 --- a/tests/factories/models/user.py +++ b/tests/factories/models/user.py @@ -19,7 +19,7 @@ SessionId, ) from prodik.domain.user import User, UserId -from tests.factories.common import generate_random_ip, generate_random_string +from tests.factories.common import generate_random_string @dataclass @@ -71,7 +71,7 @@ async def build(self) -> UserFactoryResponse: session = Session.new( session_id=SessionId(uuid4()), - host=generate_random_ip(), + host="127.0.0.1", token=refresh_token, user=user, ) From 4a637fc485da2acf0fac50d12b7d079edecfc538 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 18:09:28 +0300 Subject: [PATCH 16/55] infra: some config changes --- example.config.toml | 6 ++++- .../bootstrap/di/providers/connection.py | 25 +++++++++++++++---- src/prodik/infrastructure/config.py | 14 +++++++++-- tests/conftest.py | 15 ++++++++++- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/example.config.toml b/example.config.toml index 1eab433..7ec4eaa 100644 --- a/example.config.toml +++ b/example.config.toml @@ -3,7 +3,11 @@ host = "0.0.0.0" port = 8000 [database] -url = "postgresql+asyncpg://postgres:admin_password@persistence.literally.io:5432/postgres" +username = "postgres" +password = "admin_password" +host = "persistence.literally.io" +database = "postgres" +port = 5432 [cache] host = "cache.literally.io" diff --git a/src/prodik/bootstrap/di/providers/connection.py b/src/prodik/bootstrap/di/providers/connection.py index bdd639e..5870711 100644 --- a/src/prodik/bootstrap/di/providers/connection.py +++ b/src/prodik/bootstrap/di/providers/connection.py @@ -2,6 +2,7 @@ from dishka import Provider, Scope, WithParents, provide, provide_all from redis.asyncio import Redis +from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, @@ -17,13 +18,27 @@ class ConnectionProvider(Provider): provides = provide_all(WithParents[TransactionManagerImpl], scope=Scope.REQUEST) @provide(scope=Scope.APP) - async def get_engine(self, config: DatabaseConfig) -> AsyncIterator[AsyncEngine]: - engine = create_async_engine(config.url, future=True) + async def provide_async_engine( + self, config: DatabaseConfig + ) -> AsyncIterator[AsyncEngine]: + engine = create_async_engine( + url=URL.create( + "postgresql+asyncpg", + username=config.username, + password=config.password, + database=config.database, + port=config.port, + host=config.host, + ), + pool_size=5, + max_overflow=96, + pool_timeout=30, + ) yield engine await engine.dispose() @provide(scope=Scope.APP) - async def get_async_sessionmaker( + async def provide_async_sessionmaker( self, engine: AsyncEngine, ) -> async_sessionmaker[AsyncSession]: @@ -33,7 +48,7 @@ async def get_async_sessionmaker( ) @provide(scope=Scope.REQUEST) - async def get_async_session( + async def provide_async_session( self, session_factory: async_sessionmaker[AsyncSession], ) -> AsyncIterator[AsyncSession]: @@ -41,7 +56,7 @@ async def get_async_session( yield session @provide(scope=Scope.REQUEST) - async def get_redis_connection( + async def provide_redis_connection( self, config: CacheConfig, ) -> AsyncIterator[Redis]: diff --git a/src/prodik/infrastructure/config.py b/src/prodik/infrastructure/config.py index 7bb7de4..6d634a3 100644 --- a/src/prodik/infrastructure/config.py +++ b/src/prodik/infrastructure/config.py @@ -27,7 +27,11 @@ class CacheConfig: @dataclass(slots=True, frozen=True, kw_only=True) class DatabaseConfig: - url: str + username: str + password: str + database: str + host: str + port: int @dataclass(slots=True, frozen=True, kw_only=True) @@ -48,7 +52,13 @@ def load_config(path: str = "config.toml") -> Config: port=config["api"]["port"], debug=os.getenv("DEBUG", "false") in ("true", "false"), ), - database=DatabaseConfig(url=config["database"]["url"]), + database=DatabaseConfig( + username=config["database"]["username"], + password=config["database"]["password"], + database=config["database"]["database"], + host=config["database"]["host"], + port=config["database"]["port"], + ), secrets=SecretsConfig( secret=config["secrets"]["secret"], expires_in=config["secrets"]["expires_in"], diff --git a/tests/conftest.py b/tests/conftest.py index 444768e..ec47c95 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ ) from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient +from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncSession, create_async_engine, @@ -123,7 +124,19 @@ async def company_factory(container: AsyncContainer) -> CompanyFactory: @pytest.fixture async def session(config: Config) -> AsyncIterator[AsyncSession]: - engine = create_async_engine(config.database.url) + engine = create_async_engine( + url=URL.create( + "postgresql+asyncpg", + username=config.database.username, + password=config.database.password, + database=config.database.database, + port=config.database.port, + host=config.database.host, + ), + pool_size=5, + max_overflow=96, + pool_timeout=30, + ) async with AsyncSession(engine) as session: session.commit = AsyncMock() # type: ignore From 30a420b7b288a201d8e90279e56f0b2cdef5fb04 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 18:55:17 +0300 Subject: [PATCH 17/55] fix(infrastructure): wrong session store --- src/prodik/application/authorization/login.py | 6 ++- .../application/authorization/refresh.py | 3 +- .../repositories/authorization/session.py | 7 ++- .../update_current_user_password.py | 7 ++- .../repositories/authorization/session.py | 44 +++++++++++++------ 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/prodik/application/authorization/login.py b/src/prodik/application/authorization/login.py index 1850b80..c08ed5e 100644 --- a/src/prodik/application/authorization/login.py +++ b/src/prodik/application/authorization/login.py @@ -79,7 +79,9 @@ async def execute(self, request: LoginRequestDTO) -> LoginResponseDTO: access_token, expires_in = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() - session = await self.session_repository.get_by_host(host) + session = await self.session_repository.get_by_host_and_user_id( + user.id, host + ) if session is None: session = Session.new( session_id=SessionId(uuid4()), @@ -89,7 +91,9 @@ async def execute(self, request: LoginRequestDTO) -> LoginResponseDTO: ) await self.session_repository.create(session) else: + prev_token = session.token session.update_token(refresh_token) + await self.session_repository.update(prev_token, session) return LoginResponseDTO( access_token=access_token, diff --git a/src/prodik/application/authorization/refresh.py b/src/prodik/application/authorization/refresh.py index b3ffacf..9ccc4b5 100644 --- a/src/prodik/application/authorization/refresh.py +++ b/src/prodik/application/authorization/refresh.py @@ -35,9 +35,10 @@ async def execute(self, token: str) -> RefreshTokenResponse: access_token, expires_in = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() + prev_token = session.token session.update_token(refresh_token) - await self.session_repository.update(session) + await self.session_repository.update(prev_token, session) return RefreshTokenResponse( access_token=access_token, diff --git a/src/prodik/application/interfaces/repositories/authorization/session.py b/src/prodik/application/interfaces/repositories/authorization/session.py index 3e7ff15..646ae3c 100644 --- a/src/prodik/application/interfaces/repositories/authorization/session.py +++ b/src/prodik/application/interfaces/repositories/authorization/session.py @@ -1,10 +1,13 @@ from typing import Protocol from prodik.domain.authorization import Session +from prodik.domain.user import UserId class SessionRepository(Protocol): async def create(self, session: Session) -> None: ... - async def update(self, session: Session) -> None: ... - async def get_by_host(self, host: str) -> Session | None: ... + async def update(self, prev_token: str, session: Session) -> None: ... + async def get_by_host_and_user_id( + self, user_id: UserId, host: str + ) -> Session | None: ... async def get_by_token(self, token: str) -> Session: ... diff --git a/src/prodik/application/manage_credentials/update_current_user_password.py b/src/prodik/application/manage_credentials/update_current_user_password.py index 5f851d3..17930ba 100644 --- a/src/prodik/application/manage_credentials/update_current_user_password.py +++ b/src/prodik/application/manage_credentials/update_current_user_password.py @@ -77,7 +77,9 @@ async def execute( authorization.update_password(hashed_password) user.increment_revision() - session = await self.session_repository.get_by_host(host) + session = await self.session_repository.get_by_host_and_user_id( + user.id, host + ) if session is None: raise SessionNotFoundError( "Session not found", @@ -87,11 +89,12 @@ async def execute( access_token, expires_in = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() + prev_token = session.token session.update_token(refresh_token) await self.user_repository.update(user) await self.local_authorization_repository.update(authorization) - await self.session_repository.update(session) + await self.session_repository.update(prev_token, session) return UpdateCurrentUserPasswordResponseDTO( access_token=access_token, diff --git a/src/prodik/infrastructure/repositories/authorization/session.py b/src/prodik/infrastructure/repositories/authorization/session.py index c718744..6d177d8 100644 --- a/src/prodik/infrastructure/repositories/authorization/session.py +++ b/src/prodik/infrastructure/repositories/authorization/session.py @@ -20,8 +20,10 @@ class SessionRepositoryImpl(SessionRepository): async def create(self, session: Session) -> None: logger.debug("Repository create session", session_id=session.id) - session_key = f"session:{session.token}" - host_key = f"host:{session.host}" + session_key = f"session:{session.id}" + + host_key = f"host:{session.host}:{session.user_id}" + token_key = f"token:{session.token}" await self.client.hset( session_key, @@ -35,13 +37,17 @@ async def create(self, session: Session) -> None: }, ) - await self.client.set(host_key, session.token) + await self.client.set(host_key, str(session.id)) + await self.client.set(token_key, str(session.id)) await self.client.expire(session_key, self.config.ttl) - async def update(self, session: Session) -> None: + async def update(self, prev_token: str, session: Session) -> None: logger.debug("Repository update session", session_id=session.id) - session_key = f"session:{session.token}" + + session_key = f"session:{session.id}" + token_key = f"token:{prev_token}" + await self.client.hset( session_key, mapping={ # type: ignore @@ -50,11 +56,21 @@ async def update(self, session: Session) -> None: }, ) + await self.client.delete(token_key) + await self.client.set(token_key, str(session.id)) + async def get_by_token(self, token: str) -> Session: logger.debug("Repository get session by token") - data = await self.client.hgetall(f"session:{token}") # type: ignore - if not data: + session_id = await self.client.get(f"token:{token}") + if session_id is None: + raise SessionNotFoundError( + "Session not found", + [{"key": "refresh_token", "value": token}], + ) + + data = await self.client.hgetall(f"session:{session_id}") # type: ignore + if data is None: raise SessionNotFoundError( "Session not found", [{"key": "refresh_token", "value": token}], @@ -72,18 +88,20 @@ async def get_by_token(self, token: str) -> Session: logger.debug("Repository fetched session by token", found=session is not None) return session - async def get_by_host(self, host: str) -> Session | None: + async def get_by_host_and_user_id( + self, user_id: UserId, host: str + ) -> Session | None: logger.debug("Repository get session by host", host=host) - token = await self.client.get(f"host:{host}") - if token is None: + session_id = await self.client.get(f"host:{host}:{user_id}") + if session_id is None: return None - data = await self.client.hgetall(f"session:{token}") # type: ignore - if not data: + data = await self.client.hgetall(f"session:{session_id}") # type: ignore + if data is None: raise SessionNotFoundError( "Session not found", - [{"key": "refresh_token", "value": token}], + [{"key": "refresh_token", "value": session_id}], ) session = Session( From 7e61db1003bd5244e89db7631d959b0020a3c2b2 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 18:55:59 +0300 Subject: [PATCH 18/55] tests: some changes --- tests/e2e/authentication/test_login.py | 28 ++++++++++++++++--- .../e2e/authentication/test_refresh_token.py | 19 +++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/e2e/authentication/test_login.py b/tests/e2e/authentication/test_login.py index 060faac..2ea94de 100644 --- a/tests/e2e/authentication/test_login.py +++ b/tests/e2e/authentication/test_login.py @@ -1,15 +1,20 @@ from http import HTTPStatus import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr +from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr from httpx import AsyncClient +from prodik.application.interfaces.repositories import SessionRepository from tests.factories.models import UserFactory from tests.factories.schemas import LoginRequestFactory @pytest.mark.asyncio -async def test_login_ok(transport: AsyncClient, user_factory: UserFactory) -> None: +async def test_login_ok( + transport: AsyncClient, + user_factory: UserFactory, + session_repository: SessionRepository, +) -> None: user_factory_response = await user_factory.build() request = LoginRequestFactory.build( @@ -18,9 +23,19 @@ async def test_login_ok(transport: AsyncClient, user_factory: UserFactory) -> No ) response = await transport.post("/auth/login", json=request.model_dump()) + content = response.json() + + session = await session_repository.get_by_host_and_user_id( + user_factory_response.user.id, "127.0.0.1" + ) assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( + assert session == IsPartialDataclass( + user_id=str(user_factory_response.user.id), + token=content["refresh_token"], + host="127.0.0.1", + ) + assert content == IsPartialDict( access_token=IsStr, refresh_token=IsStr, expires_in=IsInt, @@ -29,7 +44,9 @@ async def test_login_ok(transport: AsyncClient, user_factory: UserFactory) -> No @pytest.mark.asyncio async def test_login_invalid_password( - transport: AsyncClient, user_factory: UserFactory + transport: AsyncClient, + user_factory: UserFactory, + session_repository: SessionRepository, ) -> None: user_factory_response = await user_factory.build() @@ -40,6 +57,9 @@ async def test_login_invalid_password( response = await transport.post("/auth/login", json=request.model_dump()) + session = await session_repository.get_by_token(user_factory_response.refresh_token) + + assert session is not None assert response.status_code == HTTPStatus.UNAUTHORIZED assert response.json() == IsPartialDict( detail="Invalid email or password", diff --git a/tests/e2e/authentication/test_refresh_token.py b/tests/e2e/authentication/test_refresh_token.py index 4df0847..167e8c9 100644 --- a/tests/e2e/authentication/test_refresh_token.py +++ b/tests/e2e/authentication/test_refresh_token.py @@ -1,9 +1,10 @@ from http import HTTPStatus import pytest -from dirty_equals import IsInt, IsPartialDict, IsStr +from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr from httpx import AsyncClient +from prodik.application.interfaces.repositories import SessionRepository from tests.factories.common import authorization_headers, generate_random_string from tests.factories.models import UserFactory from tests.factories.schemas import RefreshTokenRequestFactory @@ -11,7 +12,9 @@ @pytest.mark.asyncio async def test_refresh_token_ok( - transport: AsyncClient, user_factory: UserFactory + transport: AsyncClient, + user_factory: UserFactory, + session_repository: SessionRepository, ) -> None: user_factory_response = await user_factory.build() @@ -24,9 +27,19 @@ async def test_refresh_token_ok( headers=authorization_headers(user_factory_response.access_token), json=request.model_dump(), ) + content = response.json() + + session = await session_repository.get_by_host_and_user_id( + user_factory_response.user.id, "127.0.0.1" + ) assert response.status_code == HTTPStatus.OK - assert response.json() == IsPartialDict( + assert session == IsPartialDataclass( + user_id=str(user_factory_response.user.id), + token=content["refresh_token"], + host="127.0.0.1", + ) + assert content == IsPartialDict( access_token=IsStr, refresh_token=IsStr, expires_in=IsInt, From d6f420b9500cf25f3fbf5e16f3ef18be539d9672 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 20:29:11 +0300 Subject: [PATCH 19/55] tests: some company test checks --- .../interfaces/repositories/role.py | 1 - .../infrastructure/repositories/role.py | 13 ------- tests/conftest.py | 18 +++++++++ tests/e2e/company/test_register_company.py | 37 +++++++++++++++++-- tests/factories/models/company.py | 8 ++++ 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/prodik/application/interfaces/repositories/role.py b/src/prodik/application/interfaces/repositories/role.py index ad27d1f..7e520a3 100644 --- a/src/prodik/application/interfaces/repositories/role.py +++ b/src/prodik/application/interfaces/repositories/role.py @@ -8,7 +8,6 @@ class RoleRepository(Protocol): async def update(self, role: Role) -> None: ... async def delete(self, role: Role) -> None: ... async def create(self, role: Role) -> None: ... - async def get_by_name(self, name: RoleName) -> Role | None: ... async def get_by_id(self, role_id: RoleId) -> Role: ... async def get_all_by_ids(self, roles_ids: list[RoleId]) -> list[Role]: ... async def get_by_name_and_company_id( diff --git a/src/prodik/infrastructure/repositories/role.py b/src/prodik/infrastructure/repositories/role.py index 9dc48f5..f9496a0 100644 --- a/src/prodik/infrastructure/repositories/role.py +++ b/src/prodik/infrastructure/repositories/role.py @@ -46,19 +46,6 @@ async def get_all_by_ids(self, roles_ids: list[RoleId]) -> list[Role]: return roles - async def get_by_name(self, name: RoleName) -> Role | None: - logger.debug("Repository get role by name", role_name=name) - result = await self.session.execute( - select(Role).where( - Role.name == name, # type: ignore - ), - ) - - role = result.scalar_one_or_none() - logger.debug("Repository fetched role by name", found=role is not None) - - return role - async def update(self, role: Role) -> None: logger.debug("Repository update role", role_id=role.id) await self.session.execute( diff --git a/tests/conftest.py b/tests/conftest.py index ec47c95..23f6f0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,6 +91,24 @@ async def user_repository(container: AsyncContainer) -> UserRepository: return await test_container.get(UserRepository) # type: ignore[no-any-return] +@pytest.fixture +async def user_grant_repository(container: AsyncContainer) -> UserGrantRepository: + async with container() as test_container: + return await test_container.get(UserGrantRepository) # type: ignore[no-any-return] + + +@pytest.fixture +async def company_repository(container: AsyncContainer) -> CompanyRepository: + async with container() as test_contaner: + return await test_contaner.get(CompanyRepository) # type: ignore[no-any-return] + + +@pytest.fixture +async def role_repository(container: AsyncContainer) -> RoleRepository: + async with container() as test_container: + return await test_container.get(RoleRepository) # type: ignore[no-any-return] + + @pytest.fixture async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: diff --git a/tests/e2e/company/test_register_company.py b/tests/e2e/company/test_register_company.py index 7392c0d..488d99b 100644 --- a/tests/e2e/company/test_register_company.py +++ b/tests/e2e/company/test_register_company.py @@ -4,6 +4,12 @@ from dirty_equals import IsPartialDict, IsStr from httpx import AsyncClient +from prodik.application.interfaces.repositories import ( + CompanyRepository, + RoleRepository, + UserGrantRepository, +) +from prodik.domain.role import RoleName from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, UserFactory from tests.factories.schemas import RegisterCompanyRequestFactory @@ -11,7 +17,11 @@ @pytest.mark.asyncio async def test_register_company_ok( - transport: AsyncClient, user_factory: UserFactory + transport: AsyncClient, + user_factory: UserFactory, + role_repository: RoleRepository, + company_repository: CompanyRepository, + user_grant_repository: UserGrantRepository, ) -> None: user_factory_response = await user_factory.build() @@ -22,9 +32,22 @@ async def test_register_company_ok( headers=authorization_headers(user_factory_response.access_token), json=request.model_dump(), ) + content = response.json() + + company = await company_repository.get_by_id(content["id"]) + role = await role_repository.get_by_name_and_company_id( + RoleName("owner"), content["id"] + ) + assert company is not None + assert role is not None + + grant = await user_grant_repository.get_by_user_and_role_id( + user_factory_response.user.id, role.id + ) + assert grant is not None assert response.status_code == HTTPStatus.CREATED - assert response.json() == IsPartialDict( + assert content == IsPartialDict( id=IsStr, name=request.name, owner_id=str(user_factory_response.user.id), @@ -35,7 +58,10 @@ async def test_register_company_ok( @pytest.mark.asyncio async def test_register_company_while_same_company_exists( - transport: AsyncClient, user_factory: UserFactory, company_factory: CompanyFactory + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + user_grant_repository: UserGrantRepository, ) -> None: user_factory_response = await user_factory.build() @@ -52,6 +78,11 @@ async def test_register_company_while_same_company_exists( json=request.model_dump(), ) + grants = await user_grant_repository.get_all_by_user_id( + user_factory_response.user.id + ) + + assert len(grants) == 1 assert response.status_code == HTTPStatus.CONFLICT assert response.json() == IsPartialDict( detail="Company with this name already exists", diff --git a/tests/factories/models/company.py b/tests/factories/models/company.py index d6584c3..c966a43 100644 --- a/tests/factories/models/company.py +++ b/tests/factories/models/company.py @@ -10,6 +10,7 @@ from prodik.application.interfaces.transaction_manager import TransactionManager from prodik.application.services import RoleManagmentService from prodik.domain.company import Company, CompanyId +from prodik.domain.grant import UserGrant, UserGrantId from prodik.domain.role import EntityType, PermissionType from prodik.domain.user import User from tests.factories.common import generate_random_string @@ -54,10 +55,17 @@ async def build(self, *, owner: User) -> Company: ) ) + grant = UserGrant.new( + user_grant_id=UserGrantId(uuid4()), + role=role_managment_service_response.role, + user=owner, + ) + await self.company_repository.create(company) await self.role_repository.create(role_managment_service_response.role) await self.role_permissions_repository.create_all( role_managment_service_response.permissions ) + await self.user_grant_repository.create(grant) return company From a23a6618b5444164331132092a43be7341a18b32 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 20:44:41 +0300 Subject: [PATCH 20/55] tests: move long asserts --- tests/e2e/company/test_register_company.py | 21 +++++++---------- tests/e2e/helpers/__init__.py | 0 tests/e2e/helpers/asserts.py | 27 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 tests/e2e/helpers/__init__.py create mode 100644 tests/e2e/helpers/asserts.py diff --git a/tests/e2e/company/test_register_company.py b/tests/e2e/company/test_register_company.py index 488d99b..b78e356 100644 --- a/tests/e2e/company/test_register_company.py +++ b/tests/e2e/company/test_register_company.py @@ -9,7 +9,7 @@ RoleRepository, UserGrantRepository, ) -from prodik.domain.role import RoleName +from tests.e2e.helpers.asserts import assert_user_have_correct_rights from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, UserFactory from tests.factories.schemas import RegisterCompanyRequestFactory @@ -34,19 +34,14 @@ async def test_register_company_ok( ) content = response.json() - company = await company_repository.get_by_id(content["id"]) - role = await role_repository.get_by_name_and_company_id( - RoleName("owner"), content["id"] - ) - assert company is not None - assert role is not None - - grant = await user_grant_repository.get_by_user_and_role_id( - user_factory_response.user.id, role.id - ) - assert grant is not None - assert response.status_code == HTTPStatus.CREATED + await assert_user_have_correct_rights( + content, + user_factory_response.user, + user_grant_repository, + company_repository, + role_repository, + ) assert content == IsPartialDict( id=IsStr, name=request.name, diff --git a/tests/e2e/helpers/__init__.py b/tests/e2e/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/helpers/asserts.py b/tests/e2e/helpers/asserts.py new file mode 100644 index 0000000..469c5cb --- /dev/null +++ b/tests/e2e/helpers/asserts.py @@ -0,0 +1,27 @@ +from typing import Any + +from prodik.application.interfaces.repositories import ( + CompanyRepository, + RoleRepository, + UserGrantRepository, +) +from prodik.domain.role import RoleName +from prodik.domain.user import User + + +async def assert_user_have_correct_rights( + content: dict[str, Any], + user: User, + user_grant_repository: UserGrantRepository, + company_repository: CompanyRepository, + role_repository: RoleRepository, +) -> None: + company = await company_repository.get_by_id(content["id"]) + role = await role_repository.get_by_name_and_company_id( + RoleName("owner"), content["id"] + ) + assert role is not None + + grant = await user_grant_repository.get_by_user_and_role_id(user.id, role.id) + assert company is not None + assert grant is not None From cb6190659639084cc24032f104432622b3777cfa Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 21:01:03 +0300 Subject: [PATCH 21/55] tests: test verify company --- tests/e2e/company/test_verify_company.py | 51 ++++++++++++++++++++++++ tests/factories/models/user.py | 6 ++- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/company/test_verify_company.py diff --git a/tests/e2e/company/test_verify_company.py b/tests/e2e/company/test_verify_company.py new file mode 100644 index 0000000..b3d1404 --- /dev/null +++ b/tests/e2e/company/test_verify_company.py @@ -0,0 +1,51 @@ +from http import HTTPStatus + +import pytest +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import CompanyRepository +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, UserFactory + + +@pytest.mark.asyncio +async def test_verify_company_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + company_repository: CompanyRepository, +) -> None: + user_factory_response = await user_factory.build(admin=True) + + company = await company_factory.build(owner=user_factory_response.user) + + response = await transport.patch( + f"/companies/{company.id}/verify", + headers=authorization_headers(user_factory_response.access_token), + ) + + company = await company_repository.get_by_id(company.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert company.verified + + +@pytest.mark.asyncio +async def test_verify_company_user_not_admin( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + company_repository: CompanyRepository, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + + response = await transport.patch( + f"/companies/{company.id}/verify", + headers=authorization_headers(user_factory_response.access_token), + ) + company = await company_repository.get_by_id(company.id) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert not company.verified diff --git a/tests/factories/models/user.py b/tests/factories/models/user.py index 096dd3d..8d9ae90 100644 --- a/tests/factories/models/user.py +++ b/tests/factories/models/user.py @@ -18,7 +18,7 @@ Session, SessionId, ) -from prodik.domain.user import User, UserId +from prodik.domain.user import User, UserId, UserSystemRole from tests.factories.common import generate_random_string @@ -40,7 +40,7 @@ class UserFactory: user_repository: UserRepository password_hasher: PasswordHasher - async def build(self) -> UserFactoryResponse: + async def build(self, *, admin: bool = False) -> UserFactoryResponse: async with self.transaction_manager: user = User.new( user_id=UserId(uuid4()), @@ -57,6 +57,8 @@ async def build(self) -> UserFactoryResponse: + "@testing.org", bio=generate_random_string(100), ) + if admin: + user.system_role = UserSystemRole.ADMIN access_token = self.access_token_manager.encode(user) refresh_token = self.refresh_token_manager.encode() From 44f53ada4b8ff5a4534427bd73653b924d7036ee Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 22:24:03 +0300 Subject: [PATCH 22/55] tests: create group test --- .../application/manage_group/create_group.py | 12 +- .../persistence/migrations/env.py | 14 +- .../versions/0333150881b1_parent_group_id.py | 34 ++++ .../infrastructure/persistence/registry.py | 5 + .../infrastructure/repositories/group.py | 6 +- .../presentation/exceptions/handlers.py | 6 +- tests/conftest.py | 24 ++- tests/e2e/group/__init__.py | 0 tests/e2e/group/test_create_group.py | 152 ++++++++++++++++++ tests/factories/models/__init__.py | 3 +- tests/factories/models/group.py | 30 ++++ tests/factories/schemas/__init__.py | 2 + tests/factories/schemas/group.py | 7 + 13 files changed, 277 insertions(+), 18 deletions(-) create mode 100644 src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py create mode 100644 tests/e2e/group/__init__.py create mode 100644 tests/e2e/group/test_create_group.py create mode 100644 tests/factories/models/group.py create mode 100644 tests/factories/schemas/group.py diff --git a/src/prodik/application/manage_group/create_group.py b/src/prodik/application/manage_group/create_group.py index 6e58b77..89783d6 100644 --- a/src/prodik/application/manage_group/create_group.py +++ b/src/prodik/application/manage_group/create_group.py @@ -1,9 +1,6 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.errors import ( - GroupNotFoundError, -) from prodik.application.interfaces.repositories import ( CompanyRepository, GroupRepository, @@ -43,20 +40,15 @@ async def execute(self, request: CreateGroupRequestDTO) -> Group: parent_group = None if request.parent_group_id: parent_group = await self.group_repository.get_by_id( - request.parent_group_id, + request.parent_group_id ) - if parent_group is None: - raise GroupNotFoundError( - "Group not found", - [{"key": "group_id", "value": request.parent_group_id}], - ) group = Group.new( group_id=GroupId(uuid4()), name=request.name, description=request.description, company=company, - parent_group=parent_group if parent_group is not None else None, + parent_group=parent_group, ) await self.group_repository.create(group) diff --git a/src/prodik/infrastructure/persistence/migrations/env.py b/src/prodik/infrastructure/persistence/migrations/env.py index 1a07e5e..f78aae0 100644 --- a/src/prodik/infrastructure/persistence/migrations/env.py +++ b/src/prodik/infrastructure/persistence/migrations/env.py @@ -2,7 +2,7 @@ from logging.config import fileConfig from alembic import context -from sqlalchemy import Connection, pool +from sqlalchemy import Connection, pool, URL from sqlalchemy.ext.asyncio import async_engine_from_config from prodik.infrastructure.config import load_config @@ -36,7 +36,17 @@ def render_item(type_, obj, autogen_context): # ... etc. database_config = load_config() -config.set_main_option("sqlalchemy.url", database_config.database.url) +config.set_main_option( + "sqlalchemy.url", + URL.create( + "postgresql+asyncpg", + username=database_config.database.username, + password=database_config.database.password, + database=database_config.database.database, + port=database_config.database.port, + host=database_config.database.host, + ).render_as_string(hide_password=False) +) def run_migrations_offline() -> None: diff --git a/src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py b/src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py new file mode 100644 index 0000000..f82d345 --- /dev/null +++ b/src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py @@ -0,0 +1,34 @@ +"""parent_group_id + +Revision ID: 0333150881b1 +Revises: 4a52d766f55e +Create Date: 2026-06-25 19:10:38.311452 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0333150881b1' +down_revision: Union[str, Sequence[str], None] = '4a52d766f55e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('group_record', sa.Column('parent_group_id', sa.UUID(), nullable=True)) + op.create_foreign_key(None, 'group_record', 'group_record', ['parent_group_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'group_record', type_='foreignkey') + op.drop_column('group_record', 'parent_group_id') + # ### end Alembic commands ### diff --git a/src/prodik/infrastructure/persistence/registry.py b/src/prodik/infrastructure/persistence/registry.py index 785f716..0a36e39 100644 --- a/src/prodik/infrastructure/persistence/registry.py +++ b/src/prodik/infrastructure/persistence/registry.py @@ -131,6 +131,11 @@ ForeignKey("company_record.id", ondelete="CASCADE"), nullable=False, ), + Column( + "parent_group_id", + ForeignKey("group_record.id", ondelete="CASCADE"), + nullable=True, + ), Column("created_at", DateTime(timezone=True), nullable=False), Column("updated_at", DateTime(timezone=True), nullable=False), ) diff --git a/src/prodik/infrastructure/repositories/group.py b/src/prodik/infrastructure/repositories/group.py index 8aca04a..25310f8 100644 --- a/src/prodik/infrastructure/repositories/group.py +++ b/src/prodik/infrastructure/repositories/group.py @@ -23,6 +23,7 @@ async def create(self, group: Group) -> None: id=group.id, name=group.name, description=group.description, + parent_group_id=group.parent_group_id, company_id=group.company_id, created_at=group.created_at, updated_at=group.updated_at, @@ -60,16 +61,17 @@ async def delete(self, group: Group) -> None: async def get_all_by_company_id(self, company_id: CompanyId) -> list[Group]: logger.debug("Repository get groups by company id", company_id=company_id) result = await self.session.execute( - delete(Group).where( + select(Group).where( Group.company_id == company_id, # type: ignore ), ) - result_groups = list(result.scalars()) + result_groups = list(result.scalars().all()) logger.debug( "Repository fetched groups by company id", found_count=len(result_groups), ) + return result_groups async def get_all_by_ids(self, group_ids: list[GroupId]) -> list[Group]: diff --git a/src/prodik/presentation/exceptions/handlers.py b/src/prodik/presentation/exceptions/handlers.py index 5b13020..e8ce595 100644 --- a/src/prodik/presentation/exceptions/handlers.py +++ b/src/prodik/presentation/exceptions/handlers.py @@ -88,8 +88,10 @@ async def application_error_handler( ) -> JSONResponse: status_code = HTTPStatus.INTERNAL_SERVER_ERROR for error in type(exception).mro(): - if error in EXCEPTION_HANDLERS: - status_code = EXCEPTION_HANDLERS[error] + handler_status = EXCEPTION_HANDLERS.get(error) + if handler_status is not None: + status_code = handler_status + break logger.warning( "Handled application error", diff --git a/tests/conftest.py b/tests/conftest.py index 23f6f0d..15cbef3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ from prodik.application.interfaces.password_hasher import PasswordHasher from prodik.application.interfaces.repositories import ( CompanyRepository, + GroupRepository, LocalAuthorizationRepository, RolePermissionsRepository, RoleRepository, @@ -52,7 +53,11 @@ load_config, ) from prodik.infrastructure.persistence import start_mapper -from tests.factories.models import CompanyFactory, UserFactory +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + UserFactory, +) @pytest.fixture(scope="session") @@ -79,6 +84,14 @@ async def local_authorization_repository( return await test_container.get(LocalAuthorizationRepository) # type: ignore[no-any-return] +@pytest.fixture +async def group_repository( + container: AsyncContainer, +) -> GroupRepository: + async with container() as test_container: + return await test_container.get(GroupRepository) # type: ignore[no-any-return] + + @pytest.fixture async def access_token_manager(container: AsyncContainer) -> AccessTokenManager: async with container() as test_container: @@ -125,6 +138,15 @@ async def user_factory(container: AsyncContainer) -> UserFactory: ) +@pytest.fixture +async def group_factory(container: AsyncContainer) -> GroupFactory: + async with container() as test_container: + return GroupFactory( + group_repository=await test_container.get(GroupRepository), + transaction_manager=await test_container.get(TransactionManager), + ) + + @pytest.fixture async def company_factory(container: AsyncContainer) -> CompanyFactory: async with container() as test_container: diff --git a/tests/e2e/group/__init__.py b/tests/e2e/group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/group/test_create_group.py b/tests/e2e/group/test_create_group.py new file mode 100644 index 0000000..2cb374f --- /dev/null +++ b/tests/e2e/group/test_create_group.py @@ -0,0 +1,152 @@ +from http import HTTPStatus +from uuid import uuid4 + +import pytest +from dirty_equals import IsPartialDict, IsStr +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import GroupRepository +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, GroupFactory, UserFactory +from tests.factories.schemas import CreateGroupRequestFactory + +# Создание группы это: + +# 2. Проверка того что родителькая группа существует (если она указана). + + +@pytest.mark.asyncio +async def test_create_group_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_repository: GroupRepository, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + + request = CreateGroupRequestFactory.build( + parent_group_id=None, company_id=company.id + ) + + response = await transport.post( + "/groups/", + headers=authorization_headers(user_factory_response.access_token), + json=request.model_dump(mode="json"), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 1 + assert response.status_code == HTTPStatus.CREATED + assert response.json() == IsPartialDict( + id=IsStr, + name=request.name, + description=request.description, + company_id=str(company.id), + parent_group_id=None, + ) + + +@pytest.mark.asyncio +async def test_create_group_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_repository: GroupRepository, +) -> None: + users = ( + await user_factory.build(), + await user_factory.build(), + ) + + company = await company_factory.build(owner=users[1].user) + + request = CreateGroupRequestFactory.build( + parent_group_id=None, company_id=company.id + ) + + response = await transport.post( + "/groups/", + headers=authorization_headers(users[0].access_token), + json=request.model_dump(mode="json"), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 0 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) + + +@pytest.mark.asyncio +async def test_create_group_with_parent( + transport: AsyncClient, + user_factory: UserFactory, + group_factory: GroupFactory, + company_factory: CompanyFactory, + group_repository: GroupRepository, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + parent_group = await group_factory.build(company=company) + + request = CreateGroupRequestFactory.build( + parent_group_id=parent_group.id, company_id=company.id + ) + + response = await transport.post( + "/groups/", + headers=authorization_headers(user_factory_response.access_token), + json=request.model_dump(mode="json"), + ) + content = response.json() + + group = await group_repository.get_by_id(content["id"]) + + assert group.parent_group_id == parent_group.id + assert response.status_code == HTTPStatus.CREATED + assert content == IsPartialDict( + id=IsStr, + name=request.name, + description=request.description, + company_id=str(company.id), + parent_group_id=str(parent_group.id), + ) + + +@pytest.mark.asyncio +async def test_create_group_with_parent_not_exists( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_repository: GroupRepository, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + + fake_group_id = uuid4() + request = CreateGroupRequestFactory.build( + parent_group_id=fake_group_id, company_id=company.id + ) + + response = await transport.post( + "/groups/", + headers=authorization_headers(user_factory_response.access_token), + json=request.model_dump(mode="json"), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 0 + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.json() == IsPartialDict( + detail="Group not found", + meta=[{"key": "group_id", "value": str(fake_group_id)}], + ) diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py index 47363d9..e409482 100644 --- a/tests/factories/models/__init__.py +++ b/tests/factories/models/__init__.py @@ -1,4 +1,5 @@ from .company import CompanyFactory +from .group import GroupFactory from .user import UserFactory -__all__ = ("CompanyFactory", "UserFactory") +__all__ = ("CompanyFactory", "GroupFactory", "UserFactory") diff --git a/tests/factories/models/group.py b/tests/factories/models/group.py new file mode 100644 index 0000000..c7b2ec7 --- /dev/null +++ b/tests/factories/models/group.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import GroupRepository +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.domain.company import Company +from prodik.domain.group import Group, GroupId +from tests.factories.common import generate_random_string + + +@dataclass +class GroupFactory: + group_repository: GroupRepository + transaction_manager: TransactionManager + + async def build( + self, *, company: Company, parent_group: Group | None = None + ) -> Group: + async with self.transaction_manager: + group = Group.new( + group_id=GroupId(uuid4()), + name=generate_random_string(), + description=generate_random_string(), + company=company, + parent_group=parent_group, + ) + + await self.group_repository.create(group) + + return group diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index 47e212b..cfb97a3 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -4,8 +4,10 @@ RegisterRequestFactory, ) from .company import RegisterCompanyRequestFactory +from .group import CreateGroupRequestFactory __all__ = ( + "CreateGroupRequestFactory", "LoginRequestFactory", "RefreshTokenRequestFactory", "RegisterCompanyRequestFactory", diff --git a/tests/factories/schemas/group.py b/tests/factories/schemas/group.py new file mode 100644 index 0000000..a81dd88 --- /dev/null +++ b/tests/factories/schemas/group.py @@ -0,0 +1,7 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.group import CreateGroupRequest + + +class CreateGroupRequestFactory(ModelFactory[CreateGroupRequest]): + __use_examples__ = True From 6f81bd163b3d3bdc506848daec119ee6b1a4e9b7 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 25 Jun 2026 22:24:57 +0300 Subject: [PATCH 23/55] tests: remove comm --- tests/e2e/group/test_create_group.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/e2e/group/test_create_group.py b/tests/e2e/group/test_create_group.py index 2cb374f..0d46f5a 100644 --- a/tests/e2e/group/test_create_group.py +++ b/tests/e2e/group/test_create_group.py @@ -10,10 +10,6 @@ from tests.factories.models import CompanyFactory, GroupFactory, UserFactory from tests.factories.schemas import CreateGroupRequestFactory -# Создание группы это: - -# 2. Проверка того что родителькая группа существует (если она указана). - @pytest.mark.asyncio async def test_create_group_ok( From bbd5e82d0a8248300818531c1e4f691e30ba4a66 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Fri, 26 Jun 2026 00:22:15 +0300 Subject: [PATCH 24/55] chore(application): remove useless factory from provider --- src/prodik/bootstrap/di/providers/connection.py | 13 +++---------- tests/conftest.py | 6 ++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/prodik/bootstrap/di/providers/connection.py b/src/prodik/bootstrap/di/providers/connection.py index 5870711..54b882d 100644 --- a/src/prodik/bootstrap/di/providers/connection.py +++ b/src/prodik/bootstrap/di/providers/connection.py @@ -20,7 +20,7 @@ class ConnectionProvider(Provider): @provide(scope=Scope.APP) async def provide_async_engine( self, config: DatabaseConfig - ) -> AsyncIterator[AsyncEngine]: + ) -> AsyncIterator[async_sessionmaker[AsyncSession]]: engine = create_async_engine( url=URL.create( "postgresql+asyncpg", @@ -34,18 +34,11 @@ async def provide_async_engine( max_overflow=96, pool_timeout=30, ) - yield engine - await engine.dispose() - - @provide(scope=Scope.APP) - async def provide_async_sessionmaker( - self, - engine: AsyncEngine, - ) -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( + yield async_sessionmaker( engine, expire_on_commit=False, ) + await engine.dispose() @provide(scope=Scope.REQUEST) async def provide_async_session( diff --git a/tests/conftest.py b/tests/conftest.py index 15cbef3..862daae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ provide, provide_all, ) +from redis.asyncio import Redis from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient from sqlalchemy import URL @@ -69,6 +70,11 @@ def config() -> Config: def startup() -> None: start_mapper() +@pytest.fixture(autouse=True) +async def clear_cache(container: AsyncContainer) -> AsyncIterator[None]: + yield + redis = await container.get(Redis) + await redis.flushdb() @pytest.fixture async def session_repository(container: AsyncContainer) -> SessionRepository: From e999dd7b9efea3399990f1e43439060b4e092013 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Fri, 26 Jun 2026 01:05:05 +0300 Subject: [PATCH 25/55] chore(bootstrap): remove useless factory --- src/prodik/bootstrap/di/providers/connection.py | 1 - tests/conftest.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/prodik/bootstrap/di/providers/connection.py b/src/prodik/bootstrap/di/providers/connection.py index 54b882d..4d18a4f 100644 --- a/src/prodik/bootstrap/di/providers/connection.py +++ b/src/prodik/bootstrap/di/providers/connection.py @@ -4,7 +4,6 @@ from redis.asyncio import Redis from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( - AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, diff --git a/tests/conftest.py b/tests/conftest.py index 862daae..0dde63b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,9 +13,9 @@ provide, provide_all, ) -from redis.asyncio import Redis from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient +from redis.asyncio import Redis from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -70,12 +70,14 @@ def config() -> Config: def startup() -> None: start_mapper() + @pytest.fixture(autouse=True) async def clear_cache(container: AsyncContainer) -> AsyncIterator[None]: yield redis = await container.get(Redis) await redis.flushdb() + @pytest.fixture async def session_repository(container: AsyncContainer) -> SessionRepository: async with container() as test_container: From f78d1c7fa45f6a4d9e2221a2bebd817cb9078ac5 Mon Sep 17 00:00:00 2001 From: LTU Date: Fri, 26 Jun 2026 07:30:57 +0300 Subject: [PATCH 26/55] Update handlers.py --- src/prodik/presentation/exceptions/handlers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/prodik/presentation/exceptions/handlers.py b/src/prodik/presentation/exceptions/handlers.py index e8ce595..f28f803 100644 --- a/src/prodik/presentation/exceptions/handlers.py +++ b/src/prodik/presentation/exceptions/handlers.py @@ -88,8 +88,7 @@ async def application_error_handler( ) -> JSONResponse: status_code = HTTPStatus.INTERNAL_SERVER_ERROR for error in type(exception).mro(): - handler_status = EXCEPTION_HANDLERS.get(error) - if handler_status is not None: + if EXCEPTION_HANDLERS.get(error) is not None: status_code = handler_status break From 62cec157a944461da10d03cc870a2b057dd19b45 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Fri, 26 Jun 2026 07:40:44 +0300 Subject: [PATCH 27/55] feat(infrastructure): transaction logging --- .../infrastructure/transaction_manager.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/prodik/infrastructure/transaction_manager.py b/src/prodik/infrastructure/transaction_manager.py index 0965f3c..0cd7886 100644 --- a/src/prodik/infrastructure/transaction_manager.py +++ b/src/prodik/infrastructure/transaction_manager.py @@ -1,3 +1,6 @@ +import time +import structlog +from uuid import uuid4 from dataclasses import dataclass from types import TracebackType from typing import override @@ -6,13 +9,18 @@ from prodik.application.interfaces.transaction_manager import TransactionManager +logger = structlog.get_logger() @dataclass class TransactionManagerImpl(TransactionManager): _session: AsyncSession @override - async def __aenter__(self) -> None: ... + async def __aenter__(self) -> None: + self._start = time.perf_counter() + self._uuid = uuid4() + logger.debug(f"Transaction {self._uuid} opened") + @override async def __aexit__( @@ -21,7 +29,17 @@ async def __aexit__( exc: BaseException | None, tb: TracebackType | None, ) -> None: - if exc: + success = True + if exc is not None: + success = False await self._session.rollback() - return - await self._session.commit() + else: + await self._session.commit() + + process_time = time.perf_counter() - self._start + + logger.debug( + "Processed transaction", + success=success, + time=process_time, + ) \ No newline at end of file From 26ed3158aea816b32f369f36b3b30666c7238f7c Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 12:18:55 +0300 Subject: [PATCH 28/55] fix(application): typo --- src/prodik/presentation/exceptions/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prodik/presentation/exceptions/handlers.py b/src/prodik/presentation/exceptions/handlers.py index f28f803..06f74ac 100644 --- a/src/prodik/presentation/exceptions/handlers.py +++ b/src/prodik/presentation/exceptions/handlers.py @@ -89,7 +89,7 @@ async def application_error_handler( status_code = HTTPStatus.INTERNAL_SERVER_ERROR for error in type(exception).mro(): if EXCEPTION_HANDLERS.get(error) is not None: - status_code = handler_status + status_code = EXCEPTION_HANDLERS[error] break logger.warning( From afd38ca7484e654c6d54569daa8c09b0a7198a27 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 13:15:42 +0300 Subject: [PATCH 29/55] fix(infrastructure): toctou problem --- .../attach_node_to_group/attach_node.py | 20 -------- .../interfaces/repositories/node.py | 2 +- .../versions/4a8ee55b96fa_unique_constrait.py | 32 +++++++++++++ .../infrastructure/persistence/registry.py | 4 ++ .../infrastructure/repositories/node.py | 47 ++++++++++++------- .../infrastructure/transaction_manager.py | 18 ++++--- 6 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py diff --git a/src/prodik/application/attach_node_to_group/attach_node.py b/src/prodik/application/attach_node_to_group/attach_node.py index da56143..4c8c213 100644 --- a/src/prodik/application/attach_node_to_group/attach_node.py +++ b/src/prodik/application/attach_node_to_group/attach_node.py @@ -2,7 +2,6 @@ from uuid import uuid4 from prodik.application.errors import ( - NodeCannotHaveSameAssociationsError, NodeNotFoundError, ) from prodik.application.interfaces.repositories import ( @@ -49,30 +48,11 @@ async def execute(self, request: AttachNodeRequestDTO) -> list[NodeAssociation]: [{"key": "node_ids", "value": list(request_nodes)}], ) - existing_associations = ( - await self.node_association_repository.get_all_by_group_id( - request.group_id, - ) - ) - await self.access_control_service.ensure_user_can_manipulate_group( user, group, ) - existing_node_ids = {a.node_id for a in existing_associations} - - # TODO @LTU: Fix toctou problem - for node in existing_nodes: - if node.id in existing_node_ids: - raise NodeCannotHaveSameAssociationsError( - "Node cannot have same associations", - [ - {"key": "node_id", "value": node.id}, - {"key": "group_id", "value": group.id}, - ], - ) - associations = [ NodeAssociation.new( node_association_id=NodeAssociationId(uuid4()), diff --git a/src/prodik/application/interfaces/repositories/node.py b/src/prodik/application/interfaces/repositories/node.py index 85619c3..3e83551 100644 --- a/src/prodik/application/interfaces/repositories/node.py +++ b/src/prodik/application/interfaces/repositories/node.py @@ -23,7 +23,7 @@ async def get_by_id( self, node_association_id: NodeAssociationId, ) -> NodeAssociation: ... - async def create_all(self, node_association: list[NodeAssociation]) -> None: ... + async def create_all(self, node_associations: list[NodeAssociation]) -> None: ... async def get_by_node_id(self, node_id: NodeId) -> NodeAssociation | None: ... async def get_all_by_node_id(self, node_id: NodeId) -> list[NodeAssociation]: ... async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: ... diff --git a/src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py b/src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py new file mode 100644 index 0000000..1c7c118 --- /dev/null +++ b/src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py @@ -0,0 +1,32 @@ +"""unique-constrait + +Revision ID: 4a8ee55b96fa +Revises: 0333150881b1 +Create Date: 2026-06-28 09:54:53.692020 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '4a8ee55b96fa' +down_revision: Union[str, Sequence[str], None] = '0333150881b1' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('uq_node_association_record_group_id_node_id', 'node_association_record', ['group_id', 'node_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_node_association_record_group_id_node_id', 'node_association_record', type_='unique') + # ### end Alembic commands ### diff --git a/src/prodik/infrastructure/persistence/registry.py b/src/prodik/infrastructure/persistence/registry.py index 0a36e39..c56b74f 100644 --- a/src/prodik/infrastructure/persistence/registry.py +++ b/src/prodik/infrastructure/persistence/registry.py @@ -10,6 +10,7 @@ MetaData, String, Table, + UniqueConstraint, ) from sqlalchemy.orm import registry @@ -152,6 +153,9 @@ Column("node_id", ForeignKey("node_record.id", ondelete="CASCADE"), nullable=False), Column("created_at", DateTime(timezone=True), nullable=False), Column("updated_at", DateTime(timezone=True), nullable=False), + UniqueConstraint( + "group_id", "node_id", name="uq_node_association_record_group_id_node_id" + ), ) diff --git a/src/prodik/infrastructure/repositories/node.py b/src/prodik/infrastructure/repositories/node.py index 89ebd0c..6ebf9ec 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/prodik/infrastructure/repositories/node.py @@ -2,9 +2,14 @@ import structlog from sqlalchemy import delete, insert, select, update +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import AssociationNotFoundError, NodeNotFoundError +from prodik.application.errors import ( + AssociationNotFoundError, + NodeCannotHaveSameAssociationsError, + NodeNotFoundError, +) from prodik.application.interfaces.repositories import ( NodeAssociationRepository, NodeRepository, @@ -172,28 +177,34 @@ async def get_by_id( return association - async def create_all(self, node_association: list[NodeAssociation]) -> None: + async def create_all(self, node_associations: list[NodeAssociation]) -> None: logger.debug( "Repository create node associations batch", - request_count=len(node_association), + request_count=len(node_associations), ) - if not node_association: + if not node_associations: return - await self.session.execute( - insert(NodeAssociation).values( - [ - { - "id": association.id, - "group_id": association.group_id, - "node_id": association.node_id, - "created_at": association.created_at, - "updated_at": association.updated_at, - } - for association in node_association - ], - ), - ) + try: + await self.session.execute( + insert(NodeAssociation).values( + [ + { + "id": association.id, + "group_id": association.group_id, + "node_id": association.node_id, + "created_at": association.created_at, + "updated_at": association.updated_at, + } + for association in node_associations + ], + ), + ) + except IntegrityError as e: + raise NodeCannotHaveSameAssociationsError( + detail="Node cannot have same associations", + meta=[{"key": "node_associations", "value": node_associations}], + ) from e async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: logger.debug("Repository get node associations by group id", group_id=group_id) diff --git a/src/prodik/infrastructure/transaction_manager.py b/src/prodik/infrastructure/transaction_manager.py index 0cd7886..98569e5 100644 --- a/src/prodik/infrastructure/transaction_manager.py +++ b/src/prodik/infrastructure/transaction_manager.py @@ -1,16 +1,18 @@ import time -import structlog -from uuid import uuid4 from dataclasses import dataclass from types import TracebackType from typing import override +from uuid import uuid4 +import structlog from sqlalchemy.ext.asyncio import AsyncSession +from structlog.contextvars import bind_contextvars from prodik.application.interfaces.transaction_manager import TransactionManager logger = structlog.get_logger() + @dataclass class TransactionManagerImpl(TransactionManager): _session: AsyncSession @@ -18,9 +20,7 @@ class TransactionManagerImpl(TransactionManager): @override async def __aenter__(self) -> None: self._start = time.perf_counter() - self._uuid = uuid4() - logger.debug(f"Transaction {self._uuid} opened") - + bind_contextvars(transaction_id=uuid4()) @override async def __aexit__( @@ -29,17 +29,15 @@ async def __aexit__( exc: BaseException | None, tb: TracebackType | None, ) -> None: - success = True if exc is not None: - success = False await self._session.rollback() else: await self._session.commit() - + process_time = time.perf_counter() - self._start logger.debug( "Processed transaction", - success=success, + success=exc is None, time=process_time, - ) \ No newline at end of file + ) From 190b442e17a217a02f311f955fdf217a9f85f9e1 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 13:39:58 +0300 Subject: [PATCH 30/55] tests: delete group tests --- tests/e2e/group/test_delete_group.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/e2e/group/test_delete_group.py diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py new file mode 100644 index 0000000..01b1c1c --- /dev/null +++ b/tests/e2e/group/test_delete_group.py @@ -0,0 +1,61 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import GroupRepository +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, GroupFactory, UserFactory + + +@pytest.mark.asyncio +async def test_delete_group_ok( + transport: AsyncClient, + group_repository: GroupRepository, + group_factory: GroupFactory, + user_factory: UserFactory, + company_factory: CompanyFactory, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + group = await group_factory.build(company=company) + + response = await transport.delete( + f"/groups/{group.id}", + headers=authorization_headers(user_factory_response.access_token), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_group_user_have_not_access( + transport: AsyncClient, + group_repository: GroupRepository, + group_factory: GroupFactory, + user_factory: UserFactory, + company_factory: CompanyFactory, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + + company = await company_factory.build(owner=users[0].user) + + group = await group_factory.build(company=company) + + response = await transport.delete( + f"/groups/{group.id}", headers=authorization_headers(users[1].access_token) + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 1 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) From 86c50ce5b708128fd6a21328a0ce8b948f29d74c Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 17:17:25 +0300 Subject: [PATCH 31/55] tests: create node tests --- .../interfaces/repositories/node.py | 2 + .../infrastructure/repositories/node.py | 20 +++++ tests/conftest.py | 9 +++ tests/e2e/group/test_delete_group.py | 2 +- tests/e2e/node/__init__.py | 0 tests/e2e/node/test_create_node.py | 77 +++++++++++++++++++ tests/factories/models/node.py | 0 tests/factories/schemas/__init__.py | 2 + tests/factories/schemas/node.py | 7 ++ 9 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/node/__init__.py create mode 100644 tests/e2e/node/test_create_node.py create mode 100644 tests/factories/models/node.py create mode 100644 tests/factories/schemas/node.py diff --git a/src/prodik/application/interfaces/repositories/node.py b/src/prodik/application/interfaces/repositories/node.py index 3e83551..3370ac5 100644 --- a/src/prodik/application/interfaces/repositories/node.py +++ b/src/prodik/application/interfaces/repositories/node.py @@ -1,5 +1,6 @@ from typing import Protocol +from prodik.domain.company import CompanyId from prodik.domain.group import GroupId from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId @@ -8,6 +9,7 @@ class NodeRepository(Protocol): async def create(self, node: Node) -> None: ... async def update(self, node: Node) -> None: ... async def delete(self, node: Node) -> None: ... + async def get_all_by_company_id(self, company_id: CompanyId) -> list[Node]: ... async def get_by_id(self, node_id: NodeId) -> Node: ... async def get_all_by_ids(self, nodes_ids: list[NodeId]) -> list[Node]: ... async def get_all_by_associations( diff --git a/src/prodik/infrastructure/repositories/node.py b/src/prodik/infrastructure/repositories/node.py index 6ebf9ec..08d8335 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/prodik/infrastructure/repositories/node.py @@ -14,6 +14,7 @@ NodeAssociationRepository, NodeRepository, ) +from prodik.domain.company import CompanyId from prodik.domain.group import GroupId from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId @@ -60,6 +61,25 @@ async def delete(self, node: Node) -> None: ), ) + async def get_all_by_company_id(self, company_id: CompanyId) -> list[Node]: + logger.debug( + "Repository get nodes by company id", + company_id=company_id, + ) + + result = await self.session.execute( + select(Node).where( + Node.company_id == company_id # type: ignore + ), + ) + + result_nodes = list(result.scalars().all()) + logger.debug( + "Repository fetched nodes by company id", + found_count=len(result_nodes), + ) + return result_nodes + async def get_by_id(self, node_id: NodeId) -> Node: logger.debug("Repository get node by id", node_id=node_id) result = await self.session.execute( diff --git a/tests/conftest.py b/tests/conftest.py index 0dde63b..a93df38 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ CompanyRepository, GroupRepository, LocalAuthorizationRepository, + NodeRepository, RolePermissionsRepository, RoleRepository, SessionRepository, @@ -92,6 +93,14 @@ async def local_authorization_repository( return await test_container.get(LocalAuthorizationRepository) # type: ignore[no-any-return] +@pytest.fixture +async def node_repository( + container: AsyncContainer, +) -> NodeRepository: + async with container() as test_container: + return await test_container.get(NodeRepository) # type: ignore[no-any-return] + + @pytest.fixture async def group_repository( container: AsyncContainer, diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py index 01b1c1c..cae9281 100644 --- a/tests/e2e/group/test_delete_group.py +++ b/tests/e2e/group/test_delete_group.py @@ -34,7 +34,7 @@ async def test_delete_group_ok( @pytest.mark.asyncio -async def test_delete_group_user_have_not_access( +async def test_delete_group_user_without_correct_rights( transport: AsyncClient, group_repository: GroupRepository, group_factory: GroupFactory, diff --git a/tests/e2e/node/__init__.py b/tests/e2e/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/node/test_create_node.py b/tests/e2e/node/test_create_node.py new file mode 100644 index 0000000..da70c5e --- /dev/null +++ b/tests/e2e/node/test_create_node.py @@ -0,0 +1,77 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict, IsStr +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import NodeRepository +from prodik.domain.node import NodeDescription, NodeName +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, GroupFactory, UserFactory +from tests.factories.schemas import CreateNodeRequestFactory + + +@pytest.mark.asyncio +async def test_create_node_ok( + transport: AsyncClient, + node_repository: NodeRepository, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + user_factory_response = await user_factory.build() + company = await company_factory.build(owner=user_factory_response.user) + group = await group_factory.build(company=company) + + request = CreateNodeRequestFactory.build(group_id=group.id) + + response = await transport.post( + "/nodes/", + json=request.model_dump(mode="json"), + headers=authorization_headers(user_factory_response.access_token), + ) + content = response.json() + + node = await node_repository.get_by_id(content["id"]) + + assert response.status_code == HTTPStatus.CREATED + assert node == IsPartialDataclass( + name=NodeName(request.name), + description=NodeDescription(request.description), + ) + assert content == IsPartialDict( + id=IsStr, + name=request.name, + description=request.description, + company_id=str(company.id), + ) + + +@pytest.mark.asyncio +async def test_create_node_without_correct_rights( + transport: AsyncClient, + node_repository: NodeRepository, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + group = await group_factory.build(company=company) + + request = CreateNodeRequestFactory.build(group_id=group.id) + + response = await transport.post( + "/nodes/", + json=request.model_dump(mode="json"), + headers=authorization_headers(users[1].access_token), + ) + + nodes = await node_repository.get_all_by_company_id(company.id) + + assert len(nodes) == 0 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/models/node.py b/tests/factories/models/node.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index cfb97a3..b61a581 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -5,9 +5,11 @@ ) from .company import RegisterCompanyRequestFactory from .group import CreateGroupRequestFactory +from .node import CreateNodeRequestFactory __all__ = ( "CreateGroupRequestFactory", + "CreateNodeRequestFactory", "LoginRequestFactory", "RefreshTokenRequestFactory", "RegisterCompanyRequestFactory", diff --git a/tests/factories/schemas/node.py b/tests/factories/schemas/node.py new file mode 100644 index 0000000..e0ea43a --- /dev/null +++ b/tests/factories/schemas/node.py @@ -0,0 +1,7 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.node import CreateNodeRequest + + +class CreateNodeRequestFactory(ModelFactory[CreateNodeRequest]): + __use_examples__ = True From fcd33efa5443fb45c3b5a9fa3af20cc8d9e25bc4 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 19:13:12 +0300 Subject: [PATCH 32/55] tests: delete/cerate node tests --- .../detach_node_from_group/detach_node.py | 6 +- .../interfaces/repositories/node.py | 1 - tests/conftest.py | 22 ++++++ tests/e2e/node/test_create_node.py | 16 +++- tests/e2e/node/test_delete_node.py | 76 +++++++++++++++++++ tests/factories/models/__init__.py | 3 +- tests/factories/models/node.py | 39 ++++++++++ 7 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 tests/e2e/node/test_delete_node.py diff --git a/src/prodik/application/detach_node_from_group/detach_node.py b/src/prodik/application/detach_node_from_group/detach_node.py index 77bf73f..c63e8ac 100644 --- a/src/prodik/application/detach_node_from_group/detach_node.py +++ b/src/prodik/application/detach_node_from_group/detach_node.py @@ -44,12 +44,12 @@ async def execute(self, association_id: NodeAssociationId) -> None: group, ) - existing_node_association = ( - await self.node_association_repository.get_by_node_id( + existing_node_associations = ( + await self.node_association_repository.get_all_by_node_id( association.node_id, ) ) - if existing_node_association is None: + if len(existing_node_associations) == 1: raise NodeMustHaveAtLeastOneAssociationError( "Node must have at least one association", [{"key": "node_id", "value": association.node_id}], diff --git a/src/prodik/application/interfaces/repositories/node.py b/src/prodik/application/interfaces/repositories/node.py index 3370ac5..38acf87 100644 --- a/src/prodik/application/interfaces/repositories/node.py +++ b/src/prodik/application/interfaces/repositories/node.py @@ -26,6 +26,5 @@ async def get_by_id( node_association_id: NodeAssociationId, ) -> NodeAssociation: ... async def create_all(self, node_associations: list[NodeAssociation]) -> None: ... - async def get_by_node_id(self, node_id: NodeId) -> NodeAssociation | None: ... async def get_all_by_node_id(self, node_id: NodeId) -> list[NodeAssociation]: ... async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: ... diff --git a/tests/conftest.py b/tests/conftest.py index a93df38..5b19dea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ CompanyRepository, GroupRepository, LocalAuthorizationRepository, + NodeAssociationRepository, NodeRepository, RolePermissionsRepository, RoleRepository, @@ -58,6 +59,7 @@ from tests.factories.models import ( CompanyFactory, GroupFactory, + NodeFactory, UserFactory, ) @@ -101,6 +103,14 @@ async def node_repository( return await test_container.get(NodeRepository) # type: ignore[no-any-return] +@pytest.fixture +async def node_association_repository( + container: AsyncContainer, +) -> NodeAssociationRepository: + async with container() as test_container: + return await test_container.get(NodeAssociationRepository) # type: ignore[no-any-return] + + @pytest.fixture async def group_repository( container: AsyncContainer, @@ -139,6 +149,18 @@ async def role_repository(container: AsyncContainer) -> RoleRepository: return await test_container.get(RoleRepository) # type: ignore[no-any-return] +@pytest.fixture +async def node_factory(container: AsyncContainer) -> NodeFactory: + async with container() as test_container: + return NodeFactory( + node_repository=await test_container.get(NodeRepository), + transaction_manager=await test_container.get(TransactionManager), + node_association_repository=await test_container.get( + NodeAssociationRepository + ), + ) + + @pytest.fixture async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: diff --git a/tests/e2e/node/test_create_node.py b/tests/e2e/node/test_create_node.py index da70c5e..ff4f822 100644 --- a/tests/e2e/node/test_create_node.py +++ b/tests/e2e/node/test_create_node.py @@ -4,7 +4,10 @@ from dirty_equals import IsPartialDataclass, IsPartialDict, IsStr from httpx import AsyncClient -from prodik.application.interfaces.repositories import NodeRepository +from prodik.application.interfaces.repositories import ( + NodeAssociationRepository, + NodeRepository, +) from prodik.domain.node import NodeDescription, NodeName from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, GroupFactory, UserFactory @@ -15,6 +18,7 @@ async def test_create_node_ok( transport: AsyncClient, node_repository: NodeRepository, + node_association_repository: NodeAssociationRepository, user_factory: UserFactory, company_factory: CompanyFactory, group_factory: GroupFactory, @@ -33,8 +37,12 @@ async def test_create_node_ok( content = response.json() node = await node_repository.get_by_id(content["id"]) + node_associations = await node_association_repository.get_all_by_node_id( + content["id"] + ) assert response.status_code == HTTPStatus.CREATED + assert len(node_associations) == 1 assert node == IsPartialDataclass( name=NodeName(request.name), description=NodeDescription(request.description), @@ -51,6 +59,7 @@ async def test_create_node_ok( async def test_create_node_without_correct_rights( transport: AsyncClient, node_repository: NodeRepository, + node_association_repository: NodeAssociationRepository, user_factory: UserFactory, company_factory: CompanyFactory, group_factory: GroupFactory, @@ -66,12 +75,15 @@ async def test_create_node_without_correct_rights( json=request.model_dump(mode="json"), headers=authorization_headers(users[1].access_token), ) + content = response.json() nodes = await node_repository.get_all_by_company_id(company.id) + node_associations = await node_association_repository.get_all_by_group_id(group.id) assert len(nodes) == 0 + assert len(node_associations) == 0 assert response.status_code == HTTPStatus.FORBIDDEN - assert response.json() == IsPartialDict( + assert content == IsPartialDict( detail="Not enough rights to perform operation", meta=None, ) diff --git a/tests/e2e/node/test_delete_node.py b/tests/e2e/node/test_delete_node.py new file mode 100644 index 0000000..dda1777 --- /dev/null +++ b/tests/e2e/node/test_delete_node.py @@ -0,0 +1,76 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import ( + NodeAssociationRepository, + NodeRepository, +) +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_delete_node_ok( + transport: AsyncClient, + node_factory: NodeFactory, + group_factory: GroupFactory, + company_factory: CompanyFactory, + user_factory: UserFactory, + node_repository: NodeRepository, + node_association_repository: NodeAssociationRepository, +) -> None: + user_factory_response = await user_factory.build() + company = await company_factory.build(owner=user_factory_response.user) + group = await group_factory.build(company=company) + node = await node_factory.build(company, group) + + response = await transport.delete( + f"/nodes/{node.id}", + headers=authorization_headers(user_factory_response.access_token), + ) + + nodes = await node_repository.get_all_by_company_id(company.id) + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(nodes) == 0 + assert len(node_associations) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_node_user_without_correct_rights( + transport: AsyncClient, + node_factory: NodeFactory, + group_factory: GroupFactory, + company_factory: CompanyFactory, + user_factory: UserFactory, + node_repository: NodeRepository, + node_association_repository: NodeAssociationRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + group = await group_factory.build(company=company) + node = await node_factory.build(company, group) + + response = await transport.delete( + f"/nodes/{node.id}", headers=authorization_headers(users[1].access_token) + ) + + nodes = await node_repository.get_all_by_company_id(company.id) + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(nodes) == 1 + assert len(node_associations) == 1 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py index e409482..799a213 100644 --- a/tests/factories/models/__init__.py +++ b/tests/factories/models/__init__.py @@ -1,5 +1,6 @@ from .company import CompanyFactory from .group import GroupFactory +from .node import NodeFactory from .user import UserFactory -__all__ = ("CompanyFactory", "GroupFactory", "UserFactory") +__all__ = ("CompanyFactory", "GroupFactory", "NodeFactory", "UserFactory") diff --git a/tests/factories/models/node.py b/tests/factories/models/node.py index e69de29..836b536 100644 --- a/tests/factories/models/node.py +++ b/tests/factories/models/node.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import ( + NodeAssociationRepository, + NodeRepository, +) +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.domain.company import Company +from prodik.domain.group import Group +from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId +from tests.factories.common import generate_random_string + + +@dataclass +class NodeFactory: + transaction_manager: TransactionManager + node_association_repository: NodeAssociationRepository + node_repository: NodeRepository + + async def build(self, company: Company, group: Group) -> Node: + async with self.transaction_manager: + node = Node.new( + node_id=NodeId(uuid4()), + company=company, + name=generate_random_string(), + description=generate_random_string(), + ) + + node_association = NodeAssociation.new( + node_association_id=NodeAssociationId(uuid4()), + node=node, + group=group, + ) + + await self.node_repository.create(node) + await self.node_association_repository.create(node_association) + + return node From dc75b6d85f49fe938fbb2ad9f2c41d3d35fbdc21 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 19:18:45 +0300 Subject: [PATCH 33/55] tests: delete group tests extended --- tests/e2e/group/test_delete_group.py | 66 +++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py index cae9281..2a2d0ad 100644 --- a/tests/e2e/group/test_delete_group.py +++ b/tests/e2e/group/test_delete_group.py @@ -4,9 +4,17 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import GroupRepository +from prodik.application.interfaces.repositories import ( + GroupRepository, + NodeAssociationRepository, +) from tests.factories.common import authorization_headers -from tests.factories.models import CompanyFactory, GroupFactory, UserFactory +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeFactory, + UserFactory, +) @pytest.mark.asyncio @@ -59,3 +67,57 @@ async def test_delete_group_user_without_correct_rights( detail="Not enough rights to perform operation", meta=None, ) + + +@pytest.mark.asyncio +async def test_delete_group_with_childs( + transport: AsyncClient, + group_repository: GroupRepository, + group_factory: GroupFactory, + user_factory: UserFactory, + company_factory: CompanyFactory, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + parent_group = await group_factory.build(company=company) + await group_factory.build(company=company, parent_group=parent_group) + + response = await transport.delete( + f"/groups/{parent_group.id}", + headers=authorization_headers(user_factory_response.access_token), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + + assert len(groups) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_group_with_associations( + transport: AsyncClient, + group_repository: GroupRepository, + group_factory: GroupFactory, + node_factory: NodeFactory, + user_factory: UserFactory, + node_association_repository: NodeAssociationRepository, + company_factory: CompanyFactory, +) -> None: + user_factory_response = await user_factory.build() + + company = await company_factory.build(owner=user_factory_response.user) + group = await group_factory.build(company=company) + node = await node_factory.build(company=company, group=group) + + response = await transport.delete( + f"/groups/{group.id}", + headers=authorization_headers(user_factory_response.access_token), + ) + + groups = await group_repository.get_all_by_company_id(company.id) + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(groups) == 0 + assert len(node_associations) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT From 11ffe24a2a82faf68156ee4fcfe7f95d0f9b1cce Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 19:24:46 +0300 Subject: [PATCH 34/55] tests: some changes --- tests/e2e/group/test_delete_group.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py index 2a2d0ad..2c8f5e4 100644 --- a/tests/e2e/group/test_delete_group.py +++ b/tests/e2e/group/test_delete_group.py @@ -26,7 +26,6 @@ async def test_delete_group_ok( company_factory: CompanyFactory, ) -> None: user_factory_response = await user_factory.build() - company = await company_factory.build(owner=user_factory_response.user) group = await group_factory.build(company=company) @@ -50,9 +49,7 @@ async def test_delete_group_user_without_correct_rights( company_factory: CompanyFactory, ) -> None: users = (await user_factory.build(), await user_factory.build()) - company = await company_factory.build(owner=users[0].user) - group = await group_factory.build(company=company) response = await transport.delete( @@ -78,9 +75,9 @@ async def test_delete_group_with_childs( company_factory: CompanyFactory, ) -> None: user_factory_response = await user_factory.build() - company = await company_factory.build(owner=user_factory_response.user) parent_group = await group_factory.build(company=company) + await group_factory.build(company=company, parent_group=parent_group) response = await transport.delete( From f9e9f00beac6d46f37a7a5b33709a3d4f63605e7 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 21:54:09 +0300 Subject: [PATCH 35/55] tests: test update node --- .../application/update_node/update_node.py | 6 +- src/prodik/presentation/views/node.py | 13 +--- tests/e2e/node/test_update_node.py | 78 +++++++++++++++++++ tests/factories/schemas/__init__.py | 3 +- tests/factories/schemas/node.py | 6 +- 5 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 tests/e2e/node/test_update_node.py diff --git a/src/prodik/application/update_node/update_node.py b/src/prodik/application/update_node/update_node.py index 0aab9cc..1ec73e3 100644 --- a/src/prodik/application/update_node/update_node.py +++ b/src/prodik/application/update_node/update_node.py @@ -8,7 +8,7 @@ ) from prodik.application.interfaces.transaction_manager import TransactionManager from prodik.application.services import AccessControlService -from prodik.domain.node import Node, NodeId +from prodik.domain.node import NodeId @dataclass(frozen=True, slots=True, kw_only=True) @@ -27,7 +27,7 @@ class UpdateNodeInteractor: group_repository: GroupRepository transaction_manager: TransactionManager - async def execute(self, request: UpdateNodeRequestDTO) -> Node: + async def execute(self, request: UpdateNodeRequestDTO) -> None: async with self.transaction_manager: user = await self.access_control_service.get_authorized_user() @@ -47,5 +47,3 @@ async def execute(self, request: UpdateNodeRequestDTO) -> Node: node.set_name(request.name) node.set_description(request.description) - - return node diff --git a/src/prodik/presentation/views/node.py b/src/prodik/presentation/views/node.py index 4777e08..b94041b 100644 --- a/src/prodik/presentation/views/node.py +++ b/src/prodik/presentation/views/node.py @@ -93,13 +93,13 @@ async def get_neighbours( ] -@router.put("/{node_id}") +@router.put("/{node_id}", status_code=HTTPStatus.NO_CONTENT) async def update_node( node_id: NodeId, request: UpdateNodeRequest, interactor: FromDishka[UpdateNodeInteractor], -) -> NodeSchema: - result = await interactor.execute( +) -> None: + await interactor.execute( UpdateNodeRequestDTO( name=request.name, description=request.description, @@ -107,13 +107,6 @@ async def update_node( ), ) - return NodeSchema( - id=result.id, - name=result.name.value, - description=result.description.value, - company_id=result.company_id, - ) - @router.delete("/{node_id}", status_code=HTTPStatus.NO_CONTENT) async def delete_node( diff --git a/tests/e2e/node/test_update_node.py b/tests/e2e/node/test_update_node.py new file mode 100644 index 0000000..303709b --- /dev/null +++ b/tests/e2e/node/test_update_node.py @@ -0,0 +1,78 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import NodeRepository +from prodik.domain.node import NodeDescription, NodeName +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeFactory, + UserFactory, +) +from tests.factories.schemas.node import UpdateNodeRequestFactory + + +@pytest.mark.asyncio +async def test_update_node_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + node_repository: NodeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + node = await node_factory.build(company=company, group=group) + + request = UpdateNodeRequestFactory.build() + + response = await transport.put( + f"/nodes/{node.id}", + json=request.model_dump(mode="json"), + headers=authorization_headers(user.access_token), + ) + + node = await node_repository.get_by_id(node.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert node == IsPartialDataclass( + name=NodeName(request.name), + description=NodeDescription(request.description), + ) + + +@pytest.mark.asyncio +async def test_update_node_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + node_repository: NodeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + group = await group_factory.build(company=company) + node = await node_factory.build(company=company, group=group) + + request = UpdateNodeRequestFactory.build() + + response = await transport.put( + f"/nodes/{node.id}", + json=request.model_dump(mode="json"), + headers=authorization_headers(users[1].access_token), + ) + + node = await node_repository.get_by_id(node.id) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index b61a581..fb7a10e 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -5,7 +5,7 @@ ) from .company import RegisterCompanyRequestFactory from .group import CreateGroupRequestFactory -from .node import CreateNodeRequestFactory +from .node import CreateNodeRequestFactory, UpdateNodeRequestFactory __all__ = ( "CreateGroupRequestFactory", @@ -14,4 +14,5 @@ "RefreshTokenRequestFactory", "RegisterCompanyRequestFactory", "RegisterRequestFactory", + "UpdateNodeRequestFactory", ) diff --git a/tests/factories/schemas/node.py b/tests/factories/schemas/node.py index e0ea43a..576e089 100644 --- a/tests/factories/schemas/node.py +++ b/tests/factories/schemas/node.py @@ -1,7 +1,11 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.node import CreateNodeRequest +from prodik.presentation.schemas.node import CreateNodeRequest, UpdateNodeRequest class CreateNodeRequestFactory(ModelFactory[CreateNodeRequest]): __use_examples__ = True + + +class UpdateNodeRequestFactory(ModelFactory[UpdateNodeRequest]): + __use_examples__ = True From 217cb1b896c8a81f7c374b01a752618e0b20297f Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 21:58:37 +0300 Subject: [PATCH 36/55] tests: get nodes by group tests --- tests/e2e/node/test_get_nodes_by_group.py | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/e2e/node/test_get_nodes_by_group.py diff --git a/tests/e2e/node/test_get_nodes_by_group.py b/tests/e2e/node/test_get_nodes_by_group.py new file mode 100644 index 0000000..2720284 --- /dev/null +++ b/tests/e2e/node/test_get_nodes_by_group.py @@ -0,0 +1,58 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_get_nodes_by_group_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + nodes = [await node_factory.build(company=company, group=group) for _ in range(5)] + + response = await transport.get( + f"/nodes/{group.id}", + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert len(response.json()) == len(nodes) + + +@pytest.mark.asyncio +async def test_get_nodes_by_group_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + group = await group_factory.build(company=company) + + response = await transport.get( + f"/nodes/{group.id}", + headers=authorization_headers(users[1].access_token), + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) From 6e03246f81efc6abde48b9edd35c2aa4e8da59a5 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sun, 28 Jun 2026 22:22:53 +0300 Subject: [PATCH 37/55] tests: attach node tests --- .../infrastructure/repositories/node.py | 2 +- src/prodik/presentation/views/node.py | 4 +- tests/e2e/node/test_attach_node.py | 121 ++++++++++++++++++ tests/factories/schemas/__init__.py | 7 +- tests/factories/schemas/node.py | 10 +- 5 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/node/test_attach_node.py diff --git a/src/prodik/infrastructure/repositories/node.py b/src/prodik/infrastructure/repositories/node.py index 08d8335..9c41490 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/prodik/infrastructure/repositories/node.py @@ -223,7 +223,7 @@ async def create_all(self, node_associations: list[NodeAssociation]) -> None: except IntegrityError as e: raise NodeCannotHaveSameAssociationsError( detail="Node cannot have same associations", - meta=[{"key": "node_associations", "value": node_associations}], + meta=None, ) from e async def get_all_by_group_id(self, group_id: GroupId) -> list[NodeAssociation]: diff --git a/src/prodik/presentation/views/node.py b/src/prodik/presentation/views/node.py index b94041b..bbeef6b 100644 --- a/src/prodik/presentation/views/node.py +++ b/src/prodik/presentation/views/node.py @@ -116,7 +116,7 @@ async def delete_node( await interactor.execute(node_id) -@router.post("/attach", status_code=HTTPStatus.CREATED) +@router.post("/associations", status_code=HTTPStatus.CREATED) async def attach_nodes( request: AttachNodeRequest, interactor: FromDishka[AttachNodeInteractor], @@ -135,7 +135,7 @@ async def attach_nodes( ] -@router.delete("/association/{association_id}", status_code=HTTPStatus.NO_CONTENT) +@router.delete("/associations/{association_id}", status_code=HTTPStatus.NO_CONTENT) async def detach_node( association_id: NodeAssociationId, interactor: FromDishka[DetachNodeInteractor], diff --git a/tests/e2e/node/test_attach_node.py b/tests/e2e/node/test_attach_node.py new file mode 100644 index 0000000..1df10ed --- /dev/null +++ b/tests/e2e/node/test_attach_node.py @@ -0,0 +1,121 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import ( + NodeAssociationRepository, +) +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeFactory, + UserFactory, +) +from tests.factories.schemas import AttachNodeRequestFactory + +# 1. Проверка прав + + +@pytest.mark.asyncio +async def test_attach_node_ok( + transport: AsyncClient, + node_association_repository: NodeAssociationRepository, + user_factory: UserFactory, + node_factory: NodeFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + user_factory_response = await user_factory.build() + company = await company_factory.build(owner=user_factory_response.user) + group_a = await group_factory.build(company=company) + group_b = await group_factory.build(company=company) + node = await node_factory.build(group=group_a, company=company) + + request = AttachNodeRequestFactory.build( + group_id=group_b.id, + nodes=[node.id], + ) + + response = await transport.post( + "/nodes/associations", + json=request.model_dump(mode="json"), + headers=authorization_headers(user_factory_response.access_token), + ) + + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(node_associations) == 2 # noqa: PLR2004 + assert response.status_code == HTTPStatus.CREATED + + +@pytest.mark.asyncio +async def test_attach_node_already_have_association( + transport: AsyncClient, + node_association_repository: NodeAssociationRepository, + user_factory: UserFactory, + node_factory: NodeFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + user_factory_response = await user_factory.build() + company = await company_factory.build(owner=user_factory_response.user) + group = await group_factory.build(company=company) + node = await node_factory.build(group=group, company=company) + + request = AttachNodeRequestFactory.build( + group_id=group.id, + nodes=[node.id], + ) + + response = await transport.post( + "/nodes/associations", + json=request.model_dump(mode="json"), + headers=authorization_headers(user_factory_response.access_token), + ) + + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(node_associations) == 1 + assert response.status_code == HTTPStatus.CONFLICT + assert response.json() == IsPartialDict( + detail="Node cannot have same associations", + meta=None, + ) + + +@pytest.mark.asyncio +async def test_attach_node_user_without_correct_rights( + transport: AsyncClient, + node_association_repository: NodeAssociationRepository, + user_factory: UserFactory, + node_factory: NodeFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + group = await group_factory.build(company=company) + node = await node_factory.build(group=group, company=company) + + request = AttachNodeRequestFactory.build( + group_id=group.id, + nodes=[node.id], + ) + + response = await transport.post( + "/nodes/associations", + json=request.model_dump(mode="json"), + headers=authorization_headers(users[1].access_token), + ) + + node_associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(node_associations) == 1 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index fb7a10e..6f1c7ce 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -5,9 +5,14 @@ ) from .company import RegisterCompanyRequestFactory from .group import CreateGroupRequestFactory -from .node import CreateNodeRequestFactory, UpdateNodeRequestFactory +from .node import ( + AttachNodeRequestFactory, + CreateNodeRequestFactory, + UpdateNodeRequestFactory, +) __all__ = ( + "AttachNodeRequestFactory", "CreateGroupRequestFactory", "CreateNodeRequestFactory", "LoginRequestFactory", diff --git a/tests/factories/schemas/node.py b/tests/factories/schemas/node.py index 576e089..dcf4cfe 100644 --- a/tests/factories/schemas/node.py +++ b/tests/factories/schemas/node.py @@ -1,6 +1,10 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.node import CreateNodeRequest, UpdateNodeRequest +from prodik.presentation.schemas.node import ( + AttachNodeRequest, + CreateNodeRequest, + UpdateNodeRequest, +) class CreateNodeRequestFactory(ModelFactory[CreateNodeRequest]): @@ -9,3 +13,7 @@ class CreateNodeRequestFactory(ModelFactory[CreateNodeRequest]): class UpdateNodeRequestFactory(ModelFactory[UpdateNodeRequest]): __use_examples__ = True + + +class AttachNodeRequestFactory(ModelFactory[AttachNodeRequest]): + __use_examples__ = True From 0c9f2123c7c3790a8c3eeb6142cc751e3631d214 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 29 Jun 2026 21:13:40 +0300 Subject: [PATCH 38/55] tests: fix toctou problem + detach node tests --- .../detach_node_from_group/detach_node.py | 12 -- .../infrastructure/repositories/node.py | 23 +++- tests/conftest.py | 12 ++ tests/e2e/group/test_delete_group.py | 2 +- tests/e2e/node/test_attach_node.py | 6 +- tests/e2e/node/test_delete_node.py | 4 +- tests/e2e/node/test_detach_node.py | 107 ++++++++++++++++++ tests/e2e/node/test_update_node.py | 4 +- tests/factories/models/__init__.py | 10 +- tests/factories/models/node.py | 24 +++- 10 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 tests/e2e/node/test_detach_node.py diff --git a/src/prodik/application/detach_node_from_group/detach_node.py b/src/prodik/application/detach_node_from_group/detach_node.py index c63e8ac..6a9707f 100644 --- a/src/prodik/application/detach_node_from_group/detach_node.py +++ b/src/prodik/application/detach_node_from_group/detach_node.py @@ -2,7 +2,6 @@ from prodik.application.errors import ( GroupNotFoundError, - NodeMustHaveAtLeastOneAssociationError, ) from prodik.application.interfaces.repositories import ( CompanyRepository, @@ -44,15 +43,4 @@ async def execute(self, association_id: NodeAssociationId) -> None: group, ) - existing_node_associations = ( - await self.node_association_repository.get_all_by_node_id( - association.node_id, - ) - ) - if len(existing_node_associations) == 1: - raise NodeMustHaveAtLeastOneAssociationError( - "Node must have at least one association", - [{"key": "node_id", "value": association.node_id}], - ) - await self.node_association_repository.delete(association) diff --git a/src/prodik/infrastructure/repositories/node.py b/src/prodik/infrastructure/repositories/node.py index 9c41490..a887d66 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/prodik/infrastructure/repositories/node.py @@ -1,13 +1,14 @@ from dataclasses import dataclass import structlog -from sqlalchemy import delete, insert, select, update +from sqlalchemy import and_, delete, func, insert, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from prodik.application.errors import ( AssociationNotFoundError, NodeCannotHaveSameAssociationsError, + NodeMustHaveAtLeastOneAssociationError, NodeNotFoundError, ) from prodik.application.interfaces.repositories import ( @@ -162,12 +163,28 @@ async def delete(self, node_association: NodeAssociation) -> None: "Repository delete node association", association_id=node_association.id, ) - await self.session.execute( + result = await self.session.execute( delete(NodeAssociation).where( - NodeAssociation.id == node_association.id, # type: ignore + and_( + NodeAssociation.id == node_association.id, # type: ignore + select(func.count()) + .where( + NodeAssociation.node_id == node_association.node_id # type: ignore + ) + .scalar_subquery() + > 1, + ) ), ) + if result.rowcount == 0: # type: ignore[attr-defined] + raise NodeMustHaveAtLeastOneAssociationError( + detail="Node must have at least one association", + meta=[ + {"key": "node_association_id", "value": node_association.node_id} + ], + ) + async def get_by_id( self, node_association_id: NodeAssociationId, diff --git a/tests/conftest.py b/tests/conftest.py index 5b19dea..1402be7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,6 +59,7 @@ from tests.factories.models import ( CompanyFactory, GroupFactory, + NodeAssociationFactory, NodeFactory, UserFactory, ) @@ -161,6 +162,17 @@ async def node_factory(container: AsyncContainer) -> NodeFactory: ) +@pytest.fixture +async def node_association_factory(container: AsyncContainer) -> NodeAssociationFactory: + async with container() as test_container: + return NodeAssociationFactory( + transaction_manager=await test_container.get(TransactionManager), + node_association_repository=await test_container.get( + NodeAssociationRepository + ), + ) + + @pytest.fixture async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py index 2c8f5e4..8cb99f7 100644 --- a/tests/e2e/group/test_delete_group.py +++ b/tests/e2e/group/test_delete_group.py @@ -105,7 +105,7 @@ async def test_delete_group_with_associations( company = await company_factory.build(owner=user_factory_response.user) group = await group_factory.build(company=company) - node = await node_factory.build(company=company, group=group) + node, _ = await node_factory.build(company=company, group=group) response = await transport.delete( f"/groups/{group.id}", diff --git a/tests/e2e/node/test_attach_node.py b/tests/e2e/node/test_attach_node.py index 1df10ed..072bf70 100644 --- a/tests/e2e/node/test_attach_node.py +++ b/tests/e2e/node/test_attach_node.py @@ -32,7 +32,7 @@ async def test_attach_node_ok( company = await company_factory.build(owner=user_factory_response.user) group_a = await group_factory.build(company=company) group_b = await group_factory.build(company=company) - node = await node_factory.build(group=group_a, company=company) + node, _ = await node_factory.build(group=group_a, company=company) request = AttachNodeRequestFactory.build( group_id=group_b.id, @@ -63,7 +63,7 @@ async def test_attach_node_already_have_association( user_factory_response = await user_factory.build() company = await company_factory.build(owner=user_factory_response.user) group = await group_factory.build(company=company) - node = await node_factory.build(group=group, company=company) + node, _ = await node_factory.build(group=group, company=company) request = AttachNodeRequestFactory.build( group_id=group.id, @@ -98,7 +98,7 @@ async def test_attach_node_user_without_correct_rights( users = (await user_factory.build(), await user_factory.build()) company = await company_factory.build(owner=users[0].user) group = await group_factory.build(company=company) - node = await node_factory.build(group=group, company=company) + node, _ = await node_factory.build(group=group, company=company) request = AttachNodeRequestFactory.build( group_id=group.id, diff --git a/tests/e2e/node/test_delete_node.py b/tests/e2e/node/test_delete_node.py index dda1777..b967c29 100644 --- a/tests/e2e/node/test_delete_node.py +++ b/tests/e2e/node/test_delete_node.py @@ -30,7 +30,7 @@ async def test_delete_node_ok( user_factory_response = await user_factory.build() company = await company_factory.build(owner=user_factory_response.user) group = await group_factory.build(company=company) - node = await node_factory.build(company, group) + node, _ = await node_factory.build(company, group) response = await transport.delete( f"/nodes/{node.id}", @@ -58,7 +58,7 @@ async def test_delete_node_user_without_correct_rights( users = (await user_factory.build(), await user_factory.build()) company = await company_factory.build(owner=users[0].user) group = await group_factory.build(company=company) - node = await node_factory.build(company, group) + node, _ = await node_factory.build(company, group) response = await transport.delete( f"/nodes/{node.id}", headers=authorization_headers(users[1].access_token) diff --git a/tests/e2e/node/test_detach_node.py b/tests/e2e/node/test_detach_node.py new file mode 100644 index 0000000..f651b87 --- /dev/null +++ b/tests/e2e/node/test_detach_node.py @@ -0,0 +1,107 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import NodeAssociationRepository +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + GroupFactory, + NodeAssociationFactory, + NodeFactory, + UserFactory, +) + +# Отсоединение ноды от группы это: + + +@pytest.mark.asyncio +async def test_detach_node_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + node_association_factory: NodeAssociationFactory, + node_association_repository: NodeAssociationRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + additional_group = await group_factory.build(company=company) + node, association = await node_factory.build(company=company, group=group) + additional_association = await node_association_factory.build( + node=node, group=additional_group + ) + + response = await transport.delete( + f"/nodes/associations/{additional_association.id}", + headers=authorization_headers(user.access_token), + ) + + associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(associations) == 1 + assert associations[0] == IsPartialDataclass(id=association.id) + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_detach_node_have_single_association( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + node_association_repository: NodeAssociationRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + node, association = await node_factory.build(company=company, group=group) + + response = await transport.delete( + f"/nodes/associations/{association.id}", + headers=authorization_headers(user.access_token), + ) + + associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(associations) == 1 + assert associations[0] == IsPartialDataclass(id=association.id) + assert response.status_code == HTTPStatus.CONFLICT + assert response.json() == IsPartialDict( + detail="Node must have at least one association", + meta=[{"key": "node_association_id", "value": str(association.node_id)}], + ) + + +@pytest.mark.asyncio +async def test_detach_node_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + node_association_repository: NodeAssociationRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[1].user) + group = await group_factory.build(company=company) + node, association = await node_factory.build(company=company, group=group) + + response = await transport.delete( + f"/nodes/associations/{association.id}", + headers=authorization_headers(users[0].access_token), + ) + + associations = await node_association_repository.get_all_by_node_id(node.id) + + assert len(associations) == 1 + assert associations[0] == IsPartialDataclass(id=association.id) + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", meta=None + ) diff --git a/tests/e2e/node/test_update_node.py b/tests/e2e/node/test_update_node.py index 303709b..020b9cc 100644 --- a/tests/e2e/node/test_update_node.py +++ b/tests/e2e/node/test_update_node.py @@ -28,7 +28,7 @@ async def test_update_node_ok( user = await user_factory.build() company = await company_factory.build(owner=user.user) group = await group_factory.build(company=company) - node = await node_factory.build(company=company, group=group) + node, _ = await node_factory.build(company=company, group=group) request = UpdateNodeRequestFactory.build() @@ -59,7 +59,7 @@ async def test_update_node_user_without_correct_rights( users = (await user_factory.build(), await user_factory.build()) company = await company_factory.build(owner=users[0].user) group = await group_factory.build(company=company) - node = await node_factory.build(company=company, group=group) + node, _ = await node_factory.build(company=company, group=group) request = UpdateNodeRequestFactory.build() diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py index 799a213..31552e6 100644 --- a/tests/factories/models/__init__.py +++ b/tests/factories/models/__init__.py @@ -1,6 +1,12 @@ from .company import CompanyFactory from .group import GroupFactory -from .node import NodeFactory +from .node import NodeAssociationFactory, NodeFactory from .user import UserFactory -__all__ = ("CompanyFactory", "GroupFactory", "NodeFactory", "UserFactory") +__all__ = ( + "CompanyFactory", + "GroupFactory", + "NodeAssociationFactory", + "NodeFactory", + "UserFactory", +) diff --git a/tests/factories/models/node.py b/tests/factories/models/node.py index 836b536..2cfae04 100644 --- a/tests/factories/models/node.py +++ b/tests/factories/models/node.py @@ -18,7 +18,9 @@ class NodeFactory: node_association_repository: NodeAssociationRepository node_repository: NodeRepository - async def build(self, company: Company, group: Group) -> Node: + async def build( + self, company: Company, group: Group + ) -> tuple[Node, NodeAssociation]: async with self.transaction_manager: node = Node.new( node_id=NodeId(uuid4()), @@ -36,4 +38,22 @@ async def build(self, company: Company, group: Group) -> Node: await self.node_repository.create(node) await self.node_association_repository.create(node_association) - return node + return node, node_association + + +@dataclass +class NodeAssociationFactory: + transaction_manager: TransactionManager + node_association_repository: NodeAssociationRepository + + async def build(self, node: Node, group: Group) -> NodeAssociation: + async with self.transaction_manager: + node_association = NodeAssociation.new( + node_association_id=NodeAssociationId(uuid4()), + node=node, + group=group, + ) + + await self.node_association_repository.create(node_association) + + return node_association From a415fd0c9a7697169ebb72ffbc0837fdcbe779cf Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Mon, 29 Jun 2026 22:51:32 +0300 Subject: [PATCH 39/55] tests: get neighbours tests --- .../receive_graph_statistics/neighbours.py | 8 +- tests/conftest.py | 30 ++++++++ tests/e2e/node/test_get_neighbours.py | 75 +++++++++++++++++++ tests/factories/models/__init__.py | 4 + tests/factories/models/context.py | 27 +++++++ tests/factories/models/edge.py | 32 ++++++++ 6 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/node/test_get_neighbours.py create mode 100644 tests/factories/models/context.py create mode 100644 tests/factories/models/edge.py diff --git a/src/prodik/application/receive_graph_statistics/neighbours.py b/src/prodik/application/receive_graph_statistics/neighbours.py index 79cb147..e638b39 100644 --- a/src/prodik/application/receive_graph_statistics/neighbours.py +++ b/src/prodik/application/receive_graph_statistics/neighbours.py @@ -47,8 +47,6 @@ async def execute( queue: deque[tuple[NodeId, int]] = deque([(node.id, 0)]) neighbours: dict[NodeId, Edge] = {} - queue.append((node.id, 0)) - while queue: node_id, depth = queue.popleft() @@ -59,8 +57,10 @@ async def execute( other = edge.other_end(node_id) if ( - node_id in {edge.node_a_id, edge.node_b_id} - ) and other not in neighbours: + (node_id in {edge.node_a_id, edge.node_b_id}) + and other not in neighbours + and other != node.id + ): neighbours[other] = edge queue.append((other, depth + 1)) diff --git a/tests/conftest.py b/tests/conftest.py index 1402be7..a0ec9c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,8 @@ from prodik.application.interfaces.password_hasher import PasswordHasher from prodik.application.interfaces.repositories import ( CompanyRepository, + ContextRepository, + EdgeRepository, GroupRepository, LocalAuthorizationRepository, NodeAssociationRepository, @@ -58,6 +60,8 @@ from prodik.infrastructure.persistence import start_mapper from tests.factories.models import ( CompanyFactory, + ContextFactory, + EdgeFactory, GroupFactory, NodeAssociationFactory, NodeFactory, @@ -144,6 +148,12 @@ async def company_repository(container: AsyncContainer) -> CompanyRepository: return await test_contaner.get(CompanyRepository) # type: ignore[no-any-return] +@pytest.fixture +async def edge_repository(container: AsyncContainer) -> EdgeRepository: + async with container() as test_contaner: + return await test_contaner.get(EdgeRepository) # type: ignore[no-any-return] + + @pytest.fixture async def role_repository(container: AsyncContainer) -> RoleRepository: async with container() as test_container: @@ -173,6 +183,26 @@ async def node_association_factory(container: AsyncContainer) -> NodeAssociation ) +@pytest.fixture +async def edge_factory(container: AsyncContainer) -> EdgeFactory: + async with container() as test_container: + return EdgeFactory( + transaction_manager=await test_container.get(TransactionManager), + edge_repository=await test_container.get( + EdgeRepository, + ), + ) + + +@pytest.fixture +async def context_factory(container: AsyncContainer) -> ContextFactory: + async with container() as test_container: + return ContextFactory( + transaction_manager=await test_container.get(TransactionManager), + context_repository=await test_container.get(ContextRepository), + ) + + @pytest.fixture async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: diff --git a/tests/e2e/node/test_get_neighbours.py b/tests/e2e/node/test_get_neighbours.py new file mode 100644 index 0000000..ebab820 --- /dev/null +++ b/tests/e2e/node/test_get_neighbours.py @@ -0,0 +1,75 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + ContextFactory, + EdgeFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_get_neighbours_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = [ + (await node_factory.build(company=company, group=group))[0] for _ in range(5) + ] + [ + await edge_factory.build( + node_a=nodes[0], + node_b=nodes[index], + context=context, + company=company, + ) + for index in range(1, len(nodes)) + ] + + response = await transport.get( + f"/nodes/{nodes[1].id}/neighbours", + params={"context_id": str(context.id), "depth": 2}, + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert len(response.json()) == len(nodes) - 1 + + +@pytest.mark.asyncio +async def test_get_neighbours_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + + response = await transport.get( + f"/nodes/{context.id}/neighbours", + params={"context_id": str(context.id), "depth": 2}, + headers=authorization_headers(users[1].access_token), + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", meta=None + ) diff --git a/tests/factories/models/__init__.py b/tests/factories/models/__init__.py index 31552e6..df85c53 100644 --- a/tests/factories/models/__init__.py +++ b/tests/factories/models/__init__.py @@ -1,10 +1,14 @@ from .company import CompanyFactory +from .context import ContextFactory +from .edge import EdgeFactory from .group import GroupFactory from .node import NodeAssociationFactory, NodeFactory from .user import UserFactory __all__ = ( "CompanyFactory", + "ContextFactory", + "EdgeFactory", "GroupFactory", "NodeAssociationFactory", "NodeFactory", diff --git a/tests/factories/models/context.py b/tests/factories/models/context.py new file mode 100644 index 0000000..831d464 --- /dev/null +++ b/tests/factories/models/context.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import ContextRepository +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.domain.company import Company +from prodik.domain.context import Context, ContextId +from tests.factories.common import generate_random_string + + +@dataclass +class ContextFactory: + transaction_manager: TransactionManager + context_repository: ContextRepository + + async def build(self, *, company: Company) -> Context: + async with self.transaction_manager: + context = Context.new( + context_id=ContextId(uuid4()), + name=generate_random_string(), + description=generate_random_string(), + company=company, + ) + + await self.context_repository.create(context) + + return context diff --git a/tests/factories/models/edge.py b/tests/factories/models/edge.py new file mode 100644 index 0000000..f3fd160 --- /dev/null +++ b/tests/factories/models/edge.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from uuid import uuid4 + +from prodik.application.interfaces.repositories import EdgeRepository +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.domain.company import Company +from prodik.domain.context import Context +from prodik.domain.edge import Edge, EdgeId +from prodik.domain.node import Node + + +@dataclass +class EdgeFactory: + edge_repository: EdgeRepository + transaction_manager: TransactionManager + + async def build( + self, *, node_a: Node, node_b: Node, context: Context, company: Company + ) -> Edge: + async with self.transaction_manager: + edge = Edge.new( + edge_id=EdgeId(uuid4()), + node_a=node_a, + node_b=node_b, + context=context, + company=company, + weight=0, + ) + + await self.edge_repository.create(edge) + + return edge From 865e843eff4351eba14d0a1bb86b62cea18253fd Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Wed, 1 Jul 2026 15:23:47 +0300 Subject: [PATCH 40/55] tests: contexts tests --- src/prodik/domain/context/model.py | 2 +- tests/conftest.py | 6 ++ tests/e2e/context/__init__.py | 0 tests/e2e/context/test_create_context.py | 71 ++++++++++++++++++++++++ tests/e2e/context/test_delete_context.py | 59 ++++++++++++++++++++ tests/factories/schemas/__init__.py | 2 + tests/factories/schemas/context.py | 7 +++ 7 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/context/__init__.py create mode 100644 tests/e2e/context/test_create_context.py create mode 100644 tests/e2e/context/test_delete_context.py create mode 100644 tests/factories/schemas/context.py diff --git a/src/prodik/domain/context/model.py b/src/prodik/domain/context/model.py index 53f07e5..a770bda 100644 --- a/src/prodik/domain/context/model.py +++ b/src/prodik/domain/context/model.py @@ -38,7 +38,7 @@ def __init__(self, value: str) -> None: class ContextDescription(ValueObject[str]): def __init__(self, value: str) -> None: - if len(value) >= MAX_ALLOWED_CONTEXT_DESCRIPTION_LENGTH: + if len(value) > MAX_ALLOWED_CONTEXT_DESCRIPTION_LENGTH: raise InvalidContextDescriptionFormatError( "Context description must be shorter than " f"{MAX_ALLOWED_CONTEXT_DESCRIPTION_LENGTH}", diff --git a/tests/conftest.py b/tests/conftest.py index a0ec9c5..ca62933 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -148,6 +148,12 @@ async def company_repository(container: AsyncContainer) -> CompanyRepository: return await test_contaner.get(CompanyRepository) # type: ignore[no-any-return] +@pytest.fixture +async def context_repository(container: AsyncContainer) -> ContextRepository: + async with container() as test_container: + return await test_container.get(ContextRepository) # type: ignore[no-any-return] + + @pytest.fixture async def edge_repository(container: AsyncContainer) -> EdgeRepository: async with container() as test_contaner: diff --git a/tests/e2e/context/__init__.py b/tests/e2e/context/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/context/test_create_context.py b/tests/e2e/context/test_create_context.py new file mode 100644 index 0000000..fadf6b8 --- /dev/null +++ b/tests/e2e/context/test_create_context.py @@ -0,0 +1,71 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import ContextRepository +from prodik.domain.context import ContextDescription, ContextName +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, UserFactory +from tests.factories.schemas import CreateContextRequestFactory + + +@pytest.mark.asyncio +async def test_create_context_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_repository: ContextRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + + request = CreateContextRequestFactory.build(company_id=company.id) + + response = await transport.post( + "/contexts/", + json=request.model_dump(mode="json"), + headers=authorization_headers(user.access_token), + ) + content = response.json() + + context = await context_repository.get_by_id(content["id"]) + + assert response.status_code == HTTPStatus.CREATED + assert context == IsPartialDataclass( + name=ContextName(request.name), + description=ContextDescription(request.description), + company_id=company.id, + ) + assert content == IsPartialDict( + name=request.name, description=request.description, company_id=str(company.id) + ) + + +@pytest.mark.asyncio +async def test_create_context_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_repository: ContextRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + + request = CreateContextRequestFactory.build(company_id=company.id) + + response = await transport.post( + "/contexts/", + json=request.model_dump(mode="json"), + headers=authorization_headers(users[1].access_token), + ) + + contexts = await context_repository.get_all_by_company_id(company.id) + + assert len(contexts) == 0 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/e2e/context/test_delete_context.py b/tests/e2e/context/test_delete_context.py new file mode 100644 index 0000000..e608156 --- /dev/null +++ b/tests/e2e/context/test_delete_context.py @@ -0,0 +1,59 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import ContextRepository +from tests.factories.common import authorization_headers +from tests.factories.models import CompanyFactory, ContextFactory, UserFactory + + +@pytest.mark.asyncio +async def test_delete_context_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + context_repository: ContextRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + + response = await transport.delete( + f"/contexts/{context.id}", + headers=authorization_headers(user.access_token), + ) + + contexts = await context_repository.get_all_by_company_id(company.id) + + assert len(contexts) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_context_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + context_repository: ContextRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + + response = await transport.delete( + f"/contexts/{context.id}", + headers=authorization_headers(users[1].access_token), + ) + + contexts = await context_repository.get_all_by_company_id(company.id) + + assert len(contexts) == 1 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index 6f1c7ce..cceb570 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -4,6 +4,7 @@ RegisterRequestFactory, ) from .company import RegisterCompanyRequestFactory +from .context import CreateContextRequestFactory from .group import CreateGroupRequestFactory from .node import ( AttachNodeRequestFactory, @@ -13,6 +14,7 @@ __all__ = ( "AttachNodeRequestFactory", + "CreateContextRequestFactory", "CreateGroupRequestFactory", "CreateNodeRequestFactory", "LoginRequestFactory", diff --git a/tests/factories/schemas/context.py b/tests/factories/schemas/context.py new file mode 100644 index 0000000..280bca9 --- /dev/null +++ b/tests/factories/schemas/context.py @@ -0,0 +1,7 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.context import CreateContextRequest + + +class CreateContextRequestFactory(ModelFactory[CreateContextRequest]): + __use_examples__ = True From d0d6c7b463b06c8cf9c15eaecc34bca03254b1ea Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Wed, 1 Jul 2026 15:36:36 +0300 Subject: [PATCH 41/55] tests: parallel test support by cache flushing after testing session --- tests/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ca62933..e9036be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,11 +79,12 @@ def startup() -> None: start_mapper() -@pytest.fixture(autouse=True) -async def clear_cache(container: AsyncContainer) -> AsyncIterator[None]: +@pytest.fixture(scope="session", autouse=True) +async def clear_cache(config: Config) -> AsyncIterator[None]: yield - redis = await container.get(Redis) + redis = Redis(host=config.cache.host, port=config.cache.port) await redis.flushdb() + await redis.aclose() @pytest.fixture From 1a946384b94b01ef87ecc4b116474c07d4f2c14e Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 12:48:34 +0300 Subject: [PATCH 42/55] fix(application): create edge toctou problem --- .../application/manage_edge/create_edge.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/prodik/application/manage_edge/create_edge.py b/src/prodik/application/manage_edge/create_edge.py index 899a76d..02fac01 100644 --- a/src/prodik/application/manage_edge/create_edge.py +++ b/src/prodik/application/manage_edge/create_edge.py @@ -3,7 +3,6 @@ from uuid import uuid4 from prodik.application.errors import ( - EdgeAlreadyExistsError, NodeNotFoundError, ) from prodik.application.interfaces.repositories import ( @@ -61,21 +60,6 @@ async def execute(self, request: CreateEdgeRequestDTO) -> Edge: ], ) - edge = await self.edge_repository.get_by_nodes_and_context( - node_a_id=request.node_a_id, - node_b_id=request.node_b_id, - context_id=request.context_id, - ) - if edge is not None: - raise EdgeAlreadyExistsError( - "Edge between nodes in this context already exists", - [ - {"key": "node_a_id", "value": request.node_a_id}, - {"key": "node_b_id", "value": request.node_b_id}, - {"key": "context_id", "value": request.context_id}, - ], - ) - edge = Edge.new( edge_id=EdgeId(uuid4()), node_a=nodes[0], From b5c5c9affd9b08af1fc9f367948e62e39c05098a Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 12:48:53 +0300 Subject: [PATCH 43/55] fix(infrastructure): create edge toctou problem --- .../5192c880eb4e_edge_unique_constrait.py | 32 ++++++++++++++++ .../infrastructure/persistence/registry.py | 6 +++ .../infrastructure/repositories/edge.py | 37 ++++++++++++------- 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py b/src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py new file mode 100644 index 0000000..cc1c775 --- /dev/null +++ b/src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py @@ -0,0 +1,32 @@ +"""edge-unique-constrait + +Revision ID: 5192c880eb4e +Revises: 4a8ee55b96fa +Create Date: 2026-07-02 09:25:00.984222 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5192c880eb4e' +down_revision: Union[str, Sequence[str], None] = '4a8ee55b96fa' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('uq_edge_record_node_a_id_node_b_id_context_id', 'edge_record', ['context_id', 'node_a_id', 'node_b_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_edge_record_node_a_id_node_b_id_context_id', 'edge_record', type_='unique') + # ### end Alembic commands ### diff --git a/src/prodik/infrastructure/persistence/registry.py b/src/prodik/infrastructure/persistence/registry.py index c56b74f..b124ccb 100644 --- a/src/prodik/infrastructure/persistence/registry.py +++ b/src/prodik/infrastructure/persistence/registry.py @@ -201,6 +201,12 @@ Column("weight", Float, nullable=False), Column("created_at", DateTime(timezone=True), nullable=False), Column("updated_at", DateTime(timezone=True), nullable=False), + UniqueConstraint( + "context_id", + "node_a_id", + "node_b_id", + name="uq_edge_record_node_a_id_node_b_id_context_id", + ), ) role_record_table = Table( diff --git a/src/prodik/infrastructure/repositories/edge.py b/src/prodik/infrastructure/repositories/edge.py index d9d3717..c3b2451 100644 --- a/src/prodik/infrastructure/repositories/edge.py +++ b/src/prodik/infrastructure/repositories/edge.py @@ -2,9 +2,10 @@ import structlog from sqlalchemy import and_, delete, insert, or_, select, update +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import EdgeNotFoundError +from prodik.application.errors import EdgeAlreadyExistsError, EdgeNotFoundError from prodik.application.interfaces.repositories import EdgeRepository from prodik.domain.context import ContextId from prodik.domain.edge import Edge, EdgeId @@ -19,18 +20,28 @@ class EdgeRepositoryImpl(EdgeRepository): async def create(self, edge: Edge) -> None: logger.debug("Repository create edge", edge_id=edge.id) - await self.session.execute( - insert(Edge).values( - id=edge.id, - company_id=edge.company_id, - context_id=edge.context_id, - node_a_id=edge.node_a_id, - node_b_id=edge.node_b_id, - created_at=edge.created_at, - updated_at=edge.updated_at, - weight=edge.weight, - ), - ) + try: + await self.session.execute( + insert(Edge).values( + id=edge.id, + company_id=edge.company_id, + context_id=edge.context_id, + node_a_id=edge.node_a_id, + node_b_id=edge.node_b_id, + created_at=edge.created_at, + updated_at=edge.updated_at, + weight=edge.weight, + ), + ) + except IntegrityError as e: + raise EdgeAlreadyExistsError( + "Edge between nodes in this context already exists", + [ + {"key": "node_a_id", "value": edge.node_a_id}, + {"key": "node_b_id", "value": edge.node_b_id}, + {"key": "context_id", "value": edge.context_id}, + ], + ) from e async def delete(self, edge: Edge) -> None: logger.debug("Repository delete edge", edge_id=edge.id) From ee9ace79a7e4fa89ca512468d0244f597bc9d50d Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 12:49:07 +0300 Subject: [PATCH 44/55] tests: test create edge --- tests/e2e/edge/__init__.py | 0 tests/e2e/edge/test_create_edge.py | 152 ++++++++++++++++++++++++++++ tests/factories/schemas/__init__.py | 2 + tests/factories/schemas/edge.py | 6 ++ 4 files changed, 160 insertions(+) create mode 100644 tests/e2e/edge/__init__.py create mode 100644 tests/e2e/edge/test_create_edge.py create mode 100644 tests/factories/schemas/edge.py diff --git a/tests/e2e/edge/__init__.py b/tests/e2e/edge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/edge/test_create_edge.py b/tests/e2e/edge/test_create_edge.py new file mode 100644 index 0000000..6676ced --- /dev/null +++ b/tests/e2e/edge/test_create_edge.py @@ -0,0 +1,152 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import EdgeRepository +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + ContextFactory, + EdgeFactory, + GroupFactory, + NodeFactory, + UserFactory, +) +from tests.factories.schemas import CreateEdgeRequestFactory + + +@pytest.mark.asyncio +async def test_create_edge_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + + request = CreateEdgeRequestFactory.build( + context_id=context.id, node_a_id=nodes[0].id, node_b_id=nodes[1].id + ) + + response = await transport.post( + "/edges/", + json=request.model_dump(mode="json"), + headers=authorization_headers(user.access_token), + ) + content = response.json() + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert len(edges) == 1 + assert response.status_code == HTTPStatus.CREATED + assert edges[0] == IsPartialDataclass( + node_a_id=nodes[0].id, + node_b_id=nodes[1].id, + context_id=context.id, + company_id=company.id, + ) + assert content == IsPartialDict( + id=str(edges[0].id), + node_a_id=str(edges[0].node_a_id), + node_b_id=str(edges[0].node_b_id), + context_id=str(edges[0].context_id), + company_id=str(edges[0].company_id), + ) + + +@pytest.mark.asyncio +async def test_create_edge_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + node_factory: NodeFactory, + group_factory: GroupFactory, + edge_repository: EdgeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + + request = CreateEdgeRequestFactory.build( + context_id=context.id, node_a_id=nodes[0].id, node_b_id=nodes[1].id + ) + + response = await transport.post( + "/edges/", + json=request.model_dump(mode="json"), + headers=authorization_headers(users[1].access_token), + ) + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert len(edges) == 0 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) + + +@pytest.mark.asyncio +async def test_create_edge_already_exists( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + context_factory: ContextFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + group_factory: GroupFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company + ) + + request = CreateEdgeRequestFactory.build( + context_id=context.id, node_a_id=nodes[0].id, node_b_id=nodes[1].id + ) + + response = await transport.post( + "/edges/", + json=request.model_dump(mode="json"), + headers=authorization_headers(user.access_token), + ) + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert len(edges) == 1 + assert response.status_code == HTTPStatus.CONFLICT + assert response.json() == IsPartialDict( + detail="Edge between nodes in this context already exists", + meta=[ + {"key": "node_a_id", "value": str(request.node_a_id)}, + {"key": "node_b_id", "value": str(request.node_b_id)}, + {"key": "context_id", "value": str(request.context_id)}, + ], + ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index cceb570..b37499a 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -5,6 +5,7 @@ ) from .company import RegisterCompanyRequestFactory from .context import CreateContextRequestFactory +from .edge import CreateEdgeRequestFactory from .group import CreateGroupRequestFactory from .node import ( AttachNodeRequestFactory, @@ -15,6 +16,7 @@ __all__ = ( "AttachNodeRequestFactory", "CreateContextRequestFactory", + "CreateEdgeRequestFactory", "CreateGroupRequestFactory", "CreateNodeRequestFactory", "LoginRequestFactory", diff --git a/tests/factories/schemas/edge.py b/tests/factories/schemas/edge.py new file mode 100644 index 0000000..e3ef873 --- /dev/null +++ b/tests/factories/schemas/edge.py @@ -0,0 +1,6 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.edge import CreateEdgeRequest + + +class CreateEdgeRequestFactory(ModelFactory[CreateEdgeRequest]): ... From 22993186452f841afe8077e74fb02a95aee28b0c Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 13:02:09 +0300 Subject: [PATCH 45/55] tests: test delete edge --- .../application/manage_edge/delete_edge.py | 18 ---- tests/e2e/edge/test_delete_edge.py | 84 +++++++++++++++++++ 2 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/edge/test_delete_edge.py diff --git a/src/prodik/application/manage_edge/delete_edge.py b/src/prodik/application/manage_edge/delete_edge.py index 05a0cc2..9b11e2b 100644 --- a/src/prodik/application/manage_edge/delete_edge.py +++ b/src/prodik/application/manage_edge/delete_edge.py @@ -1,9 +1,5 @@ from dataclasses import dataclass -from prodik.application.errors import ( - CompanyNotFoundError, - ContextNotFoundError, -) from prodik.application.interfaces.repositories import ( CompanyRepository, ContextRepository, @@ -25,22 +21,8 @@ class DeleteEdgeInteractor: async def execute(self, edge_id: EdgeId) -> None: async with self.transaction_manager: user = await self.access_control_service.get_authorized_user() - edge = await self.edge_repository.get_by_id(edge_id) - context = await self.context_repository.get_by_id(edge.context_id) - if context is None: - raise ContextNotFoundError( - "Context by edge not found", - [{"key": "context_id", "value": edge.context_id}], - ) - - company = await self.company_repository.get_by_id(edge.company_id) - if company is None: - raise CompanyNotFoundError( - "Company by edge not found", - [{"key": "company_id", "value": edge.company_id}], - ) await self.access_control_service.ensure_user_can_manipulate_context( user, diff --git a/tests/e2e/edge/test_delete_edge.py b/tests/e2e/edge/test_delete_edge.py new file mode 100644 index 0000000..f86811e --- /dev/null +++ b/tests/e2e/edge/test_delete_edge.py @@ -0,0 +1,84 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import EdgeRepository +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + ContextFactory, + EdgeFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_delete_edge_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company + ) + + response = await transport.delete( + f"/edges/{edge.id}", headers=authorization_headers(user.access_token) + ) + edges = await edge_repository.get_all_by_context_id(context.id) + + assert len(edges) == 0 + assert response.status_code == HTTPStatus.NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_edge_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company + ) + + response = await transport.delete( + f"/edges/{edge.id}", headers=authorization_headers(users[1].access_token) + ) + edges = await edge_repository.get_all_by_context_id(context.id) + + assert len(edges) == 1 + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) From 00e189cf3951eaefdb97b7ce20d439184b5f8c60 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 13:12:09 +0300 Subject: [PATCH 46/55] tests: update edge weight tests --- tests/e2e/edge/test_manipulate_edge_weight.py | 241 ++++++++++++++++++ tests/factories/models/edge.py | 10 +- 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/edge/test_manipulate_edge_weight.py diff --git a/tests/e2e/edge/test_manipulate_edge_weight.py b/tests/e2e/edge/test_manipulate_edge_weight.py new file mode 100644 index 0000000..29f1184 --- /dev/null +++ b/tests/e2e/edge/test_manipulate_edge_weight.py @@ -0,0 +1,241 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass, IsPartialDict +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import EdgeRepository +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + ContextFactory, + EdgeFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_increment_edge_weight_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight/increment", + headers=authorization_headers(user.access_token), + ) + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert edges[0] == IsPartialDataclass(weight=edge.weight + 1) + + +@pytest.mark.asyncio +async def test_decrement_edge_weight_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company, weight=5 + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight/decrement", + headers=authorization_headers(user.access_token), + ) + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert edges[0] == IsPartialDataclass(weight=edge.weight - 1) + + +@pytest.mark.asyncio +async def test_update_edge_weight_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], node_b=nodes[1], context=context, company=company + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight", + json={"weight": 100}, + headers=authorization_headers(user.access_token), + ) + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert edges[0] == IsPartialDataclass(weight=100) + + +@pytest.mark.asyncio +async def test_increment_edge_weight_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], + node_b=nodes[1], + context=context, + company=company, + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight/increment", + headers=authorization_headers(users[1].access_token), + ) + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert edges[0] == IsPartialDataclass(weight=edge.weight) + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) + + +@pytest.mark.asyncio +async def test_decrement_edge_weight_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], + node_b=nodes[1], + context=context, + company=company, + weight=5, + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight/decrement", + headers=authorization_headers(users[1].access_token), + ) + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert edges[0] == IsPartialDataclass(weight=edge.weight) + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) + + +@pytest.mark.asyncio +async def test_update_edge_weight_user_without_correct_rights( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + node_factory: NodeFactory, + edge_factory: EdgeFactory, + context_factory: ContextFactory, + edge_repository: EdgeRepository, +) -> None: + users = (await user_factory.build(), await user_factory.build()) + company = await company_factory.build(owner=users[0].user) + context = await context_factory.build(company=company) + group = await group_factory.build(company=company) + nodes = ( + (await node_factory.build(group=group, company=company))[0], + (await node_factory.build(group=group, company=company))[0], + ) + edge = await edge_factory.build( + node_a=nodes[0], + node_b=nodes[1], + context=context, + company=company, + ) + + response = await transport.patch( + f"/edges/{edge.id}/weight", + json={"weight": 100}, + headers=authorization_headers(users[1].access_token), + ) + + edges = await edge_repository.get_all_by_context_id(context.id) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert edges[0] == IsPartialDataclass(weight=edge.weight) + assert response.json() == IsPartialDict( + detail="Not enough rights to perform operation", + meta=None, + ) diff --git a/tests/factories/models/edge.py b/tests/factories/models/edge.py index f3fd160..09b2b58 100644 --- a/tests/factories/models/edge.py +++ b/tests/factories/models/edge.py @@ -15,7 +15,13 @@ class EdgeFactory: transaction_manager: TransactionManager async def build( - self, *, node_a: Node, node_b: Node, context: Context, company: Company + self, + *, + node_a: Node, + node_b: Node, + context: Context, + company: Company, + weight: int = 0, ) -> Edge: async with self.transaction_manager: edge = Edge.new( @@ -24,7 +30,7 @@ async def build( node_b=node_b, context=context, company=company, - weight=0, + weight=weight, ) await self.edge_repository.create(edge) From 918003d26f8ff9a74f5b7a9fd35cc6f9051aeab6 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 2 Jul 2026 13:20:37 +0300 Subject: [PATCH 47/55] chore(application): remove useless interactor --- .../application/receive_edge_info/__init__.py | 3 -- .../receive_edge_info/get_edges_by_context.py | 32 ------------------- .../bootstrap/di/providers/application.py | 2 -- src/prodik/presentation/views/edge.py | 20 ------------ 4 files changed, 57 deletions(-) delete mode 100644 src/prodik/application/receive_edge_info/__init__.py delete mode 100644 src/prodik/application/receive_edge_info/get_edges_by_context.py diff --git a/src/prodik/application/receive_edge_info/__init__.py b/src/prodik/application/receive_edge_info/__init__.py deleted file mode 100644 index b6023f1..0000000 --- a/src/prodik/application/receive_edge_info/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .get_edges_by_context import GetEdgesByContextInteractor - -__all__ = ("GetEdgesByContextInteractor",) diff --git a/src/prodik/application/receive_edge_info/get_edges_by_context.py b/src/prodik/application/receive_edge_info/get_edges_by_context.py deleted file mode 100644 index e6336a4..0000000 --- a/src/prodik/application/receive_edge_info/get_edges_by_context.py +++ /dev/null @@ -1,32 +0,0 @@ -from dataclasses import dataclass - -from prodik.application.interfaces.repositories import ( - CompanyRepository, - ContextRepository, - EdgeRepository, - NodeRepository, -) -from prodik.application.services import AccessControlService -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge - - -@dataclass -class GetEdgesByContextInteractor: - edge_repository: EdgeRepository - access_control_service: AccessControlService - company_repository: CompanyRepository - context_repository: ContextRepository - node_repository: NodeRepository - - async def execute(self, context_id: ContextId) -> list[Edge]: - user = await self.access_control_service.get_authorized_user() - - context = await self.context_repository.get_by_id(context_id) - - await self.access_control_service.ensure_user_can_view_context( - user, - context, - ) - - return await self.edge_repository.get_all_by_context_id(context_id) diff --git a/src/prodik/bootstrap/di/providers/application.py b/src/prodik/bootstrap/di/providers/application.py index d43932f..7f2ac56 100644 --- a/src/prodik/bootstrap/di/providers/application.py +++ b/src/prodik/bootstrap/di/providers/application.py @@ -32,7 +32,6 @@ from prodik.application.receive_context_info import ( GetContextByIdInteractor, ) -from prodik.application.receive_edge_info import GetEdgesByContextInteractor from prodik.application.receive_graph_statistics import ( FindShortestPathInteractor, GetNodeNeighboursInteractor, @@ -82,7 +81,6 @@ class ApplicationProvider(Provider): IncrementEdgeWeightInteractor, GetGroupByIdInteractor, GetContextByIdInteractor, - GetEdgesByContextInteractor, UpdateCurrentUserProfileInteractor, UpdateCurrentUserPasswordInteractor, SendOfferToCompanyInteractor, diff --git a/src/prodik/presentation/views/edge.py b/src/prodik/presentation/views/edge.py index 610d28c..3cdaf89 100644 --- a/src/prodik/presentation/views/edge.py +++ b/src/prodik/presentation/views/edge.py @@ -8,7 +8,6 @@ CreateEdgeRequestDTO, DeleteEdgeInteractor, ) -from prodik.application.receive_edge_info import GetEdgesByContextInteractor from prodik.application.receive_graph_statistics import ( FindShortestPathInteractor, FindShortestPathRequestDTO, @@ -109,22 +108,3 @@ async def update_edge_weight( interactor: FromDishka[UpdateEdgeWeightInteractor], ) -> None: await interactor.execute(edge_id, request.weight) - - -@router.get("/{context_id}") -async def get_all_edges_by_context( - context_id: ContextId, - interactor: FromDishka[GetEdgesByContextInteractor], -) -> list[EdgeSchema]: - result = await interactor.execute(context_id) - - return [ - EdgeSchema( - id=edge.id, - node_a_id=edge.node_a_id, - node_b_id=edge.node_b_id, - context_id=edge.context_id, - company_id=edge.company_id, - ) - for edge in result - ] From 44ca3bfd6f7e541d2c6636866ce69f68c9297bdb Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Fri, 3 Jul 2026 14:28:07 +0300 Subject: [PATCH 48/55] tests find shortest path tests --- src/prodik/presentation/schemas/edge.py | 6 -- tests/e2e/edge/test_find_shortest_path.py | 119 ++++++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/edge/test_find_shortest_path.py diff --git a/src/prodik/presentation/schemas/edge.py b/src/prodik/presentation/schemas/edge.py index bcfd008..beda32e 100644 --- a/src/prodik/presentation/schemas/edge.py +++ b/src/prodik/presentation/schemas/edge.py @@ -22,9 +22,3 @@ class EdgeSchema(BaseModel): node_b_id: NodeId context_id: ContextId company_id: CompanyId - - -class FindShortestPathRequest(BaseModel): - from_node_id: NodeId - to_node_id: NodeId - context_id: ContextId diff --git a/tests/e2e/edge/test_find_shortest_path.py b/tests/e2e/edge/test_find_shortest_path.py new file mode 100644 index 0000000..6891b34 --- /dev/null +++ b/tests/e2e/edge/test_find_shortest_path.py @@ -0,0 +1,119 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.common import authorization_headers +from tests.factories.models import ( + CompanyFactory, + ContextFactory, + EdgeFactory, + GroupFactory, + NodeFactory, + UserFactory, +) + + +@pytest.mark.asyncio +async def test_find_shortest_path_ok( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + context_factory: ContextFactory, + edge_factory: EdgeFactory, + node_factory: NodeFactory, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + context = await context_factory.build(company=company) + + nodes = ( + (await node_factory.build(company=company, group=group))[0], + (await node_factory.build(company=company, group=group))[0], + (await node_factory.build(company=company, group=group))[0], + (await node_factory.build(company=company, group=group))[0], + ) + + # Simple graph where shortest path + # from nodes[0] to nodes[3] goes through edges[0] -> edges[-1] + edges = ( + await edge_factory.build( + company=company, context=context, node_a=nodes[0], node_b=nodes[1], weight=4 + ), + await edge_factory.build( + company=company, context=context, node_a=nodes[0], node_b=nodes[2], weight=3 + ), + await edge_factory.build( + company=company, context=context, node_a=nodes[0], node_b=nodes[3], weight=7 + ), + await edge_factory.build( + company=company, context=context, node_a=nodes[2], node_b=nodes[1], weight=2 + ), + await edge_factory.build( + company=company, context=context, node_a=nodes[1], node_b=nodes[3], weight=1 + ), + ) + + response = await transport.get( + "/edges/shortest", + params={ + "from_node_id": str(nodes[0].id), + "to_node_id": str(nodes[3].id), + "context_id": str(context.id), + }, + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert response.json() == [ + IsPartialDict( + id=str(edges[0].id), + node_a_id=str(nodes[0].id), + node_b_id=str(nodes[1].id), + context_id=str(context.id), + company_id=str(company.id), + ), + IsPartialDict( + id=str(edges[-1].id), + node_a_id=str(nodes[1].id), + node_b_id=str(nodes[3].id), + context_id=str(context.id), + company_id=str(company.id), + ), + ] + + +@pytest.mark.asyncio +async def test_find_shortest_path_didnt_exists( + transport: AsyncClient, + user_factory: UserFactory, + company_factory: CompanyFactory, + group_factory: GroupFactory, + context_factory: ContextFactory, + node_factory: NodeFactory, +) -> None: + user = await user_factory.build() + company = await company_factory.build(owner=user.user) + group = await group_factory.build(company=company) + context = await context_factory.build(company=company) + + nodes = ( + (await node_factory.build(company=company, group=group))[0], + (await node_factory.build(company=company, group=group))[0], + ) + + response = await transport.get( + "/edges/shortest", + params={ + "from_node_id": str(nodes[0].id), + "to_node_id": str(nodes[1].id), + "context_id": str(context.id), + }, + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert response.json() == [] From 382010b93439512689b1231de7d724d91415dfe1 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Fri, 3 Jul 2026 16:17:11 +0300 Subject: [PATCH 49/55] tests: user tests --- tests/e2e/user/__init__.py | 0 .../e2e/user/test_get_current_user_profile.py | 31 ++++++++++ tests/e2e/user/test_get_user_by_username.py | 50 ++++++++++++++++ tests/e2e/user/test_update_current_profile.py | 40 +++++++++++++ tests/factories/models/role.py | 58 +++++++++++++++++++ tests/factories/schemas/__init__.py | 6 ++ tests/factories/schemas/user.py | 16 +++++ 7 files changed, 201 insertions(+) create mode 100644 tests/e2e/user/__init__.py create mode 100644 tests/e2e/user/test_get_current_user_profile.py create mode 100644 tests/e2e/user/test_get_user_by_username.py create mode 100644 tests/e2e/user/test_update_current_profile.py create mode 100644 tests/factories/models/role.py create mode 100644 tests/factories/schemas/user.py diff --git a/tests/e2e/user/__init__.py b/tests/e2e/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/user/test_get_current_user_profile.py b/tests/e2e/user/test_get_current_user_profile.py new file mode 100644 index 0000000..4abaaf6 --- /dev/null +++ b/tests/e2e/user/test_get_current_user_profile.py @@ -0,0 +1,31 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.common import authorization_headers +from tests.factories.models import UserFactory + + +@pytest.mark.asyncio +async def test_get_current_user_profile_ok( + transport: AsyncClient, + user_factory: UserFactory, +) -> None: + user = await user_factory.build() + + response = await transport.get( + "/users/me/profile", + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert response.json() == IsPartialDict( + id=str(user.user.id), + username=user.user.username, + first_name=user.user.first_name, + last_name=user.user.last_name, + email=user.user.email, + bio=user.user.bio, + ) diff --git a/tests/e2e/user/test_get_user_by_username.py b/tests/e2e/user/test_get_user_by_username.py new file mode 100644 index 0000000..22a043d --- /dev/null +++ b/tests/e2e/user/test_get_user_by_username.py @@ -0,0 +1,50 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDict +from httpx import AsyncClient + +from tests.factories.common import authorization_headers, generate_random_string +from tests.factories.models import UserFactory + + +@pytest.mark.asyncio +async def test_get_user_by_username_ok( + transport: AsyncClient, + user_factory: UserFactory, +) -> None: + user = await user_factory.build() + + response = await transport.get( + f"/users/{user.user.username.value}", + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.OK + assert response.json() == IsPartialDict( + id=str(user.user.id), + username=user.user.username, + first_name=user.user.first_name, + last_name=user.user.last_name, + email=user.user.email, + bio=user.user.bio, + ) + + +@pytest.mark.asyncio +async def test_get_user_by_username_not_found( + transport: AsyncClient, + user_factory: UserFactory, +) -> None: + user = await user_factory.build() + + username = generate_random_string() + response = await transport.get( + f"/users/{username}", + headers=authorization_headers(user.access_token), + ) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.json() == IsPartialDict( + detail="User not found", meta=[{"key": "username", "value": username}] + ) diff --git a/tests/e2e/user/test_update_current_profile.py b/tests/e2e/user/test_update_current_profile.py new file mode 100644 index 0000000..d45fe3a --- /dev/null +++ b/tests/e2e/user/test_update_current_profile.py @@ -0,0 +1,40 @@ +from http import HTTPStatus + +import pytest +from dirty_equals import IsPartialDataclass +from httpx import AsyncClient + +from prodik.application.interfaces.repositories import UserRepository +from prodik.domain.user import Bio, Email, FirstName, LastName, Username +from tests.factories.common import authorization_headers +from tests.factories.models import UserFactory +from tests.factories.schemas import ( + UpdateCurrentUserProfileRequestFactory, +) + + +@pytest.mark.asyncio +async def test_update_current_user_profile_ok( + transport: AsyncClient, + user_factory: UserFactory, + user_repository: UserRepository, +) -> None: + user = await user_factory.build() + + request = UpdateCurrentUserProfileRequestFactory.build() + + response = await transport.put( + "/users/me/profile", + json=request.model_dump(mode="json"), + headers=authorization_headers(user.access_token), + ) + result = await user_repository.get_by_id(user.user.id) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert result == IsPartialDataclass( + first_name=FirstName(request.first_name), + last_name=LastName(request.last_name), + username=Username(request.username), + email=Email(request.email), + bio=Bio(request.bio), + ) diff --git a/tests/factories/models/role.py b/tests/factories/models/role.py new file mode 100644 index 0000000..40e0303 --- /dev/null +++ b/tests/factories/models/role.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass + +from prodik.application.interfaces.repositories import ( + RolePermissionsRepository, + RoleRepository, +) +from prodik.application.interfaces.transaction_manager import TransactionManager +from prodik.application.services import ( + RoleManagmentService, + RoleManagmentServiceResponse, +) +from prodik.domain.company import Company +from prodik.domain.context import Context +from prodik.domain.group import Group +from prodik.domain.role import EntityType, PermissionType +from tests.factories.common import generate_random_string + +entity_types_map = { + Context: EntityType.CONTEXT, + Company: EntityType.COMPANY, + Group: EntityType.GROUP, +} + + +@dataclass +class RoleFactory: + role_managment_service: RoleManagmentService + role_permissions_repository: RolePermissionsRepository + transaction_manager: TransactionManager + role_repository: RoleRepository + + async def build( + self, + company: Company, + entities_with_permissions: list[ + tuple[Context | Company | Group, PermissionType] + ], + ) -> RoleManagmentServiceResponse: + async with self.transaction_manager: + role_managment_service_request = [ + (entity.id, entity_types_map[type(entity)], permission) + for entity, permission in entities_with_permissions + ] + + role_managment_service_response = ( + self.role_managment_service.create_role_with_permissions( + generate_random_string(), + company=company, + request=role_managment_service_request, + ) + ) + + await self.role_permissions_repository.create_all( + role_managment_service_response.permissions + ) + await self.role_repository.create(role_managment_service_response.role) + + return role_managment_service_response diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index b37499a..b5c7332 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -12,6 +12,10 @@ CreateNodeRequestFactory, UpdateNodeRequestFactory, ) +from .user import ( + UpdateCurrentUserPasswordRequestFactory, + UpdateCurrentUserProfileRequestFactory, +) __all__ = ( "AttachNodeRequestFactory", @@ -23,5 +27,7 @@ "RefreshTokenRequestFactory", "RegisterCompanyRequestFactory", "RegisterRequestFactory", + "UpdateCurrentUserPasswordRequestFactory", + "UpdateCurrentUserProfileRequestFactory", "UpdateNodeRequestFactory", ) diff --git a/tests/factories/schemas/user.py b/tests/factories/schemas/user.py new file mode 100644 index 0000000..35b29d3 --- /dev/null +++ b/tests/factories/schemas/user.py @@ -0,0 +1,16 @@ +from polyfactory.factories.pydantic_factory import ModelFactory + +from prodik.presentation.schemas.user import ( + UpdateCurrentUserPasswordRequest, + UpdateCurrentUserProfileRequest, +) + + +class UpdateCurrentUserPasswordRequestFactory( + ModelFactory[UpdateCurrentUserPasswordRequest] +): ... + + +class UpdateCurrentUserProfileRequestFactory( + ModelFactory[UpdateCurrentUserProfileRequest] +): ... From 3183800cbe5420e08e65c70d8d2866b73e87ae19 Mon Sep 17 00:00:00 2001 From: LTU Date: Thu, 9 Jul 2026 15:45:38 +0300 Subject: [PATCH 50/55] Update workflow.yml --- .github/workflows/workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 27eb204..8462e75 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -2,9 +2,9 @@ name: Python CI on: push: - branches: ["main"] + branches: ["dev", "master"] pull_request: - branches: ["main"] + branches: ["dev", "master"] permissions: contents: read From 47c9f019fb2874501b2535464ba40b66a294490a Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Wed, 15 Jul 2026 21:15:26 +0300 Subject: [PATCH 51/55] feat(all): remove old A/A flow feat(all): remove old A/A flow chore(infra): moved migrations feat(infra): keycloak feat(infra): OAuth-Proxy feat(infra): set authorization token chore(all): rename project from prodik to conode feat(infra): application migration test: mock authorization flow ci: some changes some changes some changes some changes some changes some changes --- .github/workflows/workflow.yml | 38 +- Dockerfile | 2 +- README.md | 2 +- deploy/init.sql | 27 + deploy/keycloak.env | 8 + deploy/keycloak.json | 3099 +++++++++++++++++ deploy/nginx.conf | 15 - deploy/oauth_proxy.toml | 25 + deploy/postgres.env | 8 +- docker-compose.base.yaml | 125 +- docker-compose.dev.yaml | 22 +- docker-compose.prod.yaml | 9 +- example.config.toml | 10 +- pyproject.toml | 20 +- .../application/__init__.py | 0 .../attach_node_to_group/__init__.py | 0 .../attach_node_to_group/attach_node.py | 12 +- .../detach_node_from_group/__init__.py | 0 .../detach_node_from_group/detach_node.py | 10 +- src/{prodik => conode}/application/errors.py | 0 .../application/interfaces/__init__.py | 0 .../interfaces/identity_provider.py | 2 +- .../application/interfaces/password_hasher.py | 0 .../interfaces/repositories/__init__.py | 3 - .../interfaces/repositories/company.py | 4 +- .../interfaces/repositories/context.py | 4 +- .../interfaces/repositories/contract.py | 2 +- .../interfaces/repositories/edge.py | 6 +- .../interfaces/repositories/grant/__init__.py | 0 .../interfaces/repositories/grant/company.py | 0 .../interfaces/repositories/grant/user.py | 6 +- .../interfaces/repositories/group.py | 4 +- .../interfaces/repositories/node.py | 6 +- .../interfaces/repositories/offer.py | 2 +- .../interfaces/repositories/permissions.py | 2 +- .../interfaces/repositories/role.py | 4 +- .../interfaces/repositories/user.py | 2 +- .../interfaces/token_manager/__init__.py | 11 + .../interfaces/token_manager/token_manager.py | 21 + .../interfaces/transaction_manager.py | 0 .../application/manage_context/__init__.py | 0 .../manage_context/create_context.py | 10 +- .../manage_context/delete_context.py | 8 +- .../application/manage_edge/__init__.py | 0 .../application/manage_edge/create_edge.py | 14 +- .../application/manage_edge/delete_edge.py | 8 +- .../application/manage_group/__init__.py | 0 .../application/manage_group/create_group.py | 10 +- .../application/manage_group/delete_group.py | 8 +- .../application/manage_node/__init__.py | 0 .../application/manage_node/create_node.py | 10 +- .../application/manage_node/delete_node.py | 8 +- .../application/manage_profile/__init__.py | 0 .../update_current_user_profile.py | 17 +- .../application/manage_role/__init__.py | 0 .../application/manage_role/create_role.py | 12 +- .../application/manage_role/delete_role.py | 8 +- .../application/manage_role/update_role.py | 8 +- .../manage_user_rights/__init__.py | 0 .../manage_user_rights/give_role_to_user.py | 14 +- .../revoke_role_from_user.py | 12 +- .../receive_context_info/__init__.py | 0 .../receive_context_info/get_context_by_id.py | 6 +- .../receive_graph_statistics/__init__.py | 0 .../receive_graph_statistics/neighbours.py | 12 +- .../receive_graph_statistics/shortest_path.py | 10 +- .../receive_group_info/__init__.py | 0 .../receive_group_info/get_group_by_id.py | 6 +- .../application/receive_node_info/__init__.py | 0 .../receive_node_info/get_nodes_by_group.py | 8 +- .../application/receive_user_info/__init__.py | 0 .../receive_user_info/get_current_user.py | 4 +- .../receive_user_info/get_user_by_username.py | 8 +- .../application/register_company/__init__.py | 0 .../register_company/register_company.py | 14 +- .../application/services/__init__.py | 0 .../application/services/access_control.py | 40 +- .../application/services/offer_acceptance.py | 8 +- .../application/services/offer_sending.py | 10 +- .../application/services/role_managment.py | 4 +- .../application/share_graph/__init__.py | 0 .../application/share_graph/accept_offer.py | 16 +- .../application/share_graph/decline_offer.py | 10 +- .../share_graph/send_offer_to_company.py | 18 +- .../update_edge_weight/__init__.py | 0 .../decrement_edge_weight.py | 8 +- .../increment_edge_weight.py | 8 +- .../update_edge_weight/update_edge_weight.py | 8 +- .../application/update_node/__init__.py | 0 .../application/update_node/update_node.py | 8 +- .../application/verify_company/__init__.py | 0 .../verify_company/verify_company.py | 10 +- src/{prodik => conode}/bootstrap/__init__.py | 0 .../bootstrap/api/__init__.py | 0 src/{prodik => conode}/bootstrap/api/run.py | 8 +- src/{prodik => conode}/bootstrap/cli.py | 11 +- .../bootstrap/di/__init__.py | 0 .../bootstrap/di/container.py | 10 +- .../bootstrap/di/providers/__init__.py | 4 +- .../bootstrap/di/providers/application.py | 50 +- .../bootstrap/di/providers/connection.py | 18 +- .../bootstrap/di/providers/infrastructure.py | 16 +- src/conode/bootstrap/di/providers/secrets.py | 10 + src/{prodik => conode}/bootstrap/logs.py | 0 src/{prodik => conode}/domain/__init__.py | 0 .../domain/company/__init__.py | 0 .../domain/company/errors.py | 2 +- .../domain/company/model.py | 6 +- .../domain/context/__init__.py | 0 .../domain/context/errors.py | 2 +- .../domain/context/model.py | 6 +- .../domain/contract/__init__.py | 0 .../domain/contract/errors.py | 2 +- .../domain/contract/model.py | 10 +- .../domain/edge/__init__.py | 0 src/{prodik => conode}/domain/edge/errors.py | 2 +- src/{prodik => conode}/domain/edge/model.py | 10 +- .../domain/grant/__init__.py | 0 .../domain/grant/company.py | 6 +- src/{prodik => conode}/domain/grant/user.py | 6 +- .../domain/group/__init__.py | 0 src/{prodik => conode}/domain/group/errors.py | 2 +- src/{prodik => conode}/domain/group/model.py | 6 +- .../domain/node/__init__.py | 0 src/{prodik => conode}/domain/node/errors.py | 2 +- src/{prodik => conode}/domain/node/model.py | 8 +- .../domain/offer/__init__.py | 0 src/{prodik => conode}/domain/offer/errors.py | 2 +- src/{prodik => conode}/domain/offer/model.py | 12 +- .../domain/role/__init__.py | 0 src/{prodik => conode}/domain/role/errors.py | 2 +- src/{prodik => conode}/domain/role/model.py | 10 +- .../domain/shared/__init__.py | 0 .../domain/shared/entity.py | 0 .../domain/shared/value_object.py | 0 .../domain/user/__init__.py | 0 src/{prodik => conode}/domain/user/errors.py | 2 +- src/{prodik => conode}/domain/user/model.py | 11 +- .../infrastructure/__init__.py | 0 .../infrastructure/config.py | 19 +- .../infrastructure/identity_provider.py | 38 + .../infrastructure/password_hasher.py | 2 +- .../infrastructure/persistence/__init__.py | 0 .../infrastructure/persistence/alembic.ini | 0 .../persistence/migrations/README | 0 .../persistence/migrations/__init__.py | 0 .../persistence/migrations/env.py | 4 +- .../persistence/migrations/script.py.mako | 0 .../versions/0333150881b1_parent_group_id.py | 0 .../versions/080a7bac6dfb_custom_type.py | 0 .../1786a4b3cea6_remove_company_id.py | 0 .../2e3b8d977df1_offers_and_contracts.py | 0 .../migrations/versions/3057b93fb0cb_bio.py | 0 .../versions/33f30ca8ca69_company.py | 0 .../versions/4a52d766f55e_nullable_from.py | 0 .../versions/4a8ee55b96fa_unique_constrait.py | 0 .../504f086cc1a2_permission_type_offer.py | 0 .../5192c880eb4e_edge_unique_constrait.py | 0 .../versions/6240bee5cfae_remove_owner_id.py | 0 .../versions/658eaeeb4857_edge_company_id.py | 0 ...046693f2e1_renamed_hashed_password_name.py | 0 .../versions/6c7e8a84a7c8_grants.py | 0 .../74a91a825965_remove_old_authorization.py | 53 + .../78521bfa1ec2_contexts_and_edges.py | 0 .../versions/88413fda6e9e_initial.py | 0 .../versions/b5ff44a6d487_nodes_and_groups.py | 0 .../versions/d221025bd616_role_name.py | 0 .../e60ec6d4a0a5_roles_and_permissions.py | 0 .../ee772395dc0d_association_cascade.py | 0 .../versions/ee89ddc73033_contexts.py | 0 .../infrastructure/persistence/registry.py | 52 +- .../infrastructure/persistence/types.py | 16 +- .../infrastructure/repositories/__init__.py | 3 - .../infrastructure/repositories/company.py | 8 +- .../infrastructure/repositories/context.py | 8 +- .../infrastructure/repositories/contract.py | 4 +- .../infrastructure/repositories/edge.py | 10 +- .../repositories/grant/__init__.py | 0 .../infrastructure/repositories/grant/user.py | 8 +- .../infrastructure/repositories/group.py | 8 +- .../infrastructure/repositories/node.py | 10 +- .../infrastructure/repositories/offer.py | 6 +- .../infrastructure/repositories/permission.py | 6 +- .../infrastructure/repositories/role.py | 10 +- .../infrastructure/repositories/user.py | 9 +- src/conode/infrastructure/token_manager.py | 39 + .../infrastructure/transaction_manager.py | 2 +- .../presentation/__init__.py | 0 src/{prodik => conode}/presentation/common.py | 10 +- .../presentation/exceptions/__init__.py | 0 .../presentation/exceptions/handlers.py | 16 +- .../presentation/middlewares/__init__.py | 0 .../presentation/middlewares/log.py | 0 .../presentation/schemas/__init__.py | 0 .../presentation/schemas/company.py | 6 +- .../presentation/schemas/context.py | 6 +- .../presentation/schemas/edge.py | 8 +- .../presentation/schemas/group.py | 6 +- .../presentation/schemas/node.py | 8 +- .../presentation/schemas/offer.py | 10 +- .../presentation/schemas/role.py | 6 +- .../presentation/schemas/user.py | 12 +- .../presentation/views/__init__.py | 2 - .../presentation/views/company.py | 8 +- .../presentation/views/context.py | 6 +- .../presentation/views/edge.py | 14 +- .../presentation/views/group.py | 6 +- .../presentation/views/node.py | 22 +- .../presentation/views/offer.py | 6 +- .../presentation/views/role.py | 6 +- .../presentation/views/root.py | 0 .../presentation/views/user.py | 37 +- src/{prodik => conode}/py.typed | 0 .../application/authorization/__init__.py | 11 - src/prodik/application/authorization/login.py | 102 - .../application/authorization/refresh.py | 47 - .../application/authorization/register.py | 105 - .../repositories/authorization/__init__.py | 7 - .../authorization/local_authorization.py | 11 - .../repositories/authorization/session.py | 13 - .../interfaces/token_managers/__init__.py | 13 - .../token_managers/access_token_manager.py | 19 - .../token_managers/refresh_token_manager.py | 5 - .../manage_credentials/__init__.py | 6 - .../update_current_user_password.py | 103 - src/prodik/bootstrap/di/providers/cache.py | 20 - src/prodik/domain/authorization/__init__.py | 12 - .../domain/authorization/local/__init__.py | 6 - .../domain/authorization/local/model.py | 34 - .../domain/authorization/oauth/__init__.py | 6 - .../domain/authorization/oauth/model.py | 24 - .../domain/authorization/session/__init__.py | 6 - .../domain/authorization/session/model.py | 32 - .../infrastructure/identity_provider.py | 60 - .../repositories/authorization/__init__.py | 7 - .../authorization/local_authorization.py | 76 - .../repositories/authorization/session.py | 118 - .../infrastructure/token_managers/__init__.py | 7 - .../token_managers/access_token_manager.py | 43 - .../token_managers/refresh_token_manager.py | 10 - src/prodik/presentation/schemas/auth.py | 53 - src/prodik/presentation/views/auth.py | 75 - tests/conftest.py | 108 +- tests/e2e/authentication/__init__.py | 0 tests/e2e/authentication/test_login.py | 70 - .../e2e/authentication/test_refresh_token.py | 68 - tests/e2e/authentication/test_register.py | 71 - tests/e2e/company/test_register_company.py | 2 +- tests/e2e/company/test_verify_company.py | 2 +- tests/e2e/context/test_create_context.py | 4 +- tests/e2e/context/test_delete_context.py | 2 +- tests/e2e/edge/test_create_edge.py | 2 +- tests/e2e/edge/test_delete_edge.py | 2 +- tests/e2e/edge/test_manipulate_edge_weight.py | 2 +- tests/e2e/group/test_create_group.py | 2 +- tests/e2e/group/test_delete_group.py | 2 +- tests/e2e/helpers/asserts.py | 6 +- tests/e2e/node/test_attach_node.py | 2 +- tests/e2e/node/test_create_node.py | 4 +- tests/e2e/node/test_delete_node.py | 2 +- tests/e2e/node/test_detach_node.py | 2 +- tests/e2e/node/test_update_node.py | 4 +- tests/e2e/user/test_update_current_profile.py | 5 +- tests/factories/common.py | 2 +- tests/factories/models/company.py | 14 +- tests/factories/models/context.py | 8 +- tests/factories/models/edge.py | 12 +- tests/factories/models/group.py | 8 +- tests/factories/models/node.py | 10 +- tests/factories/models/role.py | 14 +- tests/factories/models/user.py | 54 +- tests/factories/schemas/__init__.py | 10 - tests/factories/schemas/authentication.py | 17 - tests/factories/schemas/company.py | 2 +- tests/factories/schemas/context.py | 2 +- tests/factories/schemas/edge.py | 2 +- tests/factories/schemas/group.py | 2 +- tests/factories/schemas/node.py | 2 +- tests/factories/schemas/user.py | 8 +- uv.lock | 1641 ++++++++- 280 files changed, 5528 insertions(+), 2293 deletions(-) create mode 100644 deploy/init.sql create mode 100644 deploy/keycloak.env create mode 100644 deploy/keycloak.json delete mode 100644 deploy/nginx.conf create mode 100644 deploy/oauth_proxy.toml rename src/{prodik => conode}/application/__init__.py (100%) rename src/{prodik => conode}/application/attach_node_to_group/__init__.py (100%) rename src/{prodik => conode}/application/attach_node_to_group/attach_node.py (85%) rename src/{prodik => conode}/application/detach_node_from_group/__init__.py (100%) rename src/{prodik => conode}/application/detach_node_from_group/detach_node.py (82%) rename src/{prodik => conode}/application/errors.py (100%) rename src/{prodik => conode}/application/interfaces/__init__.py (100%) rename src/{prodik => conode}/application/interfaces/identity_provider.py (70%) rename src/{prodik => conode}/application/interfaces/password_hasher.py (100%) rename src/{prodik => conode}/application/interfaces/repositories/__init__.py (87%) rename src/{prodik => conode}/application/interfaces/repositories/company.py (82%) rename src/{prodik => conode}/application/interfaces/repositories/context.py (81%) rename src/{prodik => conode}/application/interfaces/repositories/contract.py (74%) rename src/{prodik => conode}/application/interfaces/repositories/edge.py (82%) rename src/{prodik => conode}/application/interfaces/repositories/grant/__init__.py (100%) rename src/{prodik => conode}/application/interfaces/repositories/grant/company.py (100%) rename src/{prodik => conode}/application/interfaces/repositories/grant/user.py (77%) rename src/{prodik => conode}/application/interfaces/repositories/group.py (81%) rename src/{prodik => conode}/application/interfaces/repositories/node.py (88%) rename src/{prodik => conode}/application/interfaces/repositories/offer.py (93%) rename src/{prodik => conode}/application/interfaces/repositories/permissions.py (89%) rename src/{prodik => conode}/application/interfaces/repositories/role.py (82%) rename src/{prodik => conode}/application/interfaces/repositories/user.py (89%) create mode 100644 src/conode/application/interfaces/token_manager/__init__.py create mode 100644 src/conode/application/interfaces/token_manager/token_manager.py rename src/{prodik => conode}/application/interfaces/transaction_manager.py (100%) rename src/{prodik => conode}/application/manage_context/__init__.py (100%) rename src/{prodik => conode}/application/manage_context/create_context.py (80%) rename src/{prodik => conode}/application/manage_context/delete_context.py (77%) rename src/{prodik => conode}/application/manage_edge/__init__.py (100%) rename src/{prodik => conode}/application/manage_edge/create_edge.py (85%) rename src/{prodik => conode}/application/manage_edge/delete_edge.py (80%) rename src/{prodik => conode}/application/manage_group/__init__.py (100%) rename src/{prodik => conode}/application/manage_group/create_group.py (84%) rename src/{prodik => conode}/application/manage_group/delete_group.py (77%) rename src/{prodik => conode}/application/manage_node/__init__.py (100%) rename src/{prodik => conode}/application/manage_node/create_node.py (85%) rename src/{prodik => conode}/application/manage_node/delete_node.py (84%) rename src/{prodik => conode}/application/manage_profile/__init__.py (100%) rename src/{prodik => conode}/application/manage_profile/update_current_user_profile.py (53%) rename src/{prodik => conode}/application/manage_role/__init__.py (100%) rename src/{prodik => conode}/application/manage_role/create_role.py (88%) rename src/{prodik => conode}/application/manage_role/delete_role.py (74%) rename src/{prodik => conode}/application/manage_role/update_role.py (91%) rename src/{prodik => conode}/application/manage_user_rights/__init__.py (100%) rename src/{prodik => conode}/application/manage_user_rights/give_role_to_user.py (81%) rename src/{prodik => conode}/application/manage_user_rights/revoke_role_from_user.py (83%) rename src/{prodik => conode}/application/receive_context_info/__init__.py (100%) rename src/{prodik => conode}/application/receive_context_info/get_context_by_id.py (74%) rename src/{prodik => conode}/application/receive_graph_statistics/__init__.py (100%) rename src/{prodik => conode}/application/receive_graph_statistics/neighbours.py (86%) rename src/{prodik => conode}/application/receive_graph_statistics/shortest_path.py (91%) rename src/{prodik => conode}/application/receive_group_info/__init__.py (100%) rename src/{prodik => conode}/application/receive_group_info/get_group_by_id.py (74%) rename src/{prodik => conode}/application/receive_node_info/__init__.py (100%) rename src/{prodik => conode}/application/receive_node_info/get_nodes_by_group.py (82%) rename src/{prodik => conode}/application/receive_user_info/__init__.py (100%) rename src/{prodik => conode}/application/receive_user_info/get_current_user.py (71%) rename src/{prodik => conode}/application/receive_user_info/get_user_by_username.py (71%) rename src/{prodik => conode}/application/register_company/__init__.py (100%) rename src/{prodik => conode}/application/register_company/register_company.py (87%) rename src/{prodik => conode}/application/services/__init__.py (100%) rename src/{prodik => conode}/application/services/access_control.py (88%) rename src/{prodik => conode}/application/services/offer_acceptance.py (89%) rename src/{prodik => conode}/application/services/offer_sending.py (91%) rename src/{prodik => conode}/application/services/role_managment.py (94%) rename src/{prodik => conode}/application/share_graph/__init__.py (100%) rename src/{prodik => conode}/application/share_graph/accept_offer.py (94%) rename src/{prodik => conode}/application/share_graph/decline_offer.py (87%) rename src/{prodik => conode}/application/share_graph/send_offer_to_company.py (90%) rename src/{prodik => conode}/application/update_edge_weight/__init__.py (100%) rename src/{prodik => conode}/application/update_edge_weight/decrement_edge_weight.py (80%) rename src/{prodik => conode}/application/update_edge_weight/increment_edge_weight.py (80%) rename src/{prodik => conode}/application/update_edge_weight/update_edge_weight.py (81%) rename src/{prodik => conode}/application/update_node/__init__.py (100%) rename src/{prodik => conode}/application/update_node/update_node.py (86%) rename src/{prodik => conode}/application/verify_company/__init__.py (100%) rename src/{prodik => conode}/application/verify_company/verify_company.py (79%) rename src/{prodik => conode}/bootstrap/__init__.py (100%) rename src/{prodik => conode}/bootstrap/api/__init__.py (100%) rename src/{prodik => conode}/bootstrap/api/run.py (87%) rename src/{prodik => conode}/bootstrap/cli.py (88%) rename src/{prodik => conode}/bootstrap/di/__init__.py (100%) rename src/{prodik => conode}/bootstrap/di/container.py (75%) rename src/{prodik => conode}/bootstrap/di/providers/__init__.py (76%) rename src/{prodik => conode}/bootstrap/di/providers/application.py (60%) rename src/{prodik => conode}/bootstrap/di/providers/connection.py (73%) rename src/{prodik => conode}/bootstrap/di/providers/infrastructure.py (71%) create mode 100644 src/conode/bootstrap/di/providers/secrets.py rename src/{prodik => conode}/bootstrap/logs.py (100%) rename src/{prodik => conode}/domain/__init__.py (100%) rename src/{prodik => conode}/domain/company/__init__.py (100%) rename src/{prodik => conode}/domain/company/errors.py (79%) rename src/{prodik => conode}/domain/company/model.py (94%) rename src/{prodik => conode}/domain/context/__init__.py (100%) rename src/{prodik => conode}/domain/context/errors.py (79%) rename src/{prodik => conode}/domain/context/model.py (92%) rename src/{prodik => conode}/domain/contract/__init__.py (100%) rename src/{prodik => conode}/domain/contract/errors.py (71%) rename src/{prodik => conode}/domain/contract/model.py (89%) rename src/{prodik => conode}/domain/edge/__init__.py (100%) rename src/{prodik => conode}/domain/edge/errors.py (78%) rename src/{prodik => conode}/domain/edge/model.py (89%) rename src/{prodik => conode}/domain/grant/__init__.py (100%) rename src/{prodik => conode}/domain/grant/company.py (82%) rename src/{prodik => conode}/domain/grant/user.py (81%) rename src/{prodik => conode}/domain/group/__init__.py (100%) rename src/{prodik => conode}/domain/group/errors.py (83%) rename src/{prodik => conode}/domain/group/model.py (94%) rename src/{prodik => conode}/domain/node/__init__.py (100%) rename src/{prodik => conode}/domain/node/errors.py (78%) rename src/{prodik => conode}/domain/node/model.py (93%) rename src/{prodik => conode}/domain/offer/__init__.py (100%) rename src/{prodik => conode}/domain/offer/errors.py (89%) rename src/{prodik => conode}/domain/offer/model.py (95%) rename src/{prodik => conode}/domain/role/__init__.py (100%) rename src/{prodik => conode}/domain/role/errors.py (69%) rename src/{prodik => conode}/domain/role/model.py (91%) rename src/{prodik => conode}/domain/shared/__init__.py (100%) rename src/{prodik => conode}/domain/shared/entity.py (100%) rename src/{prodik => conode}/domain/shared/value_object.py (100%) rename src/{prodik => conode}/domain/user/__init__.py (100%) rename src/{prodik => conode}/domain/user/errors.py (86%) rename src/{prodik => conode}/domain/user/model.py (93%) rename src/{prodik => conode}/infrastructure/__init__.py (100%) rename src/{prodik => conode}/infrastructure/config.py (82%) create mode 100644 src/conode/infrastructure/identity_provider.py rename src/{prodik => conode}/infrastructure/password_hasher.py (89%) rename src/{prodik => conode}/infrastructure/persistence/__init__.py (100%) rename src/{prodik => conode}/infrastructure/persistence/alembic.ini (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/README (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/__init__.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/env.py (96%) rename src/{prodik => conode}/infrastructure/persistence/migrations/script.py.mako (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/080a7bac6dfb_custom_type.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/1786a4b3cea6_remove_company_id.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/2e3b8d977df1_offers_and_contracts.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/3057b93fb0cb_bio.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/33f30ca8ca69_company.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/4a52d766f55e_nullable_from.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/504f086cc1a2_permission_type_offer.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/6240bee5cfae_remove_owner_id.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/658eaeeb4857_edge_company_id.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/6c046693f2e1_renamed_hashed_password_name.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/6c7e8a84a7c8_grants.py (100%) create mode 100644 src/conode/infrastructure/persistence/migrations/versions/74a91a825965_remove_old_authorization.py rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/78521bfa1ec2_contexts_and_edges.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/88413fda6e9e_initial.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/b5ff44a6d487_nodes_and_groups.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/d221025bd616_role_name.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/e60ec6d4a0a5_roles_and_permissions.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/ee772395dc0d_association_cascade.py (100%) rename src/{prodik => conode}/infrastructure/persistence/migrations/versions/ee89ddc73033_contexts.py (100%) rename src/{prodik => conode}/infrastructure/persistence/registry.py (87%) rename src/{prodik => conode}/infrastructure/persistence/types.py (84%) rename src/{prodik => conode}/infrastructure/repositories/__init__.py (87%) rename src/{prodik => conode}/infrastructure/repositories/company.py (94%) rename src/{prodik => conode}/infrastructure/repositories/context.py (92%) rename src/{prodik => conode}/infrastructure/repositories/contract.py (90%) rename src/{prodik => conode}/infrastructure/repositories/edge.py (94%) rename src/{prodik => conode}/infrastructure/repositories/grant/__init__.py (100%) rename src/{prodik => conode}/infrastructure/repositories/grant/user.py (91%) rename src/{prodik => conode}/infrastructure/repositories/group.py (92%) rename src/{prodik => conode}/infrastructure/repositories/node.py (97%) rename src/{prodik => conode}/infrastructure/repositories/offer.py (97%) rename src/{prodik => conode}/infrastructure/repositories/permission.py (95%) rename src/{prodik => conode}/infrastructure/repositories/role.py (92%) rename src/{prodik => conode}/infrastructure/repositories/user.py (91%) create mode 100644 src/conode/infrastructure/token_manager.py rename src/{prodik => conode}/infrastructure/transaction_manager.py (94%) rename src/{prodik => conode}/presentation/__init__.py (100%) rename src/{prodik => conode}/presentation/common.py (78%) rename src/{prodik => conode}/presentation/exceptions/__init__.py (100%) rename src/{prodik => conode}/presentation/exceptions/handlers.py (88%) rename src/{prodik => conode}/presentation/middlewares/__init__.py (100%) rename src/{prodik => conode}/presentation/middlewares/log.py (100%) rename src/{prodik => conode}/presentation/schemas/__init__.py (100%) rename src/{prodik => conode}/presentation/schemas/company.py (86%) rename src/{prodik => conode}/presentation/schemas/context.py (83%) rename src/{prodik => conode}/presentation/schemas/edge.py (67%) rename src/{prodik => conode}/presentation/schemas/group.py (85%) rename src/{prodik => conode}/presentation/schemas/node.py (86%) rename src/{prodik => conode}/presentation/schemas/offer.py (73%) rename src/{prodik => conode}/presentation/schemas/role.py (91%) rename src/{prodik => conode}/presentation/schemas/user.py (80%) rename src/{prodik => conode}/presentation/views/__init__.py (90%) rename src/{prodik => conode}/presentation/views/company.py (83%) rename src/{prodik => conode}/presentation/views/context.py (87%) rename src/{prodik => conode}/presentation/views/edge.py (89%) rename src/{prodik => conode}/presentation/views/group.py (88%) rename src/{prodik => conode}/presentation/views/node.py (87%) rename src/{prodik => conode}/presentation/views/offer.py (92%) rename src/{prodik => conode}/presentation/views/role.py (95%) rename src/{prodik => conode}/presentation/views/root.py (100%) rename src/{prodik => conode}/presentation/views/user.py (67%) rename src/{prodik => conode}/py.typed (100%) delete mode 100644 src/prodik/application/authorization/__init__.py delete mode 100644 src/prodik/application/authorization/login.py delete mode 100644 src/prodik/application/authorization/refresh.py delete mode 100644 src/prodik/application/authorization/register.py delete mode 100644 src/prodik/application/interfaces/repositories/authorization/__init__.py delete mode 100644 src/prodik/application/interfaces/repositories/authorization/local_authorization.py delete mode 100644 src/prodik/application/interfaces/repositories/authorization/session.py delete mode 100644 src/prodik/application/interfaces/token_managers/__init__.py delete mode 100644 src/prodik/application/interfaces/token_managers/access_token_manager.py delete mode 100644 src/prodik/application/interfaces/token_managers/refresh_token_manager.py delete mode 100644 src/prodik/application/manage_credentials/__init__.py delete mode 100644 src/prodik/application/manage_credentials/update_current_user_password.py delete mode 100644 src/prodik/bootstrap/di/providers/cache.py delete mode 100644 src/prodik/domain/authorization/__init__.py delete mode 100644 src/prodik/domain/authorization/local/__init__.py delete mode 100644 src/prodik/domain/authorization/local/model.py delete mode 100644 src/prodik/domain/authorization/oauth/__init__.py delete mode 100644 src/prodik/domain/authorization/oauth/model.py delete mode 100644 src/prodik/domain/authorization/session/__init__.py delete mode 100644 src/prodik/domain/authorization/session/model.py delete mode 100644 src/prodik/infrastructure/identity_provider.py delete mode 100644 src/prodik/infrastructure/repositories/authorization/__init__.py delete mode 100644 src/prodik/infrastructure/repositories/authorization/local_authorization.py delete mode 100644 src/prodik/infrastructure/repositories/authorization/session.py delete mode 100644 src/prodik/infrastructure/token_managers/__init__.py delete mode 100644 src/prodik/infrastructure/token_managers/access_token_manager.py delete mode 100644 src/prodik/infrastructure/token_managers/refresh_token_manager.py delete mode 100644 src/prodik/presentation/schemas/auth.py delete mode 100644 src/prodik/presentation/views/auth.py delete mode 100644 tests/e2e/authentication/__init__.py delete mode 100644 tests/e2e/authentication/test_login.py delete mode 100644 tests/e2e/authentication/test_refresh_token.py delete mode 100644 tests/e2e/authentication/test_register.py delete mode 100644 tests/factories/schemas/authentication.py diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 8462e75..1000f6b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,38 +1,38 @@ -name: Python CI +name: Checks on: push: - branches: ["dev", "master"] + branches: + - master + - dev pull_request: - branches: ["dev", "master"] + branches: + - master + - dev permissions: contents: read jobs: - build: + lint: + name: Lint & type-check (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ["3.12", "3.13"] + python-version: ["3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: python-version: ${{ matrix.python-version }} - cache: "pip" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] - - - name: Ruff - run: ruff check . - - name: Mypy - run: mypy . + - run: uv sync --group dev + - run: uv run ruff check src + - run: uv run ruff format --check src + - run: uv run mypy . --strict \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b5bf309..1654041 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.13-slim-bookworm AS python-base ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ - APP_PATH=/prodik \ + APP_PATH=/conode \ UV_VERSION=0.9.22 ENV VIRTUAL_ENV="$APP_PATH/.venv" diff --git a/README.md b/README.md index 6b25965..f5e6450 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -Fun, useless (maybe), interactive project +_+ +Fun, useless (maybe), interactive project +_+ diff --git a/deploy/init.sql b/deploy/init.sql new file mode 100644 index 0000000..06aabe8 --- /dev/null +++ b/deploy/init.sql @@ -0,0 +1,27 @@ +-- Initialize user and database for KeyCloak + +CREATE ROLE keycloak +WITH + LOGIN + PASSWORD 'keycloakPassword'; + +CREATE DATABASE keycloak + OWNER keycloak + ENCODING 'UTF8' + TEMPLATE template0; + +\connect keycloak; + +GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak; + +GRANT ALL ON SCHEMA public TO keycloak; +ALTER SCHEMA public OWNER TO keycloak; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public +GRANT ALL ON TABLES TO keycloak; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public +GRANT ALL ON SEQUENCES TO keycloak; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public +GRANT ALL ON FUNCTIONS TO keycloak; \ No newline at end of file diff --git a/deploy/keycloak.env b/deploy/keycloak.env new file mode 100644 index 0000000..0405a4e --- /dev/null +++ b/deploy/keycloak.env @@ -0,0 +1,8 @@ +KC_DB=postgres +KC_DB_USERNAME=keycloak +KC_DB_URL_HOST=postgres +KC_DB_URL_PORT=5432 +KC_BOOTSTRAP_ADMIN_USERNAME=keycloak +KC_BOOTSTRAP_ADMIN_PASSWORD=keycloakPassword +KC_DB_PASSWORD=keycloakPassword +KC_HEALTH_ENABLED=true \ No newline at end of file diff --git a/deploy/keycloak.json b/deploy/keycloak.json new file mode 100644 index 0000000..be218da --- /dev/null +++ b/deploy/keycloak.json @@ -0,0 +1,3099 @@ +{ + "id": "ec41a80a-173f-4609-8a8d-b1fac4205041", + "realm": "conode", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "bruteForceStrategy": "MULTIPLE", + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "maxSecondaryAuthFailures": 0, + "roles": { + "realm": [ + { + "id": "49a995af-a1d7-42ab-8a43-966a78ef4331", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "ec41a80a-173f-4609-8a8d-b1fac4205041", + "attributes": {} + }, + { + "id": "ddce9f6a-35d1-410d-8024-f256234f531a", + "name": "admin", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "ec41a80a-173f-4609-8a8d-b1fac4205041", + "attributes": {} + }, + { + "id": "2bebf11f-cdf1-4aff-b428-40430e728bd4", + "name": "default-roles-conode", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "realm-management": [ + "view-realm", + "query-users", + "impersonation", + "query-organizations", + "query-clients", + "view-clients", + "view-events", + "view-users", + "query-groups", + "view-authorization", + "query-realms" + ], + "broker": [ + "read-token" + ], + "account": [ + "delete-account", + "view-groups", + "view-applications", + "manage-account-links", + "view-consent", + "manage-consent", + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "ec41a80a-173f-4609-8a8d-b1fac4205041", + "attributes": {} + }, + { + "id": "e1b0dbeb-0764-45fc-956a-a12aebaf0f14", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "ec41a80a-173f-4609-8a8d-b1fac4205041", + "attributes": {} + } + ], + "client": { + "conode": [ + { + "id": "1f2189b1-ec0e-4a51-a496-5ccf64330044", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "4a49a878-bf46-409c-881c-16896bdef462", + "attributes": {} + } + ], + "realm-management": [ + { + "id": "13d780d4-44c5-433d-b26d-719ef5fb695f", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "90dca780-0937-40f3-a533-12abf29e90c9", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "0c867e97-e15c-4a34-b012-f07e8b996564", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "91c01681-77c9-4b12-ada0-8b3c4d104d16", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "e315a1f6-4ac0-4d06-90e5-31616801ec9e", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "f086cb99-afdc-4724-af7d-e9d3ff5e6818", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "67209571-2887-486f-8cba-146fb12ddd10", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "635a48ad-43d7-4b4a-b6a3-1c747297fc3b", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "b883cdae-9702-4df2-a8bf-d48302462a92", + "name": "query-organizations", + "description": "${role_query-organizations}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "e5a35f08-7dc7-437d-8635-dff0a0cdbdc9", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "26429b17-4e42-4d7c-a35b-1ec74951ec41", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-realm", + "manage-identity-providers", + "query-users", + "create-client", + "manage-events", + "view-identity-providers", + "impersonation", + "query-organizations", + "manage-authorization", + "manage-users", + "query-clients", + "view-clients", + "manage-organizations", + "publish-events", + "view-events", + "view-users", + "query-groups", + "view-organizations", + "view-authorization", + "manage-clients", + "query-realms" + ] + } + }, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "3b908b30-b228-472a-8393-65777261f8a1", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "de68d8b9-f696-4862-87a2-fab2cdf4e88b", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "11306d4c-6f0b-4d6f-a3ed-b3c82997678e", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "b6519e23-766c-4351-b1ac-a4a2e3eb0f21", + "name": "manage-organizations", + "description": "${role_manage-organizations}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "a2413153-b6ec-472b-9daa-57d90bcbf0c5", + "name": "publish-events", + "description": "${role_publish-events}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "71fa910a-68c0-4aa6-b24e-fa08b1647257", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "e1092238-6117-4b18-81b9-3d147478c3af", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "da967960-8b1c-4aa4-bad6-a9575b285263", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "5319760f-a94e-47e6-8a64-bd3be72b5e9f", + "name": "view-organizations", + "description": "${role_view-organizations}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-organizations" + ] + } + }, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "51552ffa-14c0-46d1-9fdc-0dba0687e2bc", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "f4bb4418-7109-4d9b-b681-6130819d30d7", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "c517c084-2b9e-4787-9946-afc1d039b3a9", + "name": "create-organization", + "description": "${role_create-organization}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + }, + { + "id": "07726516-72a9-4397-9c96-c257bc763963", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "2b6ee18a-5dd5-43f4-8c2d-1209a645dae4", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "7904be1a-7d78-4b10-ac95-486e25689078", + "attributes": {} + } + ], + "account": [ + { + "id": "d1ba3ac8-71fc-4ce6-a8dc-f0fc8b709fe0", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "a7929188-ce65-40b3-9ca1-05bc39659e81", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "b737ec04-5145-434a-a952-67e277e5dfc2", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "29dd0736-9496-4eb4-9046-2a85ddfeced5", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "e0723b7a-8ead-4332-ba22-dd048278b678", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "f6b3caf7-b6b6-452b-8466-e5c3cebe475e", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "f874a5b8-3b8d-493d-a347-09ddb1a7658a", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + }, + { + "id": "c79656f9-016f-4a7f-a068-e4033cdabb0f", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "2bebf11f-cdf1-4aff-b428-40430e728bd4", + "name": "default-roles-conode", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "ec41a80a-173f-4609-8a8d-b1fac4205041" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256", + "RS256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256", + "RS256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessResidentKey": "required", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "required", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "2378b129-4f28-4cd9-bc25-24832d3c8b4f", + "username": "service-account-conode", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1784228290542, + "totp": false, + "serviceAccountClientId": "conode", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-conode" + ], + "clientRoles": { + "conode": [ + "uma_protection" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "75942b65-13ab-4635-9368-bdc005bd6bfa", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/conode/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/conode/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "132eb0b9-145b-4141-92d9-c7595b7ae31c", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/conode/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/conode/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "b87ac1f1-ca34-485d-a477-de0f5648315d", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "e0bf2bae-7450-47d4-940c-a25a5d7a9088", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "7904be1a-7d78-4b10-ac95-486e25689078", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "4a49a878-bf46-409c-881c-16896bdef462", + "clientId": "conode", + "name": "Conode", + "description": "Easy-to-use API for building graphs with state contexts. ", + "rootUrl": "http://conode.com", + "adminUrl": "http://conode.com", + "baseUrl": "http://conode.com", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://api.conode.com/oauth2/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "logout.confirmation.enabled": "false", + "client.secret.creation.time": "1784228290", + "oauth2.jwt.authorization.grant.enabled": "false", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "use.jwks.url": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "display.on.consent.screen": "false", + "dpop.bound.access.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "4ab86e46-3496-496f-8bbc-fdeff0c12a79", + "name": "aud-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "conode", + "id.token.claim": "false", + "lightweight.claim": "false", + "access.token.claim": "true", + "introspection.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "service_account", + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [], + "policies": [], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "e17b8210-42bb-4c4e-9de1-cf71c737dee9", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + }, + { + "id": "81c0c5f2-8a07-43f8-af8c-648586a203ef", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/conode/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/conode/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "531e9c28-1c10-455b-940e-911a0a0f6419", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "organization", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "8bbf89b4-7236-4ff2-89a5-3728c2b38c4b", + "name": "service_account", + "description": "Specific scope for a client enabled for service accounts", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "c2165ab9-929b-4131-8f0f-6862683241e5", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "id": "45f94f95-1a50-470e-8fd7-c798e1e961fa", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "fc1e2541-7286-4f4d-ada6-553be302a8e8", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "8a8108cf-eba7-46d6-a4ae-271a2c01388f", + "name": "organization", + "description": "Additional claims about the organization a subject belongs to", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${organizationScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6465ac88-a49b-4a28-93ca-5eb8f07e2fbc", + "name": "organization", + "protocol": "openid-connect", + "protocolMapper": "oidc-organization-membership-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "organization", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "0acd2827-2a80-4226-92ab-72f86772aff2", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6e8f2433-ee66-4d36-a7b3-1bcd0dcd9d29", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "258353df-f076-4824-af06-416c48faefb5", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "5fbfc1d0-03bf-4339-9cb7-af7ca7e73969", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "85448af4-c41c-42f6-ac45-58226850bb14", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "436794f4-1168-45e8-85a9-51d690d424fb", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "42e3cd61-aed5-4a4d-9b8b-eebdad37a805", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "959a0468-8688-4349-b473-7ca1f962c22d", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "cb285e2b-ac4e-4905-8654-4b242a4f0bfd", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "281dd374-e131-460a-8efd-f448d563b883", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "fbb0888c-19e3-4b8d-ab2f-8ed61e664f1d", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "05b46a46-fde1-4e2e-ad4d-5aff6e803f25", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "76085687-cc5a-4a25-a8c6-063d67945dda", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "b9ef9e68-c26a-4d2a-90f9-e0b201340c37", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "b41e6db7-a840-423d-a8c7-234bfa35d75a", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "43fee56e-094a-4a3b-98da-9185b91a584b", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "5a6fe513-023e-4ad0-82be-2be4765c3468", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "d17da110-609e-47b9-8f38-f79f8be45181", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "e9bd8ed1-554b-4c77-a225-a564b92d5fcb", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "1175d01e-62d0-48d1-bd1f-af409b1ec65b", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "0059d0b3-3f15-4525-a78e-4472842ad865", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "b52a9b4a-b171-4be1-9509-61e6607a2e25", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "307c203d-7474-4833-b883-e5c02ab5ab97", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "0f468a65-1754-4be9-8105-1e6bb1b5c55f", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "2839fee0-de0d-4c6c-9328-868e16c45d92", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "f0c6a48e-ad11-40d1-9714-311fa79e7fce", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "a4f804a0-f845-4672-b1e4-35b048bb27d1", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "9869edaf-21a9-4dec-a5d9-13b7a565136b", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fc63864e-3391-4ce0-b908-0647119a3a8b", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "3a020067-bd91-4127-963f-50bc2fd4b05a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "99310df5-6af0-4a2e-aa9e-cbd6992d259d", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "31490237-0398-41b3-a1ed-dcc69a7ef1fc", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "ea53e139-b0ec-4f61-bd8f-5a974ba66da5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "5f5cdb3b-ec94-43ae-ab80-b5ca8acceb82", + "name": "saml_organization", + "description": "Organization Membership", + "protocol": "saml", + "attributes": { + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "d307f0d4-e1b3-493d-bf51-d721b90b6cd8", + "name": "organization", + "protocol": "saml", + "protocolMapper": "saml-organization-membership-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "d70b8ad4-82c1-4567-9b11-5eb880489ebc", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "7f6a6f9e-34c4-4ff3-b4bb-c342f10f3651", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "ad635e8a-73f2-40c8-bc4e-44ed2baac881", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "e0feb8b3-0035-449c-be17-ae5140fbb675", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "af02940a-fe65-4d2b-9028-e5a75ee3182f", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "39163d5a-c8d0-4edf-8388-194be5134053", + "name": "AuthnContextClassRef", + "description": "AuthnContextClassRef Level of Authentiation", + "protocol": "saml", + "attributes": {}, + "protocolMappers": [ + { + "id": "d91a12f6-3f8c-4cce-b411-8c66768277b4", + "name": "AuthnContextClassRef", + "protocol": "saml", + "protocolMapper": "saml-authn-context-class-ref-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "7ab00999-4f7b-4b3e-a139-3ba33692b9f5", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "ef48ad34-d63e-440a-b142-afea9a37e4cb", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "saml_organization", + "AuthnContextClassRef", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt", + "organization" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "loginTheme": "phasetwo-ui", + "accountTheme": "phasetwo-ui", + "adminTheme": "phasetwo-ui", + "emailTheme": "phasetwo-ui", + "eventsEnabled": true, + "eventsListeners": [ + "ext-event-http", + "jboss-logging", + "ext-event-script", + "ext-event-webhook" + ], + "enabledEventTypes": [ + "UPDATE_CONSENT_ERROR", + "UPDATE_TOTP", + "UPDATE_CREDENTIAL", + "IMPERSONATE_ERROR", + "CUSTOM_REQUIRED_ACTION", + "RESTART_AUTHENTICATION", + "VERIFIABLE_CREDENTIAL_OFFER_REQUEST", + "IMPERSONATE", + "LOGIN", + "CLIENT_INITIATED_ACCOUNT_LINKING", + "OAUTH2_EXTENSION_GRANT", + "USER_DISABLED_BY_PERMANENT_LOCKOUT", + "VERIFIABLE_CREDENTIAL_REQUEST", + "REMOVE_CREDENTIAL_ERROR", + "TOKEN_EXCHANGE", + "REGISTER", + "DELETE_ACCOUNT_ERROR", + "VERIFIABLE_CREDENTIAL_OFFER_REQUEST_ERROR", + "IDENTITY_PROVIDER_LINK_ACCOUNT", + "USER_DISABLED_BY_TEMPORARY_LOCKOUT", + "DELETE_ACCOUNT", + "UPDATE_PASSWORD", + "IDENTITY_PROVIDER_FIRST_LOGIN", + "VERIFY_EMAIL", + "CLIENT_LOGIN_ERROR", + "RESTART_AUTHENTICATION_ERROR", + "EXECUTE_ACTIONS", + "REMOVE_FEDERATED_IDENTITY_ERROR", + "TOKEN_EXCHANGE_ERROR", + "VERIFIABLE_CREDENTIAL_PRE_AUTHORIZED_GRANT", + "FEDERATED_IDENTITY_OVERRIDE_LINK", + "SEND_IDENTITY_PROVIDER_LINK_ERROR", + "VERIFIABLE_CREDENTIAL_CREATE_OFFER", + "JWT_AUTHORIZATION_GRANT_ERROR", + "VERIFIABLE_CREDENTIAL_PRE_AUTHORIZED_GRANT_ERROR", + "EXECUTE_ACTION_TOKEN_ERROR", + "OAUTH2_EXTENSION_GRANT_ERROR", + "SEND_VERIFY_EMAIL", + "EXECUTE_ACTIONS_ERROR", + "OAUTH2_DEVICE_CODE_TO_TOKEN", + "OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR", + "REVOKE_GRANT_ERROR", + "UPDATE_EMAIL_ERROR", + "INVITE_ORG_ERROR", + "UPDATE_PROFILE", + "VERIFIABLE_CREDENTIAL_REQUEST_ERROR", + "SEND_IDENTITY_PROVIDER_LINK", + "SEND_VERIFY_EMAIL_ERROR", + "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", + "OAUTH2_DEVICE_AUTH_ERROR", + "REMOVE_TOTP_ERROR", + "VERIFY_EMAIL_ERROR", + "CLIENT_UPDATE", + "JWT_AUTHORIZATION_GRANT", + "UPDATE_TOTP_ERROR", + "VERIFY_PROFILE", + "GRANT_CONSENT_ERROR", + "SEND_RESET_PASSWORD", + "GRANT_CONSENT", + "VERIFY_PROFILE_ERROR", + "REMOVE_TOTP", + "REVOKE_GRANT", + "LOGIN_ERROR", + "CLIENT_LOGIN", + "RESET_PASSWORD_ERROR", + "CODE_TO_TOKEN_ERROR", + "OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR", + "UPDATE_PROFILE_ERROR", + "OAUTH2_DEVICE_VERIFY_USER_CODE", + "UPDATE_PASSWORD_ERROR", + "IDENTITY_PROVIDER_LOGIN", + "AUTHREQID_TO_TOKEN", + "LOGOUT", + "CLIENT_REGISTER", + "CLIENT_DELETE", + "FEDERATED_IDENTITY_LINK_ERROR", + "CLIENT_DELETE_ERROR", + "PERMISSION_TOKEN", + "REGISTER_NODE", + "UPDATE_CREDENTIAL_ERROR", + "OAUTH2_DEVICE_AUTH", + "REMOVE_FEDERATED_IDENTITY", + "IDENTITY_PROVIDER_POST_LOGIN", + "VERIFIABLE_CREDENTIAL_CREATE_OFFER_ERROR", + "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", + "FEDERATED_IDENTITY_OVERRIDE_LINK_ERROR", + "UPDATE_EMAIL", + "REGISTER_ERROR", + "EXECUTE_ACTION_TOKEN", + "LOGOUT_ERROR", + "CLIENT_UPDATE_ERROR", + "AUTHREQID_TO_TOKEN_ERROR", + "CLIENT_REGISTER_ERROR", + "FEDERATED_IDENTITY_LINK", + "INVITE_ORG", + "IDENTITY_PROVIDER_LOGIN_ERROR", + "RESET_PASSWORD", + "REMOVE_CREDENTIAL", + "UPDATE_CONSENT", + "SEND_RESET_PASSWORD_ERROR", + "CUSTOM_REQUIRED_ACTION_ERROR", + "IDENTITY_PROVIDER_POST_LOGIN_ERROR", + "CODE_TO_TOKEN", + "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" + ], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "f9b59027-c19f-439f-8734-1c59de02ed1d", + "name": "Allowed Registration Web Origins", + "providerId": "registration-web-origins", + "subType": "authenticated", + "subComponents": {}, + "config": {} + }, + { + "id": "147d6e99-6208-49e9-986f-aecc17906db7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "e97ff134-c0ad-4691-afce-88082ab14386", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "048701e6-eeb1-44c3-9fa4-109df2d9cc55", + "name": "Allowed Registration Web Origins", + "providerId": "registration-web-origins", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "080f41d8-32ec-41ce-a632-f03f3081d665", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "513d33d8-77d7-4991-b4df-06058e4b7c96", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "04f87943-c844-42c0-85dc-04d90772d8c9", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "216a7ba0-35c6-4836-9d9a-99879a74b9ae", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "b5431799-d570-4cd3-bf2a-baddaca33044", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "8c27fcaf-74ba-4622-a1b8-9f6676297dda", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "a5f87151-1431-4d46-a906-32f6354fa33a", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"org.ro.active\",\"displayName\":\"Active organization ID\",\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "e131821d-c569-4ed8-8b5b-5ad84fcc0c13", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "5e14e592-fe10-4f38-bc98-1f12e3d05b83", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "fa40d5cf-3027-40b3-819d-a1a1eae49129", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "a5dfc729-180d-4bfc-839b-92c96b37f6e0", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "authenticationFlows": [ + { + "id": "2c01c801-7adf-4dc4-b002-d7053cd7f207", + "alias": "Account verification options", + "description": "Method with which to verify the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "ce8fefa6-a08c-4ef6-b7ae-23114f8f4c70", + "alias": "Browser - Conditional 2FA", + "description": "Flow to determine if any 2FA is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorConfig": "browser-conditional-credential", + "authenticator": "conditional-credential", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "webauthn-authenticator", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-recovery-authn-code-form", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "5f39626d-9db7-4f77-bd5f-79a53d4bc027", + "alias": "Browser - Conditional Organization", + "description": "Flow to determine if the organization identity-first login is to be used", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "organization", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8603ddbd-7da4-409e-9a48-39ad4d663da2", + "alias": "Cookies Sub-Flow", + "description": "Cookie sub-flow which can be used to switch org.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "ext-select-org", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e445c393-f9a0-405e-b7fb-08c7e7060338", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "5fb98117-9ddf-415b-a541-b84a2879fdd0", + "alias": "First Broker Login - Conditional Organization", + "description": "Flow to determine if the authenticator that adds organization members is to be used", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "idp-add-organization-member", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6e11765b-49f7-4896-a911-9fc91414f7e4", + "alias": "First broker login - Conditional 2FA", + "description": "Flow to determine if any 2FA is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorConfig": "first-broker-login-conditional-credential", + "authenticator": "conditional-credential", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "webauthn-authenticator", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-recovery-authn-code-form", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8d0f87a9-64c0-4eee-815c-1006a46265e5", + "alias": "Forms Sub-Flow", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Org Browser - Conditional OTP", + "userSetupAllowed": false + }, + { + "authenticator": "ext-select-org", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e3b30e27-cd94-498f-9444-7e88f1868f6c", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "164311b5-d365-4029-a197-7ffd969854ee", + "alias": "IDP Sub-Flow", + "description": "IDP sub-flow to select org.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "ext-select-org", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ef77b92d-1007-4c1c-ad31-ff15c74fb10b", + "alias": "Org Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "814fe89f-9d8d-4d69-bee5-73819e59b067", + "alias": "Org Browser Flow", + "description": "Browser flow with select organization step.", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "Cookies Sub-Flow", + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "IDP Sub-Flow", + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Forms Sub-Flow", + "userSetupAllowed": false + } + ] + }, + { + "id": "8474c11c-3ae9-4166-9259-7774c4ac1630", + "alias": "Org Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "665e9934-860a-4c85-bf4f-fd7811456702", + "alias": "Org Direct Grant Flow", + "description": "Direct grant flow with select organization step.", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Org Direct Grant - Conditional OTP", + "userSetupAllowed": false + }, + { + "authenticator": "ext-select-org", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f3476a0f-c796-40cb-beba-716f5542fab8", + "alias": "Organization", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional Organization", + "userSetupAllowed": false + } + ] + }, + { + "id": "9984e4ed-07e6-4088-b62f-9c78dcc9bb04", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b61fc983-ae44-4110-b60b-efaa6b0e091e", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "f1ce0698-6ed6-4374-a834-9d31eb17d489", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional 2FA", + "userSetupAllowed": false + } + ] + }, + { + "id": "acced864-c2c9-484f-8e90-639f2994731b", + "alias": "browser", + "description": "Browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 26, + "autheticatorFlow": true, + "flowAlias": "Organization", + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "519f6224-da61-49f6-9a42-5e7a9b766118", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "federated-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8f53d8c1-379c-4ec6-96a0-0c2ec168c8f8", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "fa5e8e61-7d82-4dc3-ac8e-a9d3b5e630d3", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "a923c89d-fa04-4560-8587-1b1e6f12a496", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 60, + "autheticatorFlow": true, + "flowAlias": "First Broker Login - Conditional Organization", + "userSetupAllowed": false + } + ] + }, + { + "id": "ac4b0ce5-bd3e-4f8a-8db2-2a3c5f5ef46e", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional 2FA", + "userSetupAllowed": false + } + ] + }, + { + "id": "1a27d5cc-da95-469f-b78e-d535bb9d4713", + "alias": "registration", + "description": "Registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "b020c213-18ae-4812-bc27-961db8667346", + "alias": "registration form", + "description": "Registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "a2f440c0-dc4b-486a-8299-12909c7cd71a", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "ef4201b1-d166-47c3-8fdc-c254b11d32b1", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "b87b8c69-9a49-4f42-a1e6-41c259d3de38", + "alias": "browser-conditional-credential", + "config": { + "credentials": "webauthn-passwordless" + } + }, + { + "id": "33a0b22e-9103-44ac-8813-48a596f6b021", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "a26f5110-d2c5-4235-a0ad-c40185a64599", + "alias": "first-broker-login-conditional-credential", + "config": { + "credentials": "webauthn-passwordless" + } + }, + { + "id": "4d1e7d2f-cc4f-4d03-a639-c0b61c58f478", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 54, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 57, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "UPDATE_EMAIL", + "name": "Update Email", + "providerId": "UPDATE_EMAIL", + "enabled": false, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 110, + "config": {} + }, + { + "alias": "idp_link", + "name": "Linking Identity Provider", + "providerId": "idp_link", + "enabled": true, + "defaultAction": false, + "priority": 120, + "config": {} + }, + { + "alias": "CONFIGURE_RECOVERY_AUTHN_CODES", + "name": "Recovery Authentication Codes", + "providerId": "CONFIGURE_RECOVERY_AUTHN_CODES", + "enabled": true, + "defaultAction": false, + "priority": 130, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "scimApiEnabled": "false", + "darkMode": "true" + }, + "keycloakVersion": "26.7.0", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "verifiableCredentialsEnabled": false, + "adminPermissionsEnabled": false, + "scimApiEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf deleted file mode 100644 index f442545..0000000 --- a/deploy/nginx.conf +++ /dev/null @@ -1,15 +0,0 @@ -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - - server { - listen 80; - - location / { - proxy_pass http://api:8000; - } - } -} diff --git a/deploy/oauth_proxy.toml b/deploy/oauth_proxy.toml new file mode 100644 index 0000000..8acbeb7 --- /dev/null +++ b/deploy/oauth_proxy.toml @@ -0,0 +1,25 @@ +upstreams=["http://api:8000"] +http_address="0.0.0.0:4180" +reverse_proxy=true +cookie_httponly=true +request_logging=true +auth_logging=true +standard_logging=true +skip_provider_button=true +set_xauthrequest=true +pass_authorization_header=true +set_authorization_header=true +pass_access_token=true +insecure_oidc_allow_unverified_email=true +provider="oidc" +redirect_url="http://api.conode.com/oauth2/callback" +oidc_issuer_url="http://keycloak.conode.com/realms/conode" +scope="openid email profile offline_access" +code_challenge_method="S256" +email_domains="*" +cookie_domains=[".conode.com"] +whitelist_domains=[".conode.com"] +cookie_refresh="0h4m0s" +cookie_expire="720h0m0s" +cookie_secure=false +cookie_csrf_per_request = true \ No newline at end of file diff --git a/deploy/postgres.env b/deploy/postgres.env index f14be83..28dc540 100644 --- a/deploy/postgres.env +++ b/deploy/postgres.env @@ -1,7 +1,5 @@ -POSTGRES_DB=postgres -POSTGRES_USER=postgres -POSTGRES_PASSWORD=admin_password -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 +POSTGRES_DB=conode +POSTGRES_USER=conode +POSTGRES_PASSWORD=conodeSecretPassword LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml index bc3787f..51c6adc 100644 --- a/docker-compose.base.yaml +++ b/docker-compose.base.yaml @@ -1,40 +1,139 @@ services: api: build: . + restart: on-failure - container_name: api - command: ["prodik", "api", "run"] - ports: - - 8000:8000 + command: ["conode", "api", "run"] + + depends_on: + keycloak: + condition: service_healthy + postgres: + condition: service_healthy + migrations: + condition: service_completed_successfully + + networks: + - conode-network + + migrations: + build: . + + restart: on-failure + command: ["conode", "migrations", "upgrade"] + depends_on: postgres: condition: service_healthy + + networks: + - conode-network + + oauth: + image: quay.io/oauth2-proxy/oauth2-proxy:latest + command: ["--config=/etc/oauth2-proxy/oauth2-proxy.toml"] + + environment: + OAUTH2_PROXY_CLIENT_ID: conode + OAUTH2_PROXY_CLIENT_SECRET: HxZkeEWaKX0J1rXNPpkshkhojgm6roqrNxWWtTBtUiwSm4kN1QzIH1cCcTulKPlYqCEFVELVuFu94tqFGDyjuo + OAUTH2_PROXY_COOKIE_SECRET: x3CpXUQxfDgXzDDxIdST3mTbTDosKOlx + + volumes: + - "./deploy/oauth_proxy.toml:/etc/oauth2-proxy/oauth2-proxy.toml" + + labels: + - "traefik.enable=true" + + - "traefik.http.routers.oauth.rule=Host(`api.conode.com`)" + - "traefik.http.routers.oauth.entrypoints=web" + - "traefik.http.services.oauth.loadbalancer.server.port=4180" + networks: - - prodik-network + - conode-network + + depends_on: + keycloak: + condition: service_healthy + + traefik: + image: traefik:latest + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" - redis: - image: redis:latest networks: - prodik-network: + conode-network: aliases: - - cache.literally.io + - keycloak.conode.com + - api.conode.com + + keycloak: + image: quay.io/keycloak/keycloak:latest + + command: start-dev --import-realm + + depends_on: + postgres: + condition: service_healthy + + env_file: + - "./deploy/keycloak.env" + + healthcheck: + test: + - CMD-SHELL + - > + { printf 'HEAD /health/ready HTTP/1.0\r\n\r\n' >&0; + grep -q '200'; } 0<>/dev/tcp/localhost/9000 + interval: 5s + timeout: 10s + retries: 30 + start_period: 30s + + labels: + - "traefik.enable=true" + - "traefik.http.routers.keycloak.rule=Host(`keycloak.conode.com`)" + - "traefik.http.routers.keycloak.entrypoints=web" + - "traefik.http.services.keycloak.loadbalancer.server.port=8080" + + volumes: + - keycloak-data:/opt/keycloak/ + - ./deploy/keycloak.json:/opt/keycloak/data/import/realm-export.json + + networks: + - conode-network postgres: image: postgres:16-alpine + volumes: - postgres-data:/var/lib/postgresql/data - ports: - - 5432:5432 + - ./deploy:/docker-entrypoint-initdb.d:ro + networks: - prodik-network: + conode-network: aliases: - persistence.literally.io + env_file: - "./deploy/postgres.env" + healthcheck: + test: ["CMD", "pg_isready", "-U", "conode", "-d", "conode"] + interval: 5s + timeout: 15s + retries: 5 + volumes: postgres-data: + keycloak-data: networks: - prodik-network: + conode-network: driver: bridge \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 59d92f0..8383f94 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -3,15 +3,19 @@ services: environment: DEBUG: "true" volumes: - - "./src/prodik/:/prodik/src/prodik/" + - "./src/conode/:/conode/src/conode/" - redis: + postgres: ports: - - 6379:6379 + - 5432:5432 - postgres: - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres", "-d", "postgres"] - interval: 5s - timeout: 5s - retries: 5 \ No newline at end of file + traefik: + command: + - "--log.level=DEBUG" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" \ No newline at end of file diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index f3563bb..bdad469 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -3,11 +3,4 @@ services: environment: DEBUG: "false" volumes: - - "./src/prodik/infrastructure/db/migrations/versions/:/prodik/src/prodik/infrastructure/db/migrations/versions/" - - postgres: - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres", "-d", "postgres"] - interval: 15s - timeout: 15s - retries: 5 \ No newline at end of file + - "./src/conode/infrastructure/db/migrations/versions/:/conode/src/conode/infrastructure/db/migrations/versions/" \ No newline at end of file diff --git a/example.config.toml b/example.config.toml index 7ec4eaa..6ed3150 100644 --- a/example.config.toml +++ b/example.config.toml @@ -9,11 +9,9 @@ host = "persistence.literally.io" database = "postgres" port = 5432 -[cache] -host = "cache.literally.io" -port = 6379 -ttl = 3600 - [secrets] secret = "ABC123" -expires_in = 3600 \ No newline at end of file +expires_in = 3600 +jwks_url = "http://keycloak:8080/realms/conode/protocol/openid-connect/certs" +issuer = "http://keycloak.conode.com/realms/conode" +audience = "conode" diff --git a/pyproject.toml b/pyproject.toml index f060acc..7986439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "prodik" +name = "Conode" version = "0.1.0" description = "Great & powerful application" readme = "README.md" @@ -13,30 +13,32 @@ dependencies = [ "fastapi==0.128.0", "pydantic[email]==2.13.2", "pyjwt==2.11.0", - "redis[hiredis]==7.4.0", "sqlalchemy==2.0.46", "structlog==25.5.0", "uvicorn==0.40.0", ] -[project.optional-dependencies] +[dependency-groups] dev = [ "polyfactory==3.3.0", + "bandit==1.9.4", "dirty-equals==0.11", "httpx==0.28.1", "pytest-asyncio==1.3.0", "pytest", "mypy", + "semgrep==0.112.1", "ruff", - "pytest-cov>=7.1.0", - "pytest-xdist>=3.8.0", + "pytest-cov==7.1.0", + "pytest-xdist==3.8.0", + "zizmor==1.27.0", "datamodel-code-generator==0.54.0", ] [tool.ruff] target-version = "py313" line-length = 88 -exclude = [".venv", "src/prodik/infrastructure/persistence/migrations"] +exclude = [".venv", "src/conode/infrastructure/persistence/migrations"] [tool.ruff.lint] extend-select = ["ALL"] @@ -74,7 +76,7 @@ addopts = "--cov-report=term-missing" [tool.mypy] strict = true explicit_package_bases = true -exclude = ["build", "src/prodik/infrastructure/persistence/migrations"] +exclude = ["build", "src/conode/infrastructure/persistence/migrations"] warn_unreachable = true show_column_numbers = true show_error_context = true @@ -86,8 +88,8 @@ mypy_path = "src" plugins = [ "pydantic.mypy", ] -files = ["src/prodik"] +files = ["src/conode"] [project.scripts] -prodik = "prodik.bootstrap.cli:main" +conode = "conode.bootstrap.cli:main" diff --git a/src/prodik/application/__init__.py b/src/conode/application/__init__.py similarity index 100% rename from src/prodik/application/__init__.py rename to src/conode/application/__init__.py diff --git a/src/prodik/application/attach_node_to_group/__init__.py b/src/conode/application/attach_node_to_group/__init__.py similarity index 100% rename from src/prodik/application/attach_node_to_group/__init__.py rename to src/conode/application/attach_node_to_group/__init__.py diff --git a/src/prodik/application/attach_node_to_group/attach_node.py b/src/conode/application/attach_node_to_group/attach_node.py similarity index 85% rename from src/prodik/application/attach_node_to_group/attach_node.py rename to src/conode/application/attach_node_to_group/attach_node.py index 4c8c213..5cbbae3 100644 --- a/src/prodik/application/attach_node_to_group/attach_node.py +++ b/src/conode/application/attach_node_to_group/attach_node.py @@ -1,19 +1,19 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.errors import ( +from conode.application.errors import ( NodeNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.group import GroupId -from prodik.domain.node import NodeAssociation, NodeAssociationId, NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.group import GroupId +from conode.domain.node import NodeAssociation, NodeAssociationId, NodeId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/detach_node_from_group/__init__.py b/src/conode/application/detach_node_from_group/__init__.py similarity index 100% rename from src/prodik/application/detach_node_from_group/__init__.py rename to src/conode/application/detach_node_from_group/__init__.py diff --git a/src/prodik/application/detach_node_from_group/detach_node.py b/src/conode/application/detach_node_from_group/detach_node.py similarity index 82% rename from src/prodik/application/detach_node_from_group/detach_node.py rename to src/conode/application/detach_node_from_group/detach_node.py index 6a9707f..36c2eac 100644 --- a/src/prodik/application/detach_node_from_group/detach_node.py +++ b/src/conode/application/detach_node_from_group/detach_node.py @@ -1,17 +1,17 @@ from dataclasses import dataclass -from prodik.application.errors import ( +from conode.application.errors import ( GroupNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.node import NodeAssociationId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.node import NodeAssociationId @dataclass diff --git a/src/prodik/application/errors.py b/src/conode/application/errors.py similarity index 100% rename from src/prodik/application/errors.py rename to src/conode/application/errors.py diff --git a/src/prodik/application/interfaces/__init__.py b/src/conode/application/interfaces/__init__.py similarity index 100% rename from src/prodik/application/interfaces/__init__.py rename to src/conode/application/interfaces/__init__.py diff --git a/src/prodik/application/interfaces/identity_provider.py b/src/conode/application/interfaces/identity_provider.py similarity index 70% rename from src/prodik/application/interfaces/identity_provider.py rename to src/conode/application/interfaces/identity_provider.py index 4c96635..a3e986a 100644 --- a/src/prodik/application/interfaces/identity_provider.py +++ b/src/conode/application/interfaces/identity_provider.py @@ -1,6 +1,6 @@ from typing import Protocol -from prodik.application.interfaces.token_managers import UserMeta +from conode.application.interfaces.token_manager import UserMeta class IdentityProvider(Protocol): diff --git a/src/prodik/application/interfaces/password_hasher.py b/src/conode/application/interfaces/password_hasher.py similarity index 100% rename from src/prodik/application/interfaces/password_hasher.py rename to src/conode/application/interfaces/password_hasher.py diff --git a/src/prodik/application/interfaces/repositories/__init__.py b/src/conode/application/interfaces/repositories/__init__.py similarity index 87% rename from src/prodik/application/interfaces/repositories/__init__.py rename to src/conode/application/interfaces/repositories/__init__.py index 8fd5625..c1879b0 100644 --- a/src/prodik/application/interfaces/repositories/__init__.py +++ b/src/conode/application/interfaces/repositories/__init__.py @@ -1,4 +1,3 @@ -from .authorization import LocalAuthorizationRepository, SessionRepository from .company import CompanyRepository from .context import ContextRepository from .contract import ContractRepository @@ -23,7 +22,6 @@ "ContractRepository", "EdgeRepository", "GroupRepository", - "LocalAuthorizationRepository", "NodeAssociationRepository", "NodeRepository", "OfferContextRepository", @@ -32,7 +30,6 @@ "OfferRepository", "RolePermissionsRepository", "RoleRepository", - "SessionRepository", "UserGrantRepository", "UserRepository", ) diff --git a/src/prodik/application/interfaces/repositories/company.py b/src/conode/application/interfaces/repositories/company.py similarity index 82% rename from src/prodik/application/interfaces/repositories/company.py rename to src/conode/application/interfaces/repositories/company.py index 5087e6a..ed7b359 100644 --- a/src/prodik/application/interfaces/repositories/company.py +++ b/src/conode/application/interfaces/repositories/company.py @@ -1,7 +1,7 @@ from typing import Protocol -from prodik.domain.company import Company, CompanyId, CompanyName -from prodik.domain.user import UserId +from conode.domain.company import Company, CompanyId, CompanyName +from conode.domain.user import UserId class CompanyRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/context.py b/src/conode/application/interfaces/repositories/context.py similarity index 81% rename from src/prodik/application/interfaces/repositories/context.py rename to src/conode/application/interfaces/repositories/context.py index cf9e547..67efd27 100644 --- a/src/prodik/application/interfaces/repositories/context.py +++ b/src/conode/application/interfaces/repositories/context.py @@ -1,7 +1,7 @@ from typing import Protocol -from prodik.domain.company import CompanyId -from prodik.domain.context import Context, ContextId +from conode.domain.company import CompanyId +from conode.domain.context import Context, ContextId class ContextRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/contract.py b/src/conode/application/interfaces/repositories/contract.py similarity index 74% rename from src/prodik/application/interfaces/repositories/contract.py rename to src/conode/application/interfaces/repositories/contract.py index 02cd78d..a4d1118 100644 --- a/src/prodik/application/interfaces/repositories/contract.py +++ b/src/conode/application/interfaces/repositories/contract.py @@ -1,6 +1,6 @@ from typing import Protocol -from prodik.domain.contract import Contract +from conode.domain.contract import Contract class ContractRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/edge.py b/src/conode/application/interfaces/repositories/edge.py similarity index 82% rename from src/prodik/application/interfaces/repositories/edge.py rename to src/conode/application/interfaces/repositories/edge.py index dab769e..fc97385 100644 --- a/src/prodik/application/interfaces/repositories/edge.py +++ b/src/conode/application/interfaces/repositories/edge.py @@ -1,8 +1,8 @@ from typing import Protocol -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge, EdgeId -from prodik.domain.node import NodeId +from conode.domain.context import ContextId +from conode.domain.edge import Edge, EdgeId +from conode.domain.node import NodeId class EdgeRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/grant/__init__.py b/src/conode/application/interfaces/repositories/grant/__init__.py similarity index 100% rename from src/prodik/application/interfaces/repositories/grant/__init__.py rename to src/conode/application/interfaces/repositories/grant/__init__.py diff --git a/src/prodik/application/interfaces/repositories/grant/company.py b/src/conode/application/interfaces/repositories/grant/company.py similarity index 100% rename from src/prodik/application/interfaces/repositories/grant/company.py rename to src/conode/application/interfaces/repositories/grant/company.py diff --git a/src/prodik/application/interfaces/repositories/grant/user.py b/src/conode/application/interfaces/repositories/grant/user.py similarity index 77% rename from src/prodik/application/interfaces/repositories/grant/user.py rename to src/conode/application/interfaces/repositories/grant/user.py index 66f3896..341a6cd 100644 --- a/src/prodik/application/interfaces/repositories/grant/user.py +++ b/src/conode/application/interfaces/repositories/grant/user.py @@ -1,8 +1,8 @@ from typing import Protocol -from prodik.domain.grant import UserGrant -from prodik.domain.role import RoleId -from prodik.domain.user import UserId +from conode.domain.grant import UserGrant +from conode.domain.role import RoleId +from conode.domain.user import UserId class UserGrantRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/group.py b/src/conode/application/interfaces/repositories/group.py similarity index 81% rename from src/prodik/application/interfaces/repositories/group.py rename to src/conode/application/interfaces/repositories/group.py index 8570233..3f08740 100644 --- a/src/prodik/application/interfaces/repositories/group.py +++ b/src/conode/application/interfaces/repositories/group.py @@ -1,7 +1,7 @@ from typing import Protocol -from prodik.domain.company import CompanyId -from prodik.domain.group import Group, GroupId +from conode.domain.company import CompanyId +from conode.domain.group import Group, GroupId class GroupRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/node.py b/src/conode/application/interfaces/repositories/node.py similarity index 88% rename from src/prodik/application/interfaces/repositories/node.py rename to src/conode/application/interfaces/repositories/node.py index 38acf87..15d94a5 100644 --- a/src/prodik/application/interfaces/repositories/node.py +++ b/src/conode/application/interfaces/repositories/node.py @@ -1,8 +1,8 @@ from typing import Protocol -from prodik.domain.company import CompanyId -from prodik.domain.group import GroupId -from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId +from conode.domain.company import CompanyId +from conode.domain.group import GroupId +from conode.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId class NodeRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/offer.py b/src/conode/application/interfaces/repositories/offer.py similarity index 93% rename from src/prodik/application/interfaces/repositories/offer.py rename to src/conode/application/interfaces/repositories/offer.py index 75bde1b..9bfdbf0 100644 --- a/src/prodik/application/interfaces/repositories/offer.py +++ b/src/conode/application/interfaces/repositories/offer.py @@ -1,6 +1,6 @@ from typing import Protocol -from prodik.domain.offer import Offer, OfferContext, OfferGroup, OfferId, OfferLink +from conode.domain.offer import Offer, OfferContext, OfferGroup, OfferId, OfferLink class OfferRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/permissions.py b/src/conode/application/interfaces/repositories/permissions.py similarity index 89% rename from src/prodik/application/interfaces/repositories/permissions.py rename to src/conode/application/interfaces/repositories/permissions.py index 4229932..c87efb6 100644 --- a/src/prodik/application/interfaces/repositories/permissions.py +++ b/src/conode/application/interfaces/repositories/permissions.py @@ -1,6 +1,6 @@ from typing import Protocol -from prodik.domain.role import RoleId, RolePermission, RolePermissionId +from conode.domain.role import RoleId, RolePermission, RolePermissionId class RolePermissionsRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/role.py b/src/conode/application/interfaces/repositories/role.py similarity index 82% rename from src/prodik/application/interfaces/repositories/role.py rename to src/conode/application/interfaces/repositories/role.py index 7e520a3..ffe2111 100644 --- a/src/prodik/application/interfaces/repositories/role.py +++ b/src/conode/application/interfaces/repositories/role.py @@ -1,7 +1,7 @@ from typing import Protocol -from prodik.domain.company import CompanyId -from prodik.domain.role import Role, RoleId, RoleName +from conode.domain.company import CompanyId +from conode.domain.role import Role, RoleId, RoleName class RoleRepository(Protocol): diff --git a/src/prodik/application/interfaces/repositories/user.py b/src/conode/application/interfaces/repositories/user.py similarity index 89% rename from src/prodik/application/interfaces/repositories/user.py rename to src/conode/application/interfaces/repositories/user.py index f1152f8..05c29f5 100644 --- a/src/prodik/application/interfaces/repositories/user.py +++ b/src/conode/application/interfaces/repositories/user.py @@ -1,6 +1,6 @@ from typing import Protocol -from prodik.domain.user import Email, User, UserId, Username +from conode.domain.user import Email, User, UserId, Username class UserRepository(Protocol): diff --git a/src/conode/application/interfaces/token_manager/__init__.py b/src/conode/application/interfaces/token_manager/__init__.py new file mode 100644 index 0000000..49ee8bb --- /dev/null +++ b/src/conode/application/interfaces/token_manager/__init__.py @@ -0,0 +1,11 @@ +from .token_manager import ( + TokenManager, + TokenManagerResponse, + UserMeta, +) + +__all__ = ( + "TokenManager", + "TokenManagerResponse", + "UserMeta", +) diff --git a/src/conode/application/interfaces/token_manager/token_manager.py b/src/conode/application/interfaces/token_manager/token_manager.py new file mode 100644 index 0000000..5d67513 --- /dev/null +++ b/src/conode/application/interfaces/token_manager/token_manager.py @@ -0,0 +1,21 @@ +from typing import NamedTuple, Protocol, TypedDict + +from conode.domain.user import User, UserSystemRole + + +class UserMeta(TypedDict): + system_role: UserSystemRole + first_name: str + last_name: str + email: str + username: str + + +class TokenManagerResponse(NamedTuple): + token: str + expires_in: int + + +class TokenManager(Protocol): + def decode(self, token: str) -> UserMeta: ... + def encode(self, user: User) -> str: ... diff --git a/src/prodik/application/interfaces/transaction_manager.py b/src/conode/application/interfaces/transaction_manager.py similarity index 100% rename from src/prodik/application/interfaces/transaction_manager.py rename to src/conode/application/interfaces/transaction_manager.py diff --git a/src/prodik/application/manage_context/__init__.py b/src/conode/application/manage_context/__init__.py similarity index 100% rename from src/prodik/application/manage_context/__init__.py rename to src/conode/application/manage_context/__init__.py diff --git a/src/prodik/application/manage_context/create_context.py b/src/conode/application/manage_context/create_context.py similarity index 80% rename from src/prodik/application/manage_context/create_context.py rename to src/conode/application/manage_context/create_context.py index 8db86b2..8e5fc77 100644 --- a/src/prodik/application/manage_context/create_context.py +++ b/src/conode/application/manage_context/create_context.py @@ -1,14 +1,14 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.company import CompanyId -from prodik.domain.context import Context, ContextId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.company import CompanyId +from conode.domain.context import Context, ContextId @dataclass diff --git a/src/prodik/application/manage_context/delete_context.py b/src/conode/application/manage_context/delete_context.py similarity index 77% rename from src/prodik/application/manage_context/delete_context.py rename to src/conode/application/manage_context/delete_context.py index e150bf2..b98d86a 100644 --- a/src/prodik/application/manage_context/delete_context.py +++ b/src/conode/application/manage_context/delete_context.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.context import ContextId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.context import ContextId @dataclass diff --git a/src/prodik/application/manage_edge/__init__.py b/src/conode/application/manage_edge/__init__.py similarity index 100% rename from src/prodik/application/manage_edge/__init__.py rename to src/conode/application/manage_edge/__init__.py diff --git a/src/prodik/application/manage_edge/create_edge.py b/src/conode/application/manage_edge/create_edge.py similarity index 85% rename from src/prodik/application/manage_edge/create_edge.py rename to src/conode/application/manage_edge/create_edge.py index 02fac01..45e0ded 100644 --- a/src/prodik/application/manage_edge/create_edge.py +++ b/src/conode/application/manage_edge/create_edge.py @@ -2,20 +2,20 @@ from typing import Final from uuid import uuid4 -from prodik.application.errors import ( +from conode.application.errors import ( NodeNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge, EdgeId -from prodik.domain.node import NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.context import ContextId +from conode.domain.edge import Edge, EdgeId +from conode.domain.node import NodeId EXPECTED_COUNT_OF_NODES: Final = 2 diff --git a/src/prodik/application/manage_edge/delete_edge.py b/src/conode/application/manage_edge/delete_edge.py similarity index 80% rename from src/prodik/application/manage_edge/delete_edge.py rename to src/conode/application/manage_edge/delete_edge.py index 9b11e2b..0ca6a4b 100644 --- a/src/prodik/application/manage_edge/delete_edge.py +++ b/src/conode/application/manage_edge/delete_edge.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.edge import EdgeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.edge import EdgeId @dataclass diff --git a/src/prodik/application/manage_group/__init__.py b/src/conode/application/manage_group/__init__.py similarity index 100% rename from src/prodik/application/manage_group/__init__.py rename to src/conode/application/manage_group/__init__.py diff --git a/src/prodik/application/manage_group/create_group.py b/src/conode/application/manage_group/create_group.py similarity index 84% rename from src/prodik/application/manage_group/create_group.py rename to src/conode/application/manage_group/create_group.py index 89783d6..53414e5 100644 --- a/src/prodik/application/manage_group/create_group.py +++ b/src/conode/application/manage_group/create_group.py @@ -1,14 +1,14 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.company import CompanyId -from prodik.domain.group import Group, GroupId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.company import CompanyId +from conode.domain.group import Group, GroupId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/manage_group/delete_group.py b/src/conode/application/manage_group/delete_group.py similarity index 77% rename from src/prodik/application/manage_group/delete_group.py rename to src/conode/application/manage_group/delete_group.py index 7c87ece..cd04f3a 100644 --- a/src/prodik/application/manage_group/delete_group.py +++ b/src/conode/application/manage_group/delete_group.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.group import GroupId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.group import GroupId @dataclass diff --git a/src/prodik/application/manage_node/__init__.py b/src/conode/application/manage_node/__init__.py similarity index 100% rename from src/prodik/application/manage_node/__init__.py rename to src/conode/application/manage_node/__init__.py diff --git a/src/prodik/application/manage_node/create_node.py b/src/conode/application/manage_node/create_node.py similarity index 85% rename from src/prodik/application/manage_node/create_node.py rename to src/conode/application/manage_node/create_node.py index a3b287d..49e31b6 100644 --- a/src/prodik/application/manage_node/create_node.py +++ b/src/conode/application/manage_node/create_node.py @@ -1,16 +1,16 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.group import GroupId -from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.group import GroupId +from conode.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/manage_node/delete_node.py b/src/conode/application/manage_node/delete_node.py similarity index 84% rename from src/prodik/application/manage_node/delete_node.py rename to src/conode/application/manage_node/delete_node.py index 9f65c9b..6fe8753 100644 --- a/src/prodik/application/manage_node/delete_node.py +++ b/src/conode/application/manage_node/delete_node.py @@ -1,14 +1,14 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.node import NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.node import NodeId @dataclass diff --git a/src/prodik/application/manage_profile/__init__.py b/src/conode/application/manage_profile/__init__.py similarity index 100% rename from src/prodik/application/manage_profile/__init__.py rename to src/conode/application/manage_profile/__init__.py diff --git a/src/prodik/application/manage_profile/update_current_user_profile.py b/src/conode/application/manage_profile/update_current_user_profile.py similarity index 53% rename from src/prodik/application/manage_profile/update_current_user_profile.py rename to src/conode/application/manage_profile/update_current_user_profile.py index 916b65a..9bec12b 100644 --- a/src/prodik/application/manage_profile/update_current_user_profile.py +++ b/src/conode/application/manage_profile/update_current_user_profile.py @@ -1,10 +1,8 @@ from dataclasses import dataclass -from prodik.application.errors import UserAlreadyExistsError -from prodik.application.interfaces.repositories import UserRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.user import Email +from conode.application.interfaces.repositories import UserRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService @dataclass @@ -12,7 +10,6 @@ class UpdateCurrentUserProfileRequestDTO: username: str first_name: str last_name: str - email: str bio: str @@ -26,17 +23,9 @@ async def execute(self, request: UpdateCurrentUserProfileRequestDTO) -> None: async with self.transaction_manager: user = await self.access_control_service.get_authorized_user() - unique_user = await self.user_repository.get_by_email(Email(request.email)) - if unique_user is not None and unique_user != user: - raise UserAlreadyExistsError( - "User with this email already exists", - [{"key": "email", "value": request.email}], - ) - user.update_profile( first_name=request.first_name, last_name=request.last_name, username=request.username, - email=request.email, bio=request.bio, ) diff --git a/src/prodik/application/manage_role/__init__.py b/src/conode/application/manage_role/__init__.py similarity index 100% rename from src/prodik/application/manage_role/__init__.py rename to src/conode/application/manage_role/__init__.py diff --git a/src/prodik/application/manage_role/create_role.py b/src/conode/application/manage_role/create_role.py similarity index 88% rename from src/prodik/application/manage_role/create_role.py rename to src/conode/application/manage_role/create_role.py index d6cb9f4..b0bd9fa 100644 --- a/src/prodik/application/manage_role/create_role.py +++ b/src/conode/application/manage_role/create_role.py @@ -1,15 +1,15 @@ from dataclasses import dataclass -from prodik.application.errors import RoleAlreadyExistsError -from prodik.application.interfaces.repositories import ( +from conode.application.errors import RoleAlreadyExistsError +from conode.application.interfaces.repositories import ( CompanyRepository, RolePermissionsRepository, RoleRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService, RoleManagmentService -from prodik.domain.company import CompanyId -from prodik.domain.role import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService, RoleManagmentService +from conode.domain.company import CompanyId +from conode.domain.role import ( EntityType, PermissionType, Role, diff --git a/src/prodik/application/manage_role/delete_role.py b/src/conode/application/manage_role/delete_role.py similarity index 74% rename from src/prodik/application/manage_role/delete_role.py rename to src/conode/application/manage_role/delete_role.py index b6b1e09..5a83e37 100644 --- a/src/prodik/application/manage_role/delete_role.py +++ b/src/conode/application/manage_role/delete_role.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import RoleRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.role import RoleId +from conode.application.interfaces.repositories import RoleRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.role import RoleId @dataclass diff --git a/src/prodik/application/manage_role/update_role.py b/src/conode/application/manage_role/update_role.py similarity index 91% rename from src/prodik/application/manage_role/update_role.py rename to src/conode/application/manage_role/update_role.py index f2d4c53..fe2460c 100644 --- a/src/prodik/application/manage_role/update_role.py +++ b/src/conode/application/manage_role/update_role.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( RolePermissionsRepository, RoleRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.role import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.role import ( EntityType, PermissionType, Role, diff --git a/src/prodik/application/manage_user_rights/__init__.py b/src/conode/application/manage_user_rights/__init__.py similarity index 100% rename from src/prodik/application/manage_user_rights/__init__.py rename to src/conode/application/manage_user_rights/__init__.py diff --git a/src/prodik/application/manage_user_rights/give_role_to_user.py b/src/conode/application/manage_user_rights/give_role_to_user.py similarity index 81% rename from src/prodik/application/manage_user_rights/give_role_to_user.py rename to src/conode/application/manage_user_rights/give_role_to_user.py index db70484..1ad1d18 100644 --- a/src/prodik/application/manage_user_rights/give_role_to_user.py +++ b/src/conode/application/manage_user_rights/give_role_to_user.py @@ -1,19 +1,19 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.errors import ( +from conode.application.errors import ( GrantAlreadyExistsError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( RoleRepository, UserGrantRepository, UserRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.grant import UserGrant, UserGrantId -from prodik.domain.role import RoleId -from prodik.domain.user import UserId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.grant import UserGrant, UserGrantId +from conode.domain.role import RoleId +from conode.domain.user import UserId @dataclass diff --git a/src/prodik/application/manage_user_rights/revoke_role_from_user.py b/src/conode/application/manage_user_rights/revoke_role_from_user.py similarity index 83% rename from src/prodik/application/manage_user_rights/revoke_role_from_user.py rename to src/conode/application/manage_user_rights/revoke_role_from_user.py index 09a4ff9..455911a 100644 --- a/src/prodik/application/manage_user_rights/revoke_role_from_user.py +++ b/src/conode/application/manage_user_rights/revoke_role_from_user.py @@ -1,17 +1,17 @@ from dataclasses import dataclass -from prodik.application.errors import ( +from conode.application.errors import ( GrantNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( RoleRepository, UserGrantRepository, UserRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.role import RoleId -from prodik.domain.user import UserId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.role import RoleId +from conode.domain.user import UserId @dataclass diff --git a/src/prodik/application/receive_context_info/__init__.py b/src/conode/application/receive_context_info/__init__.py similarity index 100% rename from src/prodik/application/receive_context_info/__init__.py rename to src/conode/application/receive_context_info/__init__.py diff --git a/src/prodik/application/receive_context_info/get_context_by_id.py b/src/conode/application/receive_context_info/get_context_by_id.py similarity index 74% rename from src/prodik/application/receive_context_info/get_context_by_id.py rename to src/conode/application/receive_context_info/get_context_by_id.py index 681cc83..0668b38 100644 --- a/src/prodik/application/receive_context_info/get_context_by_id.py +++ b/src/conode/application/receive_context_info/get_context_by_id.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ContextRepository -from prodik.application.services import AccessControlService -from prodik.domain.context import Context, ContextId +from conode.application.interfaces.repositories import ContextRepository +from conode.application.services import AccessControlService +from conode.domain.context import Context, ContextId @dataclass diff --git a/src/prodik/application/receive_graph_statistics/__init__.py b/src/conode/application/receive_graph_statistics/__init__.py similarity index 100% rename from src/prodik/application/receive_graph_statistics/__init__.py rename to src/conode/application/receive_graph_statistics/__init__.py diff --git a/src/prodik/application/receive_graph_statistics/neighbours.py b/src/conode/application/receive_graph_statistics/neighbours.py similarity index 86% rename from src/prodik/application/receive_graph_statistics/neighbours.py rename to src/conode/application/receive_graph_statistics/neighbours.py index e638b39..bf378ff 100644 --- a/src/prodik/application/receive_graph_statistics/neighbours.py +++ b/src/conode/application/receive_graph_statistics/neighbours.py @@ -1,16 +1,16 @@ from collections import deque from dataclasses import dataclass -from prodik.application.errors import DepthCannotBeNegativeError -from prodik.application.interfaces.repositories import ( +from conode.application.errors import DepthCannotBeNegativeError +from conode.application.interfaces.repositories import ( ContextRepository, EdgeRepository, NodeRepository, ) -from prodik.application.services import AccessControlService -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge -from prodik.domain.node import Node, NodeId +from conode.application.services import AccessControlService +from conode.domain.context import ContextId +from conode.domain.edge import Edge +from conode.domain.node import Node, NodeId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/receive_graph_statistics/shortest_path.py b/src/conode/application/receive_graph_statistics/shortest_path.py similarity index 91% rename from src/prodik/application/receive_graph_statistics/shortest_path.py rename to src/conode/application/receive_graph_statistics/shortest_path.py index 59018b6..68ae5cb 100644 --- a/src/prodik/application/receive_graph_statistics/shortest_path.py +++ b/src/conode/application/receive_graph_statistics/shortest_path.py @@ -3,11 +3,11 @@ from heapq import heappop, heappush from math import inf -from prodik.application.interfaces.repositories import ContextRepository, EdgeRepository -from prodik.application.services import AccessControlService -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge -from prodik.domain.node import NodeId +from conode.application.interfaces.repositories import ContextRepository, EdgeRepository +from conode.application.services import AccessControlService +from conode.domain.context import ContextId +from conode.domain.edge import Edge +from conode.domain.node import NodeId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/receive_group_info/__init__.py b/src/conode/application/receive_group_info/__init__.py similarity index 100% rename from src/prodik/application/receive_group_info/__init__.py rename to src/conode/application/receive_group_info/__init__.py diff --git a/src/prodik/application/receive_group_info/get_group_by_id.py b/src/conode/application/receive_group_info/get_group_by_id.py similarity index 74% rename from src/prodik/application/receive_group_info/get_group_by_id.py rename to src/conode/application/receive_group_info/get_group_by_id.py index 6d8f722..8529b43 100644 --- a/src/prodik/application/receive_group_info/get_group_by_id.py +++ b/src/conode/application/receive_group_info/get_group_by_id.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import GroupRepository -from prodik.application.services import AccessControlService -from prodik.domain.group import Group, GroupId +from conode.application.interfaces.repositories import GroupRepository +from conode.application.services import AccessControlService +from conode.domain.group import Group, GroupId @dataclass diff --git a/src/prodik/application/receive_node_info/__init__.py b/src/conode/application/receive_node_info/__init__.py similarity index 100% rename from src/prodik/application/receive_node_info/__init__.py rename to src/conode/application/receive_node_info/__init__.py diff --git a/src/prodik/application/receive_node_info/get_nodes_by_group.py b/src/conode/application/receive_node_info/get_nodes_by_group.py similarity index 82% rename from src/prodik/application/receive_node_info/get_nodes_by_group.py rename to src/conode/application/receive_node_info/get_nodes_by_group.py index 559e599..f500eb3 100644 --- a/src/prodik/application/receive_node_info/get_nodes_by_group.py +++ b/src/conode/application/receive_node_info/get_nodes_by_group.py @@ -1,14 +1,14 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.services import AccessControlService -from prodik.domain.group import GroupId -from prodik.domain.node import Node +from conode.application.services import AccessControlService +from conode.domain.group import GroupId +from conode.domain.node import Node @dataclass diff --git a/src/prodik/application/receive_user_info/__init__.py b/src/conode/application/receive_user_info/__init__.py similarity index 100% rename from src/prodik/application/receive_user_info/__init__.py rename to src/conode/application/receive_user_info/__init__.py diff --git a/src/prodik/application/receive_user_info/get_current_user.py b/src/conode/application/receive_user_info/get_current_user.py similarity index 71% rename from src/prodik/application/receive_user_info/get_current_user.py rename to src/conode/application/receive_user_info/get_current_user.py index eae5e6b..80f32e8 100644 --- a/src/prodik/application/receive_user_info/get_current_user.py +++ b/src/conode/application/receive_user_info/get_current_user.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from prodik.application.services import AccessControlService -from prodik.domain.user import User +from conode.application.services import AccessControlService +from conode.domain.user import User @dataclass diff --git a/src/prodik/application/receive_user_info/get_user_by_username.py b/src/conode/application/receive_user_info/get_user_by_username.py similarity index 71% rename from src/prodik/application/receive_user_info/get_user_by_username.py rename to src/conode/application/receive_user_info/get_user_by_username.py index f67eee5..48a867d 100644 --- a/src/prodik/application/receive_user_info/get_user_by_username.py +++ b/src/conode/application/receive_user_info/get_user_by_username.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from prodik.application.errors import UserNotFoundError -from prodik.application.interfaces.repositories import UserRepository -from prodik.application.services import AccessControlService -from prodik.domain.user import User, Username +from conode.application.errors import UserNotFoundError +from conode.application.interfaces.repositories import UserRepository +from conode.application.services import AccessControlService +from conode.domain.user import User, Username @dataclass diff --git a/src/prodik/application/register_company/__init__.py b/src/conode/application/register_company/__init__.py similarity index 100% rename from src/prodik/application/register_company/__init__.py rename to src/conode/application/register_company/__init__.py diff --git a/src/prodik/application/register_company/register_company.py b/src/conode/application/register_company/register_company.py similarity index 87% rename from src/prodik/application/register_company/register_company.py rename to src/conode/application/register_company/register_company.py index 4194e69..783d32b 100644 --- a/src/prodik/application/register_company/register_company.py +++ b/src/conode/application/register_company/register_company.py @@ -1,18 +1,18 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.errors import CompanyAlreadyExistsError -from prodik.application.interfaces.repositories import ( +from conode.application.errors import CompanyAlreadyExistsError +from conode.application.interfaces.repositories import ( CompanyRepository, RolePermissionsRepository, RoleRepository, UserGrantRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService, RoleManagmentService -from prodik.domain.company import Company, CompanyId, CompanyName -from prodik.domain.grant import UserGrant, UserGrantId -from prodik.domain.role import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService, RoleManagmentService +from conode.domain.company import Company, CompanyId, CompanyName +from conode.domain.grant import UserGrant, UserGrantId +from conode.domain.role import ( EntityType, PermissionType, ) diff --git a/src/prodik/application/services/__init__.py b/src/conode/application/services/__init__.py similarity index 100% rename from src/prodik/application/services/__init__.py rename to src/conode/application/services/__init__.py diff --git a/src/prodik/application/services/access_control.py b/src/conode/application/services/access_control.py similarity index 88% rename from src/prodik/application/services/access_control.py rename to src/conode/application/services/access_control.py index 37febd5..d2d31b4 100644 --- a/src/prodik/application/services/access_control.py +++ b/src/conode/application/services/access_control.py @@ -1,30 +1,30 @@ from dataclasses import dataclass from typing import Final +from uuid import uuid4 -from prodik.application.errors import ( +from conode.application.errors import ( NotEnoughRightsError, - SessionExpiredError, ) -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.identity_provider import IdentityProvider +from conode.application.interfaces.repositories import ( CompanyRepository, RolePermissionsRepository, RoleRepository, UserGrantRepository, UserRepository, ) -from prodik.application.interfaces.token_managers import UserMeta -from prodik.domain.company import Company -from prodik.domain.context import Context -from prodik.domain.group import Group -from prodik.domain.role import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.company import Company +from conode.domain.context import Context +from conode.domain.group import Group +from conode.domain.role import ( EntityType, PermissionType, Role, RolePermission, RolePermissionEntityId, ) -from prodik.domain.user import User +from conode.domain.user import Email, User, UserId type RolesPermissions = list[RolePermission] @@ -41,12 +41,9 @@ class AccessControlService: role_repository: RoleRepository company_repository: CompanyRepository identity_provider: IdentityProvider + transaction_manager: TransactionManager user_repository: UserRepository - def _ensure_revision_is_valid(self, meta: UserMeta, user: User) -> None: - if meta["revision"] != user.token_revision: - raise SessionExpiredError("Session has expired", None) - async def _get_all_permissions(self, user: User) -> list[RolePermission]: grants = await self.user_grant_repository.get_all_by_user_id(user.id) role_ids = [g.role_id for g in grants] @@ -56,9 +53,18 @@ async def _get_all_permissions(self, user: User) -> list[RolePermission]: async def get_authorized_user(self) -> User: meta = self.identity_provider.get_current_user_meta() - user = await self.user_repository.get_by_id(meta["user_id"]) - - self._ensure_revision_is_valid(meta, user) + user = await self.user_repository.get_by_email(Email(meta["email"])) + if user is None: + async with self.transaction_manager: + user = User.new( + user_id=UserId(uuid4()), + first_name=meta["first_name"], + last_name=meta["last_name"], + username=meta["username"], + email=meta["email"], + bio="Beautiful description about me!", + ) + await self.user_repository.create(user) return user diff --git a/src/prodik/application/services/offer_acceptance.py b/src/conode/application/services/offer_acceptance.py similarity index 89% rename from src/prodik/application/services/offer_acceptance.py rename to src/conode/application/services/offer_acceptance.py index 8c1386a..f4fb4ba 100644 --- a/src/prodik/application/services/offer_acceptance.py +++ b/src/conode/application/services/offer_acceptance.py @@ -1,10 +1,10 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.domain.company import Company -from prodik.domain.contract import Contract, ContractId -from prodik.domain.offer import Offer, OfferLink -from prodik.domain.role import RoleId +from conode.domain.company import Company +from conode.domain.contract import Contract, ContractId +from conode.domain.offer import Offer, OfferLink +from conode.domain.role import RoleId @dataclass(slots=True, frozen=True, kw_only=True) diff --git a/src/prodik/application/services/offer_sending.py b/src/conode/application/services/offer_sending.py similarity index 91% rename from src/prodik/application/services/offer_sending.py rename to src/conode/application/services/offer_sending.py index 5e5bfbf..660f37a 100644 --- a/src/prodik/application/services/offer_sending.py +++ b/src/conode/application/services/offer_sending.py @@ -2,10 +2,10 @@ from datetime import datetime from uuid import uuid4 -from prodik.domain.company import Company -from prodik.domain.context import Context, ContextId -from prodik.domain.group import Group, GroupId -from prodik.domain.offer import ( +from conode.domain.company import Company +from conode.domain.context import Context, ContextId +from conode.domain.group import Group, GroupId +from conode.domain.offer import ( Offer, OfferContext, OfferContextId, @@ -15,7 +15,7 @@ OfferLink, OfferLinkId, ) -from prodik.domain.role import PermissionType +from conode.domain.role import PermissionType @dataclass(slots=True, frozen=True, kw_only=True) diff --git a/src/prodik/application/services/role_managment.py b/src/conode/application/services/role_managment.py similarity index 94% rename from src/prodik/application/services/role_managment.py rename to src/conode/application/services/role_managment.py index 6755a9e..0ee694c 100644 --- a/src/prodik/application/services/role_managment.py +++ b/src/conode/application/services/role_managment.py @@ -1,8 +1,8 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.domain.company import Company -from prodik.domain.role import ( +from conode.domain.company import Company +from conode.domain.role import ( EntityType, PermissionType, Role, diff --git a/src/prodik/application/share_graph/__init__.py b/src/conode/application/share_graph/__init__.py similarity index 100% rename from src/prodik/application/share_graph/__init__.py rename to src/conode/application/share_graph/__init__.py diff --git a/src/prodik/application/share_graph/accept_offer.py b/src/conode/application/share_graph/accept_offer.py similarity index 94% rename from src/prodik/application/share_graph/accept_offer.py rename to src/conode/application/share_graph/accept_offer.py index 4252179..c1642fe 100644 --- a/src/prodik/application/share_graph/accept_offer.py +++ b/src/conode/application/share_graph/accept_offer.py @@ -2,11 +2,11 @@ from dataclasses import dataclass from typing import cast -from prodik.application.errors import ( +from conode.application.errors import ( CompanyNotFoundError, OfferLinkNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContractRepository, OfferContextRepository, @@ -16,16 +16,16 @@ RolePermissionsRepository, RoleRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import ( AccessControlService, OfferAcceptanceService, RoleManagmentService, ) -from prodik.domain.company import Company -from prodik.domain.contract import Contract -from prodik.domain.offer import Offer, OfferContext, OfferGroup, OfferId -from prodik.domain.role import EntityType, PermissionType, RolePermissionEntityId +from conode.domain.company import Company +from conode.domain.contract import Contract +from conode.domain.offer import Offer, OfferContext, OfferGroup, OfferId +from conode.domain.role import EntityType, PermissionType, RolePermissionEntityId @dataclass diff --git a/src/prodik/application/share_graph/decline_offer.py b/src/conode/application/share_graph/decline_offer.py similarity index 87% rename from src/prodik/application/share_graph/decline_offer.py rename to src/conode/application/share_graph/decline_offer.py index a52428b..a57ae2f 100644 --- a/src/prodik/application/share_graph/decline_offer.py +++ b/src/conode/application/share_graph/decline_offer.py @@ -1,16 +1,16 @@ from dataclasses import dataclass -from prodik.application.errors import ( +from conode.application.errors import ( OfferLinkNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, OfferLinkRepository, OfferRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.offer import OfferId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.offer import OfferId @dataclass diff --git a/src/prodik/application/share_graph/send_offer_to_company.py b/src/conode/application/share_graph/send_offer_to_company.py similarity index 90% rename from src/prodik/application/share_graph/send_offer_to_company.py rename to src/conode/application/share_graph/send_offer_to_company.py index e90762d..fd67456 100644 --- a/src/prodik/application/share_graph/send_offer_to_company.py +++ b/src/conode/application/share_graph/send_offer_to_company.py @@ -1,12 +1,12 @@ from dataclasses import dataclass from datetime import datetime -from prodik.application.errors import ( +from conode.application.errors import ( ContextNotFoundError, GroupNotFoundError, OfferNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, GroupRepository, @@ -15,13 +15,13 @@ OfferLinkRepository, OfferRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService, OfferSendingService -from prodik.domain.company import CompanyId -from prodik.domain.context import ContextId -from prodik.domain.group import GroupId -from prodik.domain.offer import Offer, OfferId -from prodik.domain.role import PermissionType +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService, OfferSendingService +from conode.domain.company import CompanyId +from conode.domain.context import ContextId +from conode.domain.group import GroupId +from conode.domain.offer import Offer, OfferId +from conode.domain.role import PermissionType @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/update_edge_weight/__init__.py b/src/conode/application/update_edge_weight/__init__.py similarity index 100% rename from src/prodik/application/update_edge_weight/__init__.py rename to src/conode/application/update_edge_weight/__init__.py diff --git a/src/prodik/application/update_edge_weight/decrement_edge_weight.py b/src/conode/application/update_edge_weight/decrement_edge_weight.py similarity index 80% rename from src/prodik/application/update_edge_weight/decrement_edge_weight.py rename to src/conode/application/update_edge_weight/decrement_edge_weight.py index 781e30c..eda3fd3 100644 --- a/src/prodik/application/update_edge_weight/decrement_edge_weight.py +++ b/src/conode/application/update_edge_weight/decrement_edge_weight.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.edge import EdgeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.edge import EdgeId @dataclass diff --git a/src/prodik/application/update_edge_weight/increment_edge_weight.py b/src/conode/application/update_edge_weight/increment_edge_weight.py similarity index 80% rename from src/prodik/application/update_edge_weight/increment_edge_weight.py rename to src/conode/application/update_edge_weight/increment_edge_weight.py index bf88b8e..39ee4f1 100644 --- a/src/prodik/application/update_edge_weight/increment_edge_weight.py +++ b/src/conode/application/update_edge_weight/increment_edge_weight.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.edge import EdgeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.edge import EdgeId @dataclass diff --git a/src/prodik/application/update_edge_weight/update_edge_weight.py b/src/conode/application/update_edge_weight/update_edge_weight.py similarity index 81% rename from src/prodik/application/update_edge_weight/update_edge_weight.py rename to src/conode/application/update_edge_weight/update_edge_weight.py index e51945b..d4bb09d 100644 --- a/src/prodik/application/update_edge_weight/update_edge_weight.py +++ b/src/conode/application/update_edge_weight/update_edge_weight.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.edge import EdgeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.edge import EdgeId @dataclass diff --git a/src/prodik/application/update_node/__init__.py b/src/conode/application/update_node/__init__.py similarity index 100% rename from src/prodik/application/update_node/__init__.py rename to src/conode/application/update_node/__init__.py diff --git a/src/prodik/application/update_node/update_node.py b/src/conode/application/update_node/update_node.py similarity index 86% rename from src/prodik/application/update_node/update_node.py rename to src/conode/application/update_node/update_node.py index 1ec73e3..23ab72f 100644 --- a/src/prodik/application/update_node/update_node.py +++ b/src/conode/application/update_node/update_node.py @@ -1,14 +1,14 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, GroupRepository, NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.node import NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.node import NodeId @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/src/prodik/application/verify_company/__init__.py b/src/conode/application/verify_company/__init__.py similarity index 100% rename from src/prodik/application/verify_company/__init__.py rename to src/conode/application/verify_company/__init__.py diff --git a/src/prodik/application/verify_company/verify_company.py b/src/conode/application/verify_company/verify_company.py similarity index 79% rename from src/prodik/application/verify_company/verify_company.py rename to src/conode/application/verify_company/verify_company.py index 88f4384..601bf9a 100644 --- a/src/prodik/application/verify_company/verify_company.py +++ b/src/conode/application/verify_company/verify_company.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from prodik.application.errors import ( +from conode.application.errors import ( CompanyNotFoundError, NotEnoughRightsError, ) -from prodik.application.interfaces.repositories import CompanyRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService -from prodik.domain.company import CompanyId +from conode.application.interfaces.repositories import CompanyRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import AccessControlService +from conode.domain.company import CompanyId @dataclass diff --git a/src/prodik/bootstrap/__init__.py b/src/conode/bootstrap/__init__.py similarity index 100% rename from src/prodik/bootstrap/__init__.py rename to src/conode/bootstrap/__init__.py diff --git a/src/prodik/bootstrap/api/__init__.py b/src/conode/bootstrap/api/__init__.py similarity index 100% rename from src/prodik/bootstrap/api/__init__.py rename to src/conode/bootstrap/api/__init__.py diff --git a/src/prodik/bootstrap/api/run.py b/src/conode/bootstrap/api/run.py similarity index 87% rename from src/prodik/bootstrap/api/run.py rename to src/conode/bootstrap/api/run.py index 9113187..10eeead 100644 --- a/src/prodik/bootstrap/api/run.py +++ b/src/conode/bootstrap/api/run.py @@ -6,10 +6,10 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from prodik.bootstrap.di import get_async_container -from prodik.bootstrap.logs import configure_structlog -from prodik.infrastructure.config import Config, load_config -from prodik.presentation.common import ( +from conode.bootstrap.di import get_async_container +from conode.bootstrap.logs import configure_structlog +from conode.infrastructure.config import Config, load_config +from conode.presentation.common import ( include_exception_handlers, include_handlers, include_middlewares, diff --git a/src/prodik/bootstrap/cli.py b/src/conode/bootstrap/cli.py similarity index 88% rename from src/prodik/bootstrap/cli.py rename to src/conode/bootstrap/cli.py index 674dacf..e04a44b 100644 --- a/src/prodik/bootstrap/cli.py +++ b/src/conode/bootstrap/cli.py @@ -6,13 +6,13 @@ import alembic.config -import prodik.infrastructure.persistence -from prodik.bootstrap.api import run_http -from prodik.infrastructure.persistence import start_mapper +import conode.infrastructure.persistence +from conode.bootstrap.api import run_http +from conode.infrastructure.persistence import start_mapper def get_alembic_config_path() -> Iterator[Path]: - source = files(prodik.infrastructure.persistence).joinpath("alembic.ini") + source = files(conode.infrastructure.persistence).joinpath("alembic.ini") with as_file(source) as path: yield path @@ -69,9 +69,6 @@ def main() -> None: parser = configure_argument_parser() args = parser.parse_args() - if args.module == "api": - run_migrations() - start_mapper() args.func(args) diff --git a/src/prodik/bootstrap/di/__init__.py b/src/conode/bootstrap/di/__init__.py similarity index 100% rename from src/prodik/bootstrap/di/__init__.py rename to src/conode/bootstrap/di/__init__.py diff --git a/src/prodik/bootstrap/di/container.py b/src/conode/bootstrap/di/container.py similarity index 75% rename from src/prodik/bootstrap/di/container.py rename to src/conode/bootstrap/di/container.py index c75a4a4..f0c4ff2 100644 --- a/src/prodik/bootstrap/di/container.py +++ b/src/conode/bootstrap/di/container.py @@ -1,15 +1,14 @@ from dishka import AsyncContainer, make_async_container from dishka.integrations.fastapi import FastapiProvider -from prodik.bootstrap.di.providers import ( +from conode.bootstrap.di.providers import ( ApplicationProvider, - CacheConnectionProvider, ConnectionProvider, InfrastructureProvider, + SecretsProvider, ) -from prodik.infrastructure.config import ( +from conode.infrastructure.config import ( APIConfig, - CacheConfig, Config, DatabaseConfig, SecretsConfig, @@ -21,12 +20,11 @@ def get_async_container(config: Config) -> AsyncContainer: FastapiProvider(), InfrastructureProvider(), ApplicationProvider(), - CacheConnectionProvider(), + SecretsProvider(), ConnectionProvider(), context={ APIConfig: config.api, DatabaseConfig: config.database, SecretsConfig: config.secrets, - CacheConfig: config.cache, }, ) diff --git a/src/prodik/bootstrap/di/providers/__init__.py b/src/conode/bootstrap/di/providers/__init__.py similarity index 76% rename from src/prodik/bootstrap/di/providers/__init__.py rename to src/conode/bootstrap/di/providers/__init__.py index a91ffb0..6c35744 100644 --- a/src/prodik/bootstrap/di/providers/__init__.py +++ b/src/conode/bootstrap/di/providers/__init__.py @@ -1,11 +1,11 @@ from .application import ApplicationProvider -from .cache import CacheConnectionProvider from .connection import ConnectionProvider from .infrastructure import InfrastructureProvider +from .secrets import SecretsProvider __all__ = ( "ApplicationProvider", - "CacheConnectionProvider", "ConnectionProvider", "InfrastructureProvider", + "SecretsProvider", ) diff --git a/src/prodik/bootstrap/di/providers/application.py b/src/conode/bootstrap/di/providers/application.py similarity index 60% rename from src/prodik/bootstrap/di/providers/application.py rename to src/conode/bootstrap/di/providers/application.py index 7f2ac56..36f4816 100644 --- a/src/prodik/bootstrap/di/providers/application.py +++ b/src/conode/bootstrap/di/providers/application.py @@ -1,88 +1,78 @@ from dishka import Provider, Scope, provide_all -from prodik.application.attach_node_to_group import ( +from conode.application.attach_node_to_group import ( AttachNodeInteractor, ) -from prodik.application.authorization import ( - LoginInteractor, - RefreshTokenInteractor, - RegisterInteractor, -) -from prodik.application.detach_node_from_group import ( +from conode.application.detach_node_from_group import ( DetachNodeInteractor, ) -from prodik.application.manage_context import ( +from conode.application.manage_context import ( CreateContextInteractor, DeleteContextInteractor, ) -from prodik.application.manage_credentials import UpdateCurrentUserPasswordInteractor -from prodik.application.manage_edge import CreateEdgeInteractor, DeleteEdgeInteractor -from prodik.application.manage_group import CreateGroupInteractor, DeleteGroupInteractor -from prodik.application.manage_node import CreateNodeInteractor, DeleteNodeInteractor -from prodik.application.manage_profile import UpdateCurrentUserProfileInteractor -from prodik.application.manage_role import ( +from conode.application.manage_edge import CreateEdgeInteractor, DeleteEdgeInteractor +from conode.application.manage_group import CreateGroupInteractor, DeleteGroupInteractor +from conode.application.manage_node import CreateNodeInteractor, DeleteNodeInteractor +from conode.application.manage_profile import UpdateCurrentUserProfileInteractor +from conode.application.manage_role import ( CreateRoleInteractor, DeleteRoleInteractor, UpdateRoleInteractor, ) -from prodik.application.manage_user_rights import ( +from conode.application.manage_user_rights import ( GiveRoleToUserInteractor, RevokeRoleFromUserInteractor, ) -from prodik.application.receive_context_info import ( +from conode.application.receive_context_info import ( GetContextByIdInteractor, ) -from prodik.application.receive_graph_statistics import ( +from conode.application.receive_graph_statistics import ( FindShortestPathInteractor, GetNodeNeighboursInteractor, ) -from prodik.application.receive_group_info import GetGroupByIdInteractor -from prodik.application.receive_node_info import GetNodesByGroupInteractor -from prodik.application.receive_user_info import ( +from conode.application.receive_group_info import GetGroupByIdInteractor +from conode.application.receive_node_info import GetNodesByGroupInteractor +from conode.application.receive_user_info import ( GetCurrentUserInteractor, GetUserByUsernameInteractor, ) -from prodik.application.register_company import ( +from conode.application.register_company import ( RegisterCompanyInteractor, ) -from prodik.application.services import ( +from conode.application.services import ( AccessControlService, OfferAcceptanceService, OfferSendingService, RoleManagmentService, ) -from prodik.application.share_graph import ( +from conode.application.share_graph import ( AcceptOfferInteractor, DeclineOfferInteractor, SendOfferToCompanyInteractor, ) -from prodik.application.update_edge_weight import ( +from conode.application.update_edge_weight import ( DecrementEdgeWeightInteractor, IncrementEdgeWeightInteractor, UpdateEdgeWeightInteractor, ) -from prodik.application.update_node import ( +from conode.application.update_node import ( UpdateNodeInteractor, ) -from prodik.application.verify_company import VerifyCompanyInteractor +from conode.application.verify_company import VerifyCompanyInteractor class ApplicationProvider(Provider): provides = provide_all( UpdateNodeInteractor, - RegisterInteractor, - LoginInteractor, AccessControlService, OfferAcceptanceService, OfferSendingService, DeleteEdgeInteractor, - RefreshTokenInteractor, UpdateEdgeWeightInteractor, IncrementEdgeWeightInteractor, GetGroupByIdInteractor, GetContextByIdInteractor, UpdateCurrentUserProfileInteractor, - UpdateCurrentUserPasswordInteractor, SendOfferToCompanyInteractor, DecrementEdgeWeightInteractor, GetUserByUsernameInteractor, diff --git a/src/prodik/bootstrap/di/providers/connection.py b/src/conode/bootstrap/di/providers/connection.py similarity index 73% rename from src/prodik/bootstrap/di/providers/connection.py rename to src/conode/bootstrap/di/providers/connection.py index 4d18a4f..094a790 100644 --- a/src/prodik/bootstrap/di/providers/connection.py +++ b/src/conode/bootstrap/di/providers/connection.py @@ -1,7 +1,6 @@ from collections.abc import AsyncIterator from dishka import Provider, Scope, WithParents, provide, provide_all -from redis.asyncio import Redis from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -9,8 +8,8 @@ create_async_engine, ) -from prodik.infrastructure.config import CacheConfig, DatabaseConfig -from prodik.infrastructure.transaction_manager import TransactionManagerImpl +from conode.infrastructure.config import DatabaseConfig +from conode.infrastructure.transaction_manager import TransactionManagerImpl class ConnectionProvider(Provider): @@ -46,16 +45,3 @@ async def provide_async_session( ) -> AsyncIterator[AsyncSession]: async with session_factory() as session: yield session - - @provide(scope=Scope.REQUEST) - async def provide_redis_connection( - self, - config: CacheConfig, - ) -> AsyncIterator[Redis]: - client = Redis( - host=config.host, - port=config.port, - decode_responses=True, - ) - yield client - await client.aclose() diff --git a/src/prodik/bootstrap/di/providers/infrastructure.py b/src/conode/bootstrap/di/providers/infrastructure.py similarity index 71% rename from src/prodik/bootstrap/di/providers/infrastructure.py rename to src/conode/bootstrap/di/providers/infrastructure.py index 58d4948..4ad41a3 100644 --- a/src/prodik/bootstrap/di/providers/infrastructure.py +++ b/src/conode/bootstrap/di/providers/infrastructure.py @@ -1,14 +1,13 @@ from dishka import Provider, Scope, WithParents, provide_all -from prodik.infrastructure.identity_provider import IdentityProviderImpl -from prodik.infrastructure.password_hasher import PasswordHasherImpl -from prodik.infrastructure.repositories import ( +from conode.infrastructure.identity_provider import IdentityProviderImpl +from conode.infrastructure.password_hasher import PasswordHasherImpl +from conode.infrastructure.repositories import ( CompanyRepositoryImpl, ContextRepositoryImpl, ContractRepositoryImpl, EdgeRepositoryImpl, GroupRepositoryImpl, - LocalAuthorizationRepositoryImpl, NodeAssociationRepositoryImpl, NodeRepositoryImpl, OfferContextRepositoryImpl, @@ -17,14 +16,9 @@ OfferRepositoryImpl, RolePermissionsRepositoryImpl, RoleRepositoryImpl, - SessionRepositoryImpl, UserGrantRepositoryImpl, UserRepositoryImpl, ) -from prodik.infrastructure.token_managers import ( - AccessTokenManagerImpl, - RefreshTokenManagerImpl, -) class InfrastructureProvider(Provider): @@ -32,11 +26,7 @@ class InfrastructureProvider(Provider): WithParents[IdentityProviderImpl], WithParents[GroupRepositoryImpl], WithParents[PasswordHasherImpl], - WithParents[AccessTokenManagerImpl], - WithParents[RefreshTokenManagerImpl], WithParents[NodeAssociationRepositoryImpl], - WithParents[LocalAuthorizationRepositoryImpl], - WithParents[SessionRepositoryImpl], WithParents[CompanyRepositoryImpl], WithParents[UserRepositoryImpl], WithParents[NodeRepositoryImpl], diff --git a/src/conode/bootstrap/di/providers/secrets.py b/src/conode/bootstrap/di/providers/secrets.py new file mode 100644 index 0000000..76a11e6 --- /dev/null +++ b/src/conode/bootstrap/di/providers/secrets.py @@ -0,0 +1,10 @@ +from dishka import Provider, Scope, WithParents, provide_all + +from conode.infrastructure.token_manager import TokenManagerImpl + + +class SecretsProvider(Provider): + provides = provide_all( + WithParents[TokenManagerImpl], + scope=Scope.REQUEST, + ) diff --git a/src/prodik/bootstrap/logs.py b/src/conode/bootstrap/logs.py similarity index 100% rename from src/prodik/bootstrap/logs.py rename to src/conode/bootstrap/logs.py diff --git a/src/prodik/domain/__init__.py b/src/conode/domain/__init__.py similarity index 100% rename from src/prodik/domain/__init__.py rename to src/conode/domain/__init__.py diff --git a/src/prodik/domain/company/__init__.py b/src/conode/domain/company/__init__.py similarity index 100% rename from src/prodik/domain/company/__init__.py rename to src/conode/domain/company/__init__.py diff --git a/src/prodik/domain/company/errors.py b/src/conode/domain/company/errors.py similarity index 79% rename from src/prodik/domain/company/errors.py rename to src/conode/domain/company/errors.py index 9b24dd3..17f9b5b 100644 --- a/src/prodik/domain/company/errors.py +++ b/src/conode/domain/company/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class CompanyDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/company/model.py b/src/conode/domain/company/model.py similarity index 94% rename from src/prodik/domain/company/model.py rename to src/conode/domain/company/model.py index c8b93c0..57306fb 100644 --- a/src/prodik/domain/company/model.py +++ b/src/conode/domain/company/model.py @@ -3,12 +3,12 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company.errors import ( +from conode.domain.company.errors import ( InvalidCompanyDescriptionFormatError, InvalidCompanyNameFormatError, ) -from prodik.domain.shared import Entity, ValueObject -from prodik.domain.user import User, UserId +from conode.domain.shared import Entity, ValueObject +from conode.domain.user import User, UserId CompanyId = NewType("CompanyId", UUID) diff --git a/src/prodik/domain/context/__init__.py b/src/conode/domain/context/__init__.py similarity index 100% rename from src/prodik/domain/context/__init__.py rename to src/conode/domain/context/__init__.py diff --git a/src/prodik/domain/context/errors.py b/src/conode/domain/context/errors.py similarity index 79% rename from src/prodik/domain/context/errors.py rename to src/conode/domain/context/errors.py index 2661e1d..669be44 100644 --- a/src/prodik/domain/context/errors.py +++ b/src/conode/domain/context/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class ContextDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/context/model.py b/src/conode/domain/context/model.py similarity index 92% rename from src/prodik/domain/context/model.py rename to src/conode/domain/context/model.py index a770bda..b37e70c 100644 --- a/src/prodik/domain/context/model.py +++ b/src/conode/domain/context/model.py @@ -3,12 +3,12 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.context.errors import ( +from conode.domain.company import Company, CompanyId +from conode.domain.context.errors import ( InvalidContextDescriptionFormatError, InvalidContextNameFormatError, ) -from prodik.domain.shared import Entity, ValueObject +from conode.domain.shared import Entity, ValueObject ContextId = NewType("ContextId", UUID) diff --git a/src/prodik/domain/contract/__init__.py b/src/conode/domain/contract/__init__.py similarity index 100% rename from src/prodik/domain/contract/__init__.py rename to src/conode/domain/contract/__init__.py diff --git a/src/prodik/domain/contract/errors.py b/src/conode/domain/contract/errors.py similarity index 71% rename from src/prodik/domain/contract/errors.py rename to src/conode/domain/contract/errors.py index 6ceb02e..2a47081 100644 --- a/src/prodik/domain/contract/errors.py +++ b/src/conode/domain/contract/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class ContractDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/contract/model.py b/src/conode/domain/contract/model.py similarity index 89% rename from src/prodik/domain/contract/model.py rename to src/conode/domain/contract/model.py index 6a59b28..0ec5721 100644 --- a/src/prodik/domain/contract/model.py +++ b/src/conode/domain/contract/model.py @@ -4,11 +4,11 @@ from typing import NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.contract.errors import InvalidCompanyOffersFormatError -from prodik.domain.offer import Offer, OfferId -from prodik.domain.role import RoleId -from prodik.domain.shared import Entity +from conode.domain.company import Company, CompanyId +from conode.domain.contract.errors import InvalidCompanyOffersFormatError +from conode.domain.offer import Offer, OfferId +from conode.domain.role import RoleId +from conode.domain.shared import Entity ContractId = NewType("ContractId", UUID) diff --git a/src/prodik/domain/edge/__init__.py b/src/conode/domain/edge/__init__.py similarity index 100% rename from src/prodik/domain/edge/__init__.py rename to src/conode/domain/edge/__init__.py diff --git a/src/prodik/domain/edge/errors.py b/src/conode/domain/edge/errors.py similarity index 78% rename from src/prodik/domain/edge/errors.py rename to src/conode/domain/edge/errors.py index 8212a1a..60bde6d 100644 --- a/src/prodik/domain/edge/errors.py +++ b/src/conode/domain/edge/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class EdgeDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/edge/model.py b/src/conode/domain/edge/model.py similarity index 89% rename from src/prodik/domain/edge/model.py rename to src/conode/domain/edge/model.py index d4af854..23abb90 100644 --- a/src/prodik/domain/edge/model.py +++ b/src/conode/domain/edge/model.py @@ -3,14 +3,14 @@ from typing import NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.context import Context, ContextId -from prodik.domain.edge.errors import ( +from conode.domain.company import Company, CompanyId +from conode.domain.context import Context, ContextId +from conode.domain.edge.errors import ( EdgeCannotConnectTwoSameNodesError, EdgeWeightCannotBeNegativeError, ) -from prodik.domain.node import Node, NodeId -from prodik.domain.shared import Entity +from conode.domain.node import Node, NodeId +from conode.domain.shared import Entity EdgeId = NewType("EdgeId", UUID) diff --git a/src/prodik/domain/grant/__init__.py b/src/conode/domain/grant/__init__.py similarity index 100% rename from src/prodik/domain/grant/__init__.py rename to src/conode/domain/grant/__init__.py diff --git a/src/prodik/domain/grant/company.py b/src/conode/domain/grant/company.py similarity index 82% rename from src/prodik/domain/grant/company.py rename to src/conode/domain/grant/company.py index cd90359..ba4189e 100644 --- a/src/prodik/domain/grant/company.py +++ b/src/conode/domain/grant/company.py @@ -3,9 +3,9 @@ from typing import NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.role import Role, RoleId -from prodik.domain.shared import Entity +from conode.domain.company import Company, CompanyId +from conode.domain.role import Role, RoleId +from conode.domain.shared import Entity CompanyGrantId = NewType("CompanyGrantId", UUID) diff --git a/src/prodik/domain/grant/user.py b/src/conode/domain/grant/user.py similarity index 81% rename from src/prodik/domain/grant/user.py rename to src/conode/domain/grant/user.py index 6aad633..a2bc606 100644 --- a/src/prodik/domain/grant/user.py +++ b/src/conode/domain/grant/user.py @@ -3,9 +3,9 @@ from typing import NewType, Self from uuid import UUID -from prodik.domain.role import Role, RoleId -from prodik.domain.shared import Entity -from prodik.domain.user import User, UserId +from conode.domain.role import Role, RoleId +from conode.domain.shared import Entity +from conode.domain.user import User, UserId UserGrantId = NewType("UserGrantId", UUID) diff --git a/src/prodik/domain/group/__init__.py b/src/conode/domain/group/__init__.py similarity index 100% rename from src/prodik/domain/group/__init__.py rename to src/conode/domain/group/__init__.py diff --git a/src/prodik/domain/group/errors.py b/src/conode/domain/group/errors.py similarity index 83% rename from src/prodik/domain/group/errors.py rename to src/conode/domain/group/errors.py index 667be70..6c332da 100644 --- a/src/prodik/domain/group/errors.py +++ b/src/conode/domain/group/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class GroupDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/group/model.py b/src/conode/domain/group/model.py similarity index 94% rename from src/prodik/domain/group/model.py rename to src/conode/domain/group/model.py index a3d5ca2..b5adc30 100644 --- a/src/prodik/domain/group/model.py +++ b/src/conode/domain/group/model.py @@ -3,13 +3,13 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.group.errors import ( +from conode.domain.company import Company, CompanyId +from conode.domain.group.errors import ( GroupCannotInheritedFromItselfError, InvalidGroupDescriptionFormatError, InvalidGroupNameFormatError, ) -from prodik.domain.shared import Entity, ValueObject +from conode.domain.shared import Entity, ValueObject GroupId = NewType("GroupId", UUID) diff --git a/src/prodik/domain/node/__init__.py b/src/conode/domain/node/__init__.py similarity index 100% rename from src/prodik/domain/node/__init__.py rename to src/conode/domain/node/__init__.py diff --git a/src/prodik/domain/node/errors.py b/src/conode/domain/node/errors.py similarity index 78% rename from src/prodik/domain/node/errors.py rename to src/conode/domain/node/errors.py index 37cae77..733445d 100644 --- a/src/prodik/domain/node/errors.py +++ b/src/conode/domain/node/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class NodeDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/node/model.py b/src/conode/domain/node/model.py similarity index 93% rename from src/prodik/domain/node/model.py rename to src/conode/domain/node/model.py index 92e0fcd..28d9eea 100644 --- a/src/prodik/domain/node/model.py +++ b/src/conode/domain/node/model.py @@ -3,13 +3,13 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.group import Group, GroupId -from prodik.domain.node.errors import ( +from conode.domain.company import Company, CompanyId +from conode.domain.group import Group, GroupId +from conode.domain.node.errors import ( InvalidNodeDescriptionFormatError, InvalidNodeNameFormatError, ) -from prodik.domain.shared import Entity, ValueObject +from conode.domain.shared import Entity, ValueObject NodeId = NewType("NodeId", UUID) NodeAssociationId = NewType("NodeAssociationId", UUID) diff --git a/src/prodik/domain/offer/__init__.py b/src/conode/domain/offer/__init__.py similarity index 100% rename from src/prodik/domain/offer/__init__.py rename to src/conode/domain/offer/__init__.py diff --git a/src/prodik/domain/offer/errors.py b/src/conode/domain/offer/errors.py similarity index 89% rename from src/prodik/domain/offer/errors.py rename to src/conode/domain/offer/errors.py index 55689c8..1e7d6c8 100644 --- a/src/prodik/domain/offer/errors.py +++ b/src/conode/domain/offer/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class OfferDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/offer/model.py b/src/conode/domain/offer/model.py similarity index 95% rename from src/prodik/domain/offer/model.py rename to src/conode/domain/offer/model.py index 3142689..2c4701c 100644 --- a/src/prodik/domain/offer/model.py +++ b/src/conode/domain/offer/model.py @@ -4,10 +4,10 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.context import Context, ContextId -from prodik.domain.group import Group, GroupId -from prodik.domain.offer.errors import ( +from conode.domain.company import Company, CompanyId +from conode.domain.context import Context, ContextId +from conode.domain.group import Group, GroupId +from conode.domain.offer.errors import ( InvalidOfferDescriptionFormatError, InvalidOfferTitleFormatError, OfferCannotAnswerToItselfError, @@ -15,8 +15,8 @@ OfferCannotExpireAtPastError, OfferIsNotCounterOfferError, ) -from prodik.domain.role import PermissionType -from prodik.domain.shared import Entity, ValueObject +from conode.domain.role import PermissionType +from conode.domain.shared import Entity, ValueObject OfferId = NewType("OfferId", UUID) OfferLinkId = NewType("OfferLinkId", UUID) diff --git a/src/prodik/domain/role/__init__.py b/src/conode/domain/role/__init__.py similarity index 100% rename from src/prodik/domain/role/__init__.py rename to src/conode/domain/role/__init__.py diff --git a/src/prodik/domain/role/errors.py b/src/conode/domain/role/errors.py similarity index 69% rename from src/prodik/domain/role/errors.py rename to src/conode/domain/role/errors.py index 5bb2879..398935e 100644 --- a/src/prodik/domain/role/errors.py +++ b/src/conode/domain/role/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class RoleDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/role/model.py b/src/conode/domain/role/model.py similarity index 91% rename from src/prodik/domain/role/model.py rename to src/conode/domain/role/model.py index eae6147..7305c3d 100644 --- a/src/prodik/domain/role/model.py +++ b/src/conode/domain/role/model.py @@ -4,11 +4,11 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.company import Company, CompanyId -from prodik.domain.context import ContextId -from prodik.domain.group import GroupId -from prodik.domain.role.errors import InvalidRoleNameFormatError -from prodik.domain.shared import Entity, ValueObject +from conode.domain.company import Company, CompanyId +from conode.domain.context import ContextId +from conode.domain.group import GroupId +from conode.domain.role.errors import InvalidRoleNameFormatError +from conode.domain.shared import Entity, ValueObject RoleId = NewType("RoleId", UUID) RolePermissionId = NewType("RolePermissionId", UUID) diff --git a/src/prodik/domain/shared/__init__.py b/src/conode/domain/shared/__init__.py similarity index 100% rename from src/prodik/domain/shared/__init__.py rename to src/conode/domain/shared/__init__.py diff --git a/src/prodik/domain/shared/entity.py b/src/conode/domain/shared/entity.py similarity index 100% rename from src/prodik/domain/shared/entity.py rename to src/conode/domain/shared/entity.py diff --git a/src/prodik/domain/shared/value_object.py b/src/conode/domain/shared/value_object.py similarity index 100% rename from src/prodik/domain/shared/value_object.py rename to src/conode/domain/shared/value_object.py diff --git a/src/prodik/domain/user/__init__.py b/src/conode/domain/user/__init__.py similarity index 100% rename from src/prodik/domain/user/__init__.py rename to src/conode/domain/user/__init__.py diff --git a/src/prodik/domain/user/errors.py b/src/conode/domain/user/errors.py similarity index 86% rename from src/prodik/domain/user/errors.py rename to src/conode/domain/user/errors.py index 82c6786..3430605 100644 --- a/src/prodik/domain/user/errors.py +++ b/src/conode/domain/user/errors.py @@ -1,4 +1,4 @@ -from prodik.application.errors import ApplicationError +from conode.application.errors import ApplicationError class UserDomainValidationError(ApplicationError): ... diff --git a/src/prodik/domain/user/model.py b/src/conode/domain/user/model.py similarity index 93% rename from src/prodik/domain/user/model.py rename to src/conode/domain/user/model.py index d96d544..226ea83 100644 --- a/src/prodik/domain/user/model.py +++ b/src/conode/domain/user/model.py @@ -5,8 +5,8 @@ from typing import Final, NewType, Self from uuid import UUID -from prodik.domain.shared import Entity, ValueObject -from prodik.domain.user.errors import ( +from conode.domain.shared import Entity, ValueObject +from conode.domain.user.errors import ( InvalidUserEmailFormatError, InvalidUserFirstNameFormatError, InvalidUserLastNameFormatError, @@ -117,7 +117,6 @@ class User(Entity[UserId]): username: Username email: Email bio: Bio - token_revision: int @classmethod def new( @@ -139,7 +138,6 @@ def new( system_role=UserSystemRole.USER, created_at=now, updated_at=now, - token_revision=1, bio=Bio(bio), ) @@ -150,16 +148,11 @@ def update_profile( last_name: str, username: str, bio: str, - email: str, ) -> None: self.first_name = FirstName(first_name) self.last_name = LastName(last_name) self.username = Username(username) - self.email = Email(email) self.bio = Bio(bio) - def increment_revision(self) -> None: - self.token_revision += 1 - def is_admin(self) -> bool: return self.system_role == UserSystemRole.ADMIN diff --git a/src/prodik/infrastructure/__init__.py b/src/conode/infrastructure/__init__.py similarity index 100% rename from src/prodik/infrastructure/__init__.py rename to src/conode/infrastructure/__init__.py diff --git a/src/prodik/infrastructure/config.py b/src/conode/infrastructure/config.py similarity index 82% rename from src/prodik/infrastructure/config.py rename to src/conode/infrastructure/config.py index 6d634a3..48b08d2 100644 --- a/src/prodik/infrastructure/config.py +++ b/src/conode/infrastructure/config.py @@ -16,13 +16,9 @@ class APIConfig: class SecretsConfig: secret: str expires_in: int - - -@dataclass(slots=True, frozen=True, kw_only=True) -class CacheConfig: - host: str - port: int - ttl: int + jwks_url: str + audience: str + issuer: str @dataclass(slots=True, frozen=True, kw_only=True) @@ -39,7 +35,6 @@ class Config: api: APIConfig database: DatabaseConfig secrets: SecretsConfig - cache: CacheConfig def load_config(path: str = "config.toml") -> Config: @@ -62,10 +57,8 @@ def load_config(path: str = "config.toml") -> Config: secrets=SecretsConfig( secret=config["secrets"]["secret"], expires_in=config["secrets"]["expires_in"], - ), - cache=CacheConfig( - host=config["cache"]["host"], - port=config["cache"]["port"], - ttl=config["cache"]["ttl"], + jwks_url=config["secrets"]["jwks_url"], + audience=config["secrets"]["audience"], + issuer=config["secrets"]["issuer"], ), ) diff --git a/src/conode/infrastructure/identity_provider.py b/src/conode/infrastructure/identity_provider.py new file mode 100644 index 0000000..1386f8f --- /dev/null +++ b/src/conode/infrastructure/identity_provider.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass + +import structlog +from fastapi import Request +from jwt import PyJWTError + +from conode.application.errors import FailedToReadClientError, InvalidTokenError +from conode.application.interfaces.identity_provider import IdentityProvider +from conode.application.interfaces.token_manager import TokenManager, UserMeta + +logger = structlog.get_logger() + + +@dataclass +class IdentityProviderImpl(IdentityProvider): + request: Request + access_token_manager: TokenManager + + def get_current_ip(self) -> str: + if self.request.client is None: + raise FailedToReadClientError("Failed to read client error", None) + + return self.request.client.host + + def get_current_user_meta(self) -> UserMeta: + token = self.request.headers.get("X-Forwarded-Access-Token") + if token is None: + raise InvalidTokenError("Authorization token not found", None) + + try: + content = self.access_token_manager.decode(token) + except PyJWTError as e: + raise InvalidTokenError( + "Invalid authorization token", + [{"key": "Authorization", "value": token}], + ) from e + + return content diff --git a/src/prodik/infrastructure/password_hasher.py b/src/conode/infrastructure/password_hasher.py similarity index 89% rename from src/prodik/infrastructure/password_hasher.py rename to src/conode/infrastructure/password_hasher.py index 22a55cd..753a802 100644 --- a/src/prodik/infrastructure/password_hasher.py +++ b/src/conode/infrastructure/password_hasher.py @@ -3,7 +3,7 @@ from argon2 import PasswordHasher as Argon2Hasher from argon2.exceptions import Argon2Error, InvalidHashError -from prodik.application.interfaces.password_hasher import PasswordHasher +from conode.application.interfaces.password_hasher import PasswordHasher @dataclass diff --git a/src/prodik/infrastructure/persistence/__init__.py b/src/conode/infrastructure/persistence/__init__.py similarity index 100% rename from src/prodik/infrastructure/persistence/__init__.py rename to src/conode/infrastructure/persistence/__init__.py diff --git a/src/prodik/infrastructure/persistence/alembic.ini b/src/conode/infrastructure/persistence/alembic.ini similarity index 100% rename from src/prodik/infrastructure/persistence/alembic.ini rename to src/conode/infrastructure/persistence/alembic.ini diff --git a/src/prodik/infrastructure/persistence/migrations/README b/src/conode/infrastructure/persistence/migrations/README similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/README rename to src/conode/infrastructure/persistence/migrations/README diff --git a/src/prodik/infrastructure/persistence/migrations/__init__.py b/src/conode/infrastructure/persistence/migrations/__init__.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/__init__.py rename to src/conode/infrastructure/persistence/migrations/__init__.py diff --git a/src/prodik/infrastructure/persistence/migrations/env.py b/src/conode/infrastructure/persistence/migrations/env.py similarity index 96% rename from src/prodik/infrastructure/persistence/migrations/env.py rename to src/conode/infrastructure/persistence/migrations/env.py index f78aae0..e29df13 100644 --- a/src/prodik/infrastructure/persistence/migrations/env.py +++ b/src/conode/infrastructure/persistence/migrations/env.py @@ -5,8 +5,8 @@ from sqlalchemy import Connection, pool, URL from sqlalchemy.ext.asyncio import async_engine_from_config -from prodik.infrastructure.config import load_config -from prodik.infrastructure.persistence.registry import metadata +from conode.infrastructure.config import load_config +from conode.infrastructure.persistence.registry import metadata # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/prodik/infrastructure/persistence/migrations/script.py.mako b/src/conode/infrastructure/persistence/migrations/script.py.mako similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/script.py.mako rename to src/conode/infrastructure/persistence/migrations/script.py.mako diff --git a/src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py b/src/conode/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py rename to src/conode/infrastructure/persistence/migrations/versions/0333150881b1_parent_group_id.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/080a7bac6dfb_custom_type.py b/src/conode/infrastructure/persistence/migrations/versions/080a7bac6dfb_custom_type.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/080a7bac6dfb_custom_type.py rename to src/conode/infrastructure/persistence/migrations/versions/080a7bac6dfb_custom_type.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/1786a4b3cea6_remove_company_id.py b/src/conode/infrastructure/persistence/migrations/versions/1786a4b3cea6_remove_company_id.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/1786a4b3cea6_remove_company_id.py rename to src/conode/infrastructure/persistence/migrations/versions/1786a4b3cea6_remove_company_id.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/2e3b8d977df1_offers_and_contracts.py b/src/conode/infrastructure/persistence/migrations/versions/2e3b8d977df1_offers_and_contracts.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/2e3b8d977df1_offers_and_contracts.py rename to src/conode/infrastructure/persistence/migrations/versions/2e3b8d977df1_offers_and_contracts.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/3057b93fb0cb_bio.py b/src/conode/infrastructure/persistence/migrations/versions/3057b93fb0cb_bio.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/3057b93fb0cb_bio.py rename to src/conode/infrastructure/persistence/migrations/versions/3057b93fb0cb_bio.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/33f30ca8ca69_company.py b/src/conode/infrastructure/persistence/migrations/versions/33f30ca8ca69_company.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/33f30ca8ca69_company.py rename to src/conode/infrastructure/persistence/migrations/versions/33f30ca8ca69_company.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/4a52d766f55e_nullable_from.py b/src/conode/infrastructure/persistence/migrations/versions/4a52d766f55e_nullable_from.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/4a52d766f55e_nullable_from.py rename to src/conode/infrastructure/persistence/migrations/versions/4a52d766f55e_nullable_from.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py b/src/conode/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py rename to src/conode/infrastructure/persistence/migrations/versions/4a8ee55b96fa_unique_constrait.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/504f086cc1a2_permission_type_offer.py b/src/conode/infrastructure/persistence/migrations/versions/504f086cc1a2_permission_type_offer.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/504f086cc1a2_permission_type_offer.py rename to src/conode/infrastructure/persistence/migrations/versions/504f086cc1a2_permission_type_offer.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py b/src/conode/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py rename to src/conode/infrastructure/persistence/migrations/versions/5192c880eb4e_edge_unique_constrait.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/6240bee5cfae_remove_owner_id.py b/src/conode/infrastructure/persistence/migrations/versions/6240bee5cfae_remove_owner_id.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/6240bee5cfae_remove_owner_id.py rename to src/conode/infrastructure/persistence/migrations/versions/6240bee5cfae_remove_owner_id.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/658eaeeb4857_edge_company_id.py b/src/conode/infrastructure/persistence/migrations/versions/658eaeeb4857_edge_company_id.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/658eaeeb4857_edge_company_id.py rename to src/conode/infrastructure/persistence/migrations/versions/658eaeeb4857_edge_company_id.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/6c046693f2e1_renamed_hashed_password_name.py b/src/conode/infrastructure/persistence/migrations/versions/6c046693f2e1_renamed_hashed_password_name.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/6c046693f2e1_renamed_hashed_password_name.py rename to src/conode/infrastructure/persistence/migrations/versions/6c046693f2e1_renamed_hashed_password_name.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/6c7e8a84a7c8_grants.py b/src/conode/infrastructure/persistence/migrations/versions/6c7e8a84a7c8_grants.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/6c7e8a84a7c8_grants.py rename to src/conode/infrastructure/persistence/migrations/versions/6c7e8a84a7c8_grants.py diff --git a/src/conode/infrastructure/persistence/migrations/versions/74a91a825965_remove_old_authorization.py b/src/conode/infrastructure/persistence/migrations/versions/74a91a825965_remove_old_authorization.py new file mode 100644 index 0000000..bd89c7b --- /dev/null +++ b/src/conode/infrastructure/persistence/migrations/versions/74a91a825965_remove_old_authorization.py @@ -0,0 +1,53 @@ +"""remove-old-authorization + +Revision ID: 74a91a825965 +Revises: 5192c880eb4e +Create Date: 2026-07-19 16:59:23.174543 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '74a91a825965' +down_revision: Union[str, Sequence[str], None] = '5192c880eb4e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('local_authorization_record') + op.drop_table('session_record') + op.drop_column('user_record', 'token_revision') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_record', sa.Column('token_revision', sa.INTEGER(), autoincrement=False, nullable=False)) + op.create_table('session_record', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('host', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('token', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), + sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user_record.id'], name=op.f('session_record_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('session_record_pkey')) + ) + op.create_table('local_authorization_record', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), + sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), + sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user_record.id'], name=op.f('local_authorization_record_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('local_authorization_record_pkey')) + ) + # ### end Alembic commands ### diff --git a/src/prodik/infrastructure/persistence/migrations/versions/78521bfa1ec2_contexts_and_edges.py b/src/conode/infrastructure/persistence/migrations/versions/78521bfa1ec2_contexts_and_edges.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/78521bfa1ec2_contexts_and_edges.py rename to src/conode/infrastructure/persistence/migrations/versions/78521bfa1ec2_contexts_and_edges.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/88413fda6e9e_initial.py b/src/conode/infrastructure/persistence/migrations/versions/88413fda6e9e_initial.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/88413fda6e9e_initial.py rename to src/conode/infrastructure/persistence/migrations/versions/88413fda6e9e_initial.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/b5ff44a6d487_nodes_and_groups.py b/src/conode/infrastructure/persistence/migrations/versions/b5ff44a6d487_nodes_and_groups.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/b5ff44a6d487_nodes_and_groups.py rename to src/conode/infrastructure/persistence/migrations/versions/b5ff44a6d487_nodes_and_groups.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/d221025bd616_role_name.py b/src/conode/infrastructure/persistence/migrations/versions/d221025bd616_role_name.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/d221025bd616_role_name.py rename to src/conode/infrastructure/persistence/migrations/versions/d221025bd616_role_name.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/e60ec6d4a0a5_roles_and_permissions.py b/src/conode/infrastructure/persistence/migrations/versions/e60ec6d4a0a5_roles_and_permissions.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/e60ec6d4a0a5_roles_and_permissions.py rename to src/conode/infrastructure/persistence/migrations/versions/e60ec6d4a0a5_roles_and_permissions.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/ee772395dc0d_association_cascade.py b/src/conode/infrastructure/persistence/migrations/versions/ee772395dc0d_association_cascade.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/ee772395dc0d_association_cascade.py rename to src/conode/infrastructure/persistence/migrations/versions/ee772395dc0d_association_cascade.py diff --git a/src/prodik/infrastructure/persistence/migrations/versions/ee89ddc73033_contexts.py b/src/conode/infrastructure/persistence/migrations/versions/ee89ddc73033_contexts.py similarity index 100% rename from src/prodik/infrastructure/persistence/migrations/versions/ee89ddc73033_contexts.py rename to src/conode/infrastructure/persistence/migrations/versions/ee89ddc73033_contexts.py diff --git a/src/prodik/infrastructure/persistence/registry.py b/src/conode/infrastructure/persistence/registry.py similarity index 87% rename from src/prodik/infrastructure/persistence/registry.py rename to src/conode/infrastructure/persistence/registry.py index b124ccb..829f1fa 100644 --- a/src/prodik/infrastructure/persistence/registry.py +++ b/src/conode/infrastructure/persistence/registry.py @@ -6,23 +6,20 @@ Enum, Float, ForeignKey, - Integer, MetaData, - String, Table, UniqueConstraint, ) from sqlalchemy.orm import registry -from prodik.domain.authorization import LocalAuthorization, Session -from prodik.domain.company import Company -from prodik.domain.context import Context -from prodik.domain.contract import Contract, ContractStatus -from prodik.domain.edge import Edge -from prodik.domain.grant import CompanyGrant, UserGrant -from prodik.domain.group import Group -from prodik.domain.node import Node, NodeAssociation -from prodik.domain.offer import ( +from conode.domain.company import Company +from conode.domain.context import Context +from conode.domain.contract import Contract, ContractStatus +from conode.domain.edge import Edge +from conode.domain.grant import CompanyGrant, UserGrant +from conode.domain.group import Group +from conode.domain.node import Node, NodeAssociation +from conode.domain.offer import ( Offer, OfferContext, OfferGroup, @@ -30,9 +27,9 @@ OfferLinkStatus, OfferStatus, ) -from prodik.domain.role import EntityType, PermissionType, Role, RolePermission -from prodik.domain.user import User, UserSystemRole -from prodik.infrastructure.persistence.types import ( +from conode.domain.role import EntityType, PermissionType, Role, RolePermission +from conode.domain.user import User, UserSystemRole +from conode.infrastructure.persistence.types import ( BioType, CompanyDescriptionType, CompanyNameType, @@ -64,28 +61,6 @@ Column("first_name", FirstNameType, nullable=False), Column("email", EmailType, nullable=False), Column("bio", BioType, nullable=False), - Column("token_revision", Integer, nullable=False), - Column("created_at", DateTime(timezone=True), nullable=False), - Column("updated_at", DateTime(timezone=True), nullable=False), -) - -session_record_table = Table( - "session_record", - metadata, - Column("id", UUID, primary_key=True, nullable=False), - Column("user_id", ForeignKey("user_record.id", ondelete="CASCADE"), nullable=False), - Column("host", String, nullable=False), - Column("token", String, nullable=False), - Column("created_at", DateTime(timezone=True), nullable=False), - Column("updated_at", DateTime(timezone=True), nullable=False), -) - -local_authorization_record_table = Table( - "local_authorization_record", - metadata, - Column("id", UUID, primary_key=True, nullable=False), - Column("user_id", ForeignKey("user_record.id", ondelete="CASCADE"), nullable=False), - Column("password", String, nullable=False), Column("created_at", DateTime(timezone=True), nullable=False), Column("updated_at", DateTime(timezone=True), nullable=False), ) @@ -404,11 +379,6 @@ def start_mapper() -> None: registry_mapper.map_imperatively(User, user_record_table) - registry_mapper.map_imperatively(Session, session_record_table) - registry_mapper.map_imperatively( - LocalAuthorization, - local_authorization_record_table, - ) registry_mapper.map_imperatively(Company, company_record_table) registry_mapper.map_imperatively(Node, node_record_table) registry_mapper.map_imperatively(Group, group_record_table) diff --git a/src/prodik/infrastructure/persistence/types.py b/src/conode/infrastructure/persistence/types.py similarity index 84% rename from src/prodik/infrastructure/persistence/types.py rename to src/conode/infrastructure/persistence/types.py index c200a25..56aa9d0 100644 --- a/src/prodik/infrastructure/persistence/types.py +++ b/src/conode/infrastructure/persistence/types.py @@ -3,14 +3,14 @@ from sqlalchemy import Dialect, String from sqlalchemy.types import TypeDecorator -from prodik.domain.company import CompanyDescription, CompanyName -from prodik.domain.context import ContextDescription, ContextName -from prodik.domain.group import GroupDescription, GroupName -from prodik.domain.node import NodeDescription, NodeName -from prodik.domain.offer import OfferDescription, OfferTitle -from prodik.domain.role import RoleName -from prodik.domain.shared import ValueObject -from prodik.domain.user import Bio, Email, FirstName, LastName, Username +from conode.domain.company import CompanyDescription, CompanyName +from conode.domain.context import ContextDescription, ContextName +from conode.domain.group import GroupDescription, GroupName +from conode.domain.node import NodeDescription, NodeName +from conode.domain.offer import OfferDescription, OfferTitle +from conode.domain.role import RoleName +from conode.domain.shared import ValueObject +from conode.domain.user import Bio, Email, FirstName, LastName, Username class BaseVOTypeDecorator[T: ValueObject[Any]](TypeDecorator[T]): diff --git a/src/prodik/infrastructure/repositories/__init__.py b/src/conode/infrastructure/repositories/__init__.py similarity index 87% rename from src/prodik/infrastructure/repositories/__init__.py rename to src/conode/infrastructure/repositories/__init__.py index b989eb6..a0b0e75 100644 --- a/src/prodik/infrastructure/repositories/__init__.py +++ b/src/conode/infrastructure/repositories/__init__.py @@ -1,4 +1,3 @@ -from .authorization import LocalAuthorizationRepositoryImpl, SessionRepositoryImpl from .company import CompanyRepositoryImpl from .context import ContextRepositoryImpl from .contract import ContractRepositoryImpl @@ -22,7 +21,6 @@ "ContractRepositoryImpl", "EdgeRepositoryImpl", "GroupRepositoryImpl", - "LocalAuthorizationRepositoryImpl", "NodeAssociationRepositoryImpl", "NodeRepositoryImpl", "OfferContextRepositoryImpl", @@ -31,7 +29,6 @@ "OfferRepositoryImpl", "RolePermissionsRepositoryImpl", "RoleRepositoryImpl", - "SessionRepositoryImpl", "UserGrantRepositoryImpl", "UserRepositoryImpl", ) diff --git a/src/prodik/infrastructure/repositories/company.py b/src/conode/infrastructure/repositories/company.py similarity index 94% rename from src/prodik/infrastructure/repositories/company.py rename to src/conode/infrastructure/repositories/company.py index a9146aa..10e3bdf 100644 --- a/src/prodik/infrastructure/repositories/company.py +++ b/src/conode/infrastructure/repositories/company.py @@ -4,10 +4,10 @@ from sqlalchemy import insert, select, update from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import CompanyNotFoundError -from prodik.application.interfaces.repositories import CompanyRepository -from prodik.domain.company.model import Company, CompanyId, CompanyName -from prodik.domain.user import UserId +from conode.application.errors import CompanyNotFoundError +from conode.application.interfaces.repositories import CompanyRepository +from conode.domain.company.model import Company, CompanyId, CompanyName +from conode.domain.user import UserId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/context.py b/src/conode/infrastructure/repositories/context.py similarity index 92% rename from src/prodik/infrastructure/repositories/context.py rename to src/conode/infrastructure/repositories/context.py index 0635791..e05f04c 100644 --- a/src/prodik/infrastructure/repositories/context.py +++ b/src/conode/infrastructure/repositories/context.py @@ -4,10 +4,10 @@ from sqlalchemy import delete, insert, select from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import ContextNotFoundError -from prodik.application.interfaces.repositories import ContextRepository -from prodik.domain.company import CompanyId -from prodik.domain.context import Context, ContextId +from conode.application.errors import ContextNotFoundError +from conode.application.interfaces.repositories import ContextRepository +from conode.domain.company import CompanyId +from conode.domain.context import Context, ContextId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/contract.py b/src/conode/infrastructure/repositories/contract.py similarity index 90% rename from src/prodik/infrastructure/repositories/contract.py rename to src/conode/infrastructure/repositories/contract.py index f6b9620..bba1d91 100644 --- a/src/prodik/infrastructure/repositories/contract.py +++ b/src/conode/infrastructure/repositories/contract.py @@ -4,8 +4,8 @@ from sqlalchemy import insert from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.interfaces.repositories import ContractRepository -from prodik.domain.contract.model import Contract +from conode.application.interfaces.repositories import ContractRepository +from conode.domain.contract.model import Contract logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/edge.py b/src/conode/infrastructure/repositories/edge.py similarity index 94% rename from src/prodik/infrastructure/repositories/edge.py rename to src/conode/infrastructure/repositories/edge.py index c3b2451..a772781 100644 --- a/src/prodik/infrastructure/repositories/edge.py +++ b/src/conode/infrastructure/repositories/edge.py @@ -5,11 +5,11 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import EdgeAlreadyExistsError, EdgeNotFoundError -from prodik.application.interfaces.repositories import EdgeRepository -from prodik.domain.context import ContextId -from prodik.domain.edge import Edge, EdgeId -from prodik.domain.node import NodeId +from conode.application.errors import EdgeAlreadyExistsError, EdgeNotFoundError +from conode.application.interfaces.repositories import EdgeRepository +from conode.domain.context import ContextId +from conode.domain.edge import Edge, EdgeId +from conode.domain.node import NodeId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/grant/__init__.py b/src/conode/infrastructure/repositories/grant/__init__.py similarity index 100% rename from src/prodik/infrastructure/repositories/grant/__init__.py rename to src/conode/infrastructure/repositories/grant/__init__.py diff --git a/src/prodik/infrastructure/repositories/grant/user.py b/src/conode/infrastructure/repositories/grant/user.py similarity index 91% rename from src/prodik/infrastructure/repositories/grant/user.py rename to src/conode/infrastructure/repositories/grant/user.py index 1188f58..de6ac92 100644 --- a/src/prodik/infrastructure/repositories/grant/user.py +++ b/src/conode/infrastructure/repositories/grant/user.py @@ -4,10 +4,10 @@ from sqlalchemy import insert, select from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.interfaces.repositories import UserGrantRepository -from prodik.domain.grant import UserGrant -from prodik.domain.role import RoleId -from prodik.domain.user import UserId +from conode.application.interfaces.repositories import UserGrantRepository +from conode.domain.grant import UserGrant +from conode.domain.role import RoleId +from conode.domain.user import UserId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/group.py b/src/conode/infrastructure/repositories/group.py similarity index 92% rename from src/prodik/infrastructure/repositories/group.py rename to src/conode/infrastructure/repositories/group.py index 25310f8..69910b2 100644 --- a/src/prodik/infrastructure/repositories/group.py +++ b/src/conode/infrastructure/repositories/group.py @@ -4,10 +4,10 @@ from sqlalchemy import delete, insert, select from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import GroupNotFoundError -from prodik.application.interfaces.repositories import GroupRepository -from prodik.domain.company import CompanyId -from prodik.domain.group import Group, GroupId +from conode.application.errors import GroupNotFoundError +from conode.application.interfaces.repositories import GroupRepository +from conode.domain.company import CompanyId +from conode.domain.group import Group, GroupId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/node.py b/src/conode/infrastructure/repositories/node.py similarity index 97% rename from src/prodik/infrastructure/repositories/node.py rename to src/conode/infrastructure/repositories/node.py index a887d66..266eced 100644 --- a/src/prodik/infrastructure/repositories/node.py +++ b/src/conode/infrastructure/repositories/node.py @@ -5,19 +5,19 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import ( +from conode.application.errors import ( AssociationNotFoundError, NodeCannotHaveSameAssociationsError, NodeMustHaveAtLeastOneAssociationError, NodeNotFoundError, ) -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( NodeAssociationRepository, NodeRepository, ) -from prodik.domain.company import CompanyId -from prodik.domain.group import GroupId -from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId +from conode.domain.company import CompanyId +from conode.domain.group import GroupId +from conode.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/offer.py b/src/conode/infrastructure/repositories/offer.py similarity index 97% rename from src/prodik/infrastructure/repositories/offer.py rename to src/conode/infrastructure/repositories/offer.py index e9a0eeb..047a1c1 100644 --- a/src/prodik/infrastructure/repositories/offer.py +++ b/src/conode/infrastructure/repositories/offer.py @@ -4,14 +4,14 @@ from sqlalchemy import and_, insert, select, update from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import OfferNotFoundError -from prodik.application.interfaces.repositories import ( +from conode.application.errors import OfferNotFoundError +from conode.application.interfaces.repositories import ( OfferContextRepository, OfferGroupRepository, OfferLinkRepository, OfferRepository, ) -from prodik.domain.offer.model import ( +from conode.domain.offer.model import ( Offer, OfferContext, OfferGroup, diff --git a/src/prodik/infrastructure/repositories/permission.py b/src/conode/infrastructure/repositories/permission.py similarity index 95% rename from src/prodik/infrastructure/repositories/permission.py rename to src/conode/infrastructure/repositories/permission.py index f64c872..16034ec 100644 --- a/src/prodik/infrastructure/repositories/permission.py +++ b/src/conode/infrastructure/repositories/permission.py @@ -4,11 +4,11 @@ from sqlalchemy import insert, select, update from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( RolePermissionsRepository, ) -from prodik.domain.role import RolePermission -from prodik.domain.role.model import RoleId, RolePermissionId +from conode.domain.role import RolePermission +from conode.domain.role.model import RoleId, RolePermissionId logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/role.py b/src/conode/infrastructure/repositories/role.py similarity index 92% rename from src/prodik/infrastructure/repositories/role.py rename to src/conode/infrastructure/repositories/role.py index f9496a0..8efb168 100644 --- a/src/prodik/infrastructure/repositories/role.py +++ b/src/conode/infrastructure/repositories/role.py @@ -4,11 +4,11 @@ from sqlalchemy import and_, delete, insert, select, update from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import RoleNotFoundError -from prodik.application.interfaces.repositories import RoleRepository -from prodik.domain.company import CompanyId -from prodik.domain.role import Role -from prodik.domain.role.model import RoleId, RoleName +from conode.application.errors import RoleNotFoundError +from conode.application.interfaces.repositories import RoleRepository +from conode.domain.company import CompanyId +from conode.domain.role import Role +from conode.domain.role.model import RoleId, RoleName logger = structlog.get_logger() diff --git a/src/prodik/infrastructure/repositories/user.py b/src/conode/infrastructure/repositories/user.py similarity index 91% rename from src/prodik/infrastructure/repositories/user.py rename to src/conode/infrastructure/repositories/user.py index 198805c..3ef661c 100644 --- a/src/prodik/infrastructure/repositories/user.py +++ b/src/conode/infrastructure/repositories/user.py @@ -4,9 +4,9 @@ from sqlalchemy import insert, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession -from prodik.application.errors import UserNotFoundError -from prodik.application.interfaces.repositories import UserRepository -from prodik.domain.user import Email, User, UserId, Username +from conode.application.errors import UserNotFoundError +from conode.application.interfaces.repositories import UserRepository +from conode.domain.user import Email, User, UserId, Username logger = structlog.get_logger() @@ -26,7 +26,6 @@ async def create(self, user: User) -> None: username=user.username, email=user.email, bio=user.bio, - token_revision=user.token_revision, created_at=user.created_at, updated_at=user.updated_at, ), @@ -44,9 +43,7 @@ async def update(self, user: User) -> None: first_name=user.first_name, last_name=user.last_name, username=user.username, - email=user.email, bio=user.bio, - token_revision=user.token_revision, ), ) diff --git a/src/conode/infrastructure/token_manager.py b/src/conode/infrastructure/token_manager.py new file mode 100644 index 0000000..ba26c2c --- /dev/null +++ b/src/conode/infrastructure/token_manager.py @@ -0,0 +1,39 @@ +import jwt + +from conode.application.interfaces.token_manager import ( + TokenManager, + UserMeta, +) +from conode.domain.user import UserSystemRole +from conode.domain.user.model import User +from conode.infrastructure.config import SecretsConfig + + +class TokenManagerImpl(TokenManager): + def __init__(self, config: SecretsConfig) -> None: + self._client = jwt.PyJWKClient(config.jwks_url) + self._config = config + + def encode(self, user: User) -> str: + raise NotImplementedError + + def decode(self, token: str) -> UserMeta: + signing_key = self._client.get_signing_key_from_jwt(token) + payload = jwt.decode( + token, + signing_key.key, + algorithms=["RS256"], + audience=self._config.audience, + issuer=self._config.issuer, + ) + role = UserSystemRole.USER + if "realm-admin" in payload["resource_access"]["realm-management"]["roles"]: + role = UserSystemRole.ADMIN + + return UserMeta( + email=payload["email"], + first_name=payload["given_name"], + last_name=payload["family_name"], + username=payload["preferred_username"], + system_role=role, + ) diff --git a/src/prodik/infrastructure/transaction_manager.py b/src/conode/infrastructure/transaction_manager.py similarity index 94% rename from src/prodik/infrastructure/transaction_manager.py rename to src/conode/infrastructure/transaction_manager.py index 98569e5..5814dce 100644 --- a/src/prodik/infrastructure/transaction_manager.py +++ b/src/conode/infrastructure/transaction_manager.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from structlog.contextvars import bind_contextvars -from prodik.application.interfaces.transaction_manager import TransactionManager +from conode.application.interfaces.transaction_manager import TransactionManager logger = structlog.get_logger() diff --git a/src/prodik/presentation/__init__.py b/src/conode/presentation/__init__.py similarity index 100% rename from src/prodik/presentation/__init__.py rename to src/conode/presentation/__init__.py diff --git a/src/prodik/presentation/common.py b/src/conode/presentation/common.py similarity index 78% rename from src/prodik/presentation/common.py rename to src/conode/presentation/common.py index 524c1a9..dbf4319 100644 --- a/src/prodik/presentation/common.py +++ b/src/conode/presentation/common.py @@ -1,14 +1,13 @@ import structlog from fastapi import FastAPI -from prodik.application.errors import ApplicationError -from prodik.presentation.exceptions import ( +from conode.application.errors import ApplicationError +from conode.presentation.exceptions import ( application_error_handler, default_error_handler, ) -from prodik.presentation.middlewares import LoggerMiddleware -from prodik.presentation.views import ( - auth, +from conode.presentation.middlewares import LoggerMiddleware +from conode.presentation.views import ( company, context, edge, @@ -30,7 +29,6 @@ def include_handlers(app: FastAPI) -> None: app.include_router(offer) app.include_router(role) app.include_router(root) - app.include_router(auth) app.include_router(user) app.include_router(node) app.include_router(edge) diff --git a/src/prodik/presentation/exceptions/__init__.py b/src/conode/presentation/exceptions/__init__.py similarity index 100% rename from src/prodik/presentation/exceptions/__init__.py rename to src/conode/presentation/exceptions/__init__.py diff --git a/src/prodik/presentation/exceptions/handlers.py b/src/conode/presentation/exceptions/handlers.py similarity index 88% rename from src/prodik/presentation/exceptions/handlers.py rename to src/conode/presentation/exceptions/handlers.py index 06f74ac..c469a7f 100644 --- a/src/prodik/presentation/exceptions/handlers.py +++ b/src/conode/presentation/exceptions/handlers.py @@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from prodik.application.errors import ( +from conode.application.errors import ( ApplicationError, AssociationNotFoundError, AuthorizationNotFoundError, @@ -35,13 +35,13 @@ UserAlreadyExistsError, UserNotFoundError, ) -from prodik.domain.company.errors import CompanyDomainValidationError -from prodik.domain.context.errors import ContextDomainValidationError -from prodik.domain.edge.errors import EdgeDomainValidationError -from prodik.domain.group.errors import GroupDomainValidationError -from prodik.domain.node.errors import NodeDomainValidationError -from prodik.domain.role.errors import RoleDomainValidationError -from prodik.domain.user.errors import UserDomainValidationError +from conode.domain.company.errors import CompanyDomainValidationError +from conode.domain.context.errors import ContextDomainValidationError +from conode.domain.edge.errors import EdgeDomainValidationError +from conode.domain.group.errors import GroupDomainValidationError +from conode.domain.node.errors import NodeDomainValidationError +from conode.domain.role.errors import RoleDomainValidationError +from conode.domain.user.errors import UserDomainValidationError logger = structlog.get_logger() diff --git a/src/prodik/presentation/middlewares/__init__.py b/src/conode/presentation/middlewares/__init__.py similarity index 100% rename from src/prodik/presentation/middlewares/__init__.py rename to src/conode/presentation/middlewares/__init__.py diff --git a/src/prodik/presentation/middlewares/log.py b/src/conode/presentation/middlewares/log.py similarity index 100% rename from src/prodik/presentation/middlewares/log.py rename to src/conode/presentation/middlewares/log.py diff --git a/src/prodik/presentation/schemas/__init__.py b/src/conode/presentation/schemas/__init__.py similarity index 100% rename from src/prodik/presentation/schemas/__init__.py rename to src/conode/presentation/schemas/__init__.py diff --git a/src/prodik/presentation/schemas/company.py b/src/conode/presentation/schemas/company.py similarity index 86% rename from src/prodik/presentation/schemas/company.py rename to src/conode/presentation/schemas/company.py index 2d31199..5470060 100644 --- a/src/prodik/presentation/schemas/company.py +++ b/src/conode/presentation/schemas/company.py @@ -2,14 +2,14 @@ from pydantic import BaseModel, Field -from prodik.domain.company import CompanyId -from prodik.domain.company.model import ( +from conode.domain.company import CompanyId +from conode.domain.company.model import ( MAX_ALLOWED_COMPANY_DESCRIPTION_LENGTH, MAX_ALLOWED_COMPANY_NAME_LENGTH, MIN_ALLOWED_COMPANY_DESCRIPTION_LENGTH, MIN_ALLOWED_COMPANY_NAME_LENGTH, ) -from prodik.domain.user import UserId +from conode.domain.user import UserId class CompanySchema(BaseModel): diff --git a/src/prodik/presentation/schemas/context.py b/src/conode/presentation/schemas/context.py similarity index 83% rename from src/prodik/presentation/schemas/context.py rename to src/conode/presentation/schemas/context.py index 6e05d78..fd4f9e9 100644 --- a/src/prodik/presentation/schemas/context.py +++ b/src/conode/presentation/schemas/context.py @@ -2,9 +2,9 @@ from pydantic import BaseModel, Field -from prodik.domain.company import CompanyId -from prodik.domain.context import ContextId -from prodik.domain.context.model import ( +from conode.domain.company import CompanyId +from conode.domain.context import ContextId +from conode.domain.context.model import ( MAX_ALLOWED_CONTEXT_DESCRIPTION_LENGTH, MAX_ALLOWED_CONTEXT_NAME_LENGTH, MIN_ALLOWED_CONTEXT_NAME_LENGTH, diff --git a/src/prodik/presentation/schemas/edge.py b/src/conode/presentation/schemas/edge.py similarity index 67% rename from src/prodik/presentation/schemas/edge.py rename to src/conode/presentation/schemas/edge.py index beda32e..69cf8d9 100644 --- a/src/prodik/presentation/schemas/edge.py +++ b/src/conode/presentation/schemas/edge.py @@ -1,9 +1,9 @@ from pydantic import BaseModel -from prodik.domain.company import CompanyId -from prodik.domain.context import ContextId -from prodik.domain.edge import EdgeId -from prodik.domain.node import NodeId +from conode.domain.company import CompanyId +from conode.domain.context import ContextId +from conode.domain.edge import EdgeId +from conode.domain.node import NodeId class CreateEdgeRequest(BaseModel): diff --git a/src/prodik/presentation/schemas/group.py b/src/conode/presentation/schemas/group.py similarity index 85% rename from src/prodik/presentation/schemas/group.py rename to src/conode/presentation/schemas/group.py index 927a744..47c3118 100644 --- a/src/prodik/presentation/schemas/group.py +++ b/src/conode/presentation/schemas/group.py @@ -2,9 +2,9 @@ from pydantic import BaseModel, Field -from prodik.domain.company import CompanyId -from prodik.domain.group import GroupId -from prodik.domain.group.model import ( +from conode.domain.company import CompanyId +from conode.domain.group import GroupId +from conode.domain.group.model import ( MAX_ALLOWED_GROUP_DESCRIPTION_LENGTH, MAX_ALLOWED_GROUP_NAME_LENGTH, MIN_ALLOWED_GROUP_NAME_LENGTH, diff --git a/src/prodik/presentation/schemas/node.py b/src/conode/presentation/schemas/node.py similarity index 86% rename from src/prodik/presentation/schemas/node.py rename to src/conode/presentation/schemas/node.py index 429b297..3ae4354 100644 --- a/src/prodik/presentation/schemas/node.py +++ b/src/conode/presentation/schemas/node.py @@ -2,10 +2,10 @@ from pydantic import BaseModel, Field -from prodik.domain.company import CompanyId -from prodik.domain.group import GroupId -from prodik.domain.node import NodeAssociationId, NodeId -from prodik.domain.node.model import ( +from conode.domain.company import CompanyId +from conode.domain.group import GroupId +from conode.domain.node import NodeAssociationId, NodeId +from conode.domain.node.model import ( MAX_ALLOWED_NODE_DESCRIPTION_LENGTH, MAX_ALLOWED_NODE_NAME_LENGTH, MIN_ALLOWED_NODE_NAME_LENGTH, diff --git a/src/prodik/presentation/schemas/offer.py b/src/conode/presentation/schemas/offer.py similarity index 73% rename from src/prodik/presentation/schemas/offer.py rename to src/conode/presentation/schemas/offer.py index ccd1271..8c13c39 100644 --- a/src/prodik/presentation/schemas/offer.py +++ b/src/conode/presentation/schemas/offer.py @@ -2,11 +2,11 @@ from pydantic import BaseModel -from prodik.domain.company import CompanyId -from prodik.domain.context import ContextId -from prodik.domain.group import GroupId -from prodik.domain.offer import OfferId, OfferStatus -from prodik.domain.role import PermissionType +from conode.domain.company import CompanyId +from conode.domain.context import ContextId +from conode.domain.group import GroupId +from conode.domain.offer import OfferId, OfferStatus +from conode.domain.role import PermissionType class SendOfferToCompanyRequest(BaseModel): diff --git a/src/prodik/presentation/schemas/role.py b/src/conode/presentation/schemas/role.py similarity index 91% rename from src/prodik/presentation/schemas/role.py rename to src/conode/presentation/schemas/role.py index 2404c04..cdd57a6 100644 --- a/src/prodik/presentation/schemas/role.py +++ b/src/conode/presentation/schemas/role.py @@ -2,15 +2,15 @@ from pydantic import BaseModel, Field -from prodik.domain.company import CompanyId -from prodik.domain.role import ( +from conode.domain.company import CompanyId +from conode.domain.role import ( EntityType, PermissionType, RoleId, RolePermissionEntityId, RolePermissionId, ) -from prodik.domain.role.model import ( +from conode.domain.role.model import ( MAX_ALLOWED_ROLE_NAME_LENGTH, MIN_ALLOWED_ROLE_NAME_LENGTH, ) diff --git a/src/prodik/presentation/schemas/user.py b/src/conode/presentation/schemas/user.py similarity index 80% rename from src/prodik/presentation/schemas/user.py rename to src/conode/presentation/schemas/user.py index f55a2cc..4590124 100644 --- a/src/prodik/presentation/schemas/user.py +++ b/src/conode/presentation/schemas/user.py @@ -2,9 +2,9 @@ from pydantic import BaseModel, EmailStr, Field -from prodik.domain.role import RoleId -from prodik.domain.user import UserId -from prodik.domain.user.model import ( +from conode.domain.role import RoleId +from conode.domain.user import UserId +from conode.domain.user.model import ( MAX_ALLOWED_BIO_LENGTH, MAX_ALLOWED_FIRST_NAME_LENGTH, MAX_ALLOWED_LAST_NAME_LENGTH, @@ -38,15 +38,9 @@ class UpdateCurrentUserProfileRequest(BaseModel): ), ] password: Annotated[str, Field(min_length=7, max_length=100)] - email: EmailStr bio: Annotated[str, Field(max_length=MAX_ALLOWED_BIO_LENGTH)] -class UpdateCurrentUserPasswordRequest(BaseModel): - old_password: Annotated[str, Field(min_length=7, max_length=100)] - new_password: Annotated[str, Field(min_length=7, max_length=100)] - - class UserSchema(BaseModel): id: UserId username: str diff --git a/src/prodik/presentation/views/__init__.py b/src/conode/presentation/views/__init__.py similarity index 90% rename from src/prodik/presentation/views/__init__.py rename to src/conode/presentation/views/__init__.py index a1e07ef..d76ac65 100644 --- a/src/prodik/presentation/views/__init__.py +++ b/src/conode/presentation/views/__init__.py @@ -1,4 +1,3 @@ -from .auth import router as auth from .company import router as company from .context import router as context from .edge import router as edge @@ -10,7 +9,6 @@ from .user import router as user __all__ = ( - "auth", "company", "context", "edge", diff --git a/src/prodik/presentation/views/company.py b/src/conode/presentation/views/company.py similarity index 83% rename from src/prodik/presentation/views/company.py rename to src/conode/presentation/views/company.py index 861fb33..d2194b0 100644 --- a/src/prodik/presentation/views/company.py +++ b/src/conode/presentation/views/company.py @@ -3,13 +3,13 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.register_company import ( +from conode.application.register_company import ( RegisterCompanyInteractor, RegisterCompanyRequestDTO, ) -from prodik.application.verify_company import VerifyCompanyInteractor -from prodik.domain.company import CompanyId -from prodik.presentation.schemas.company import CompanySchema, RegisterCompanyRequest +from conode.application.verify_company import VerifyCompanyInteractor +from conode.domain.company import CompanyId +from conode.presentation.schemas.company import CompanySchema, RegisterCompanyRequest router = APIRouter(tags=["companies"], prefix="/companies", route_class=DishkaRoute) diff --git a/src/prodik/presentation/views/context.py b/src/conode/presentation/views/context.py similarity index 87% rename from src/prodik/presentation/views/context.py rename to src/conode/presentation/views/context.py index fb81aa1..105afe1 100644 --- a/src/prodik/presentation/views/context.py +++ b/src/conode/presentation/views/context.py @@ -3,13 +3,13 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.manage_context import ( +from conode.application.manage_context import ( CreateContextInteractor, CreateContextRequestDTO, DeleteContextInteractor, ) -from prodik.domain.context import ContextId -from prodik.presentation.schemas.context import ContextSchema, CreateContextRequest +from conode.domain.context import ContextId +from conode.presentation.schemas.context import ContextSchema, CreateContextRequest router = APIRouter(tags=["contexts"], prefix="/contexts", route_class=DishkaRoute) diff --git a/src/prodik/presentation/views/edge.py b/src/conode/presentation/views/edge.py similarity index 89% rename from src/prodik/presentation/views/edge.py rename to src/conode/presentation/views/edge.py index 3cdaf89..fdef34a 100644 --- a/src/prodik/presentation/views/edge.py +++ b/src/conode/presentation/views/edge.py @@ -3,24 +3,24 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.manage_edge import ( +from conode.application.manage_edge import ( CreateEdgeInteractor, CreateEdgeRequestDTO, DeleteEdgeInteractor, ) -from prodik.application.receive_graph_statistics import ( +from conode.application.receive_graph_statistics import ( FindShortestPathInteractor, FindShortestPathRequestDTO, ) -from prodik.application.update_edge_weight import ( +from conode.application.update_edge_weight import ( DecrementEdgeWeightInteractor, IncrementEdgeWeightInteractor, UpdateEdgeWeightInteractor, ) -from prodik.domain.context import ContextId -from prodik.domain.edge import EdgeId -from prodik.domain.node import NodeId -from prodik.presentation.schemas.edge import ( +from conode.domain.context import ContextId +from conode.domain.edge import EdgeId +from conode.domain.node import NodeId +from conode.presentation.schemas.edge import ( CreateEdgeRequest, EdgeSchema, UpdateEdgeWeightRequest, diff --git a/src/prodik/presentation/views/group.py b/src/conode/presentation/views/group.py similarity index 88% rename from src/prodik/presentation/views/group.py rename to src/conode/presentation/views/group.py index 4ddd75d..2cdea35 100644 --- a/src/prodik/presentation/views/group.py +++ b/src/conode/presentation/views/group.py @@ -3,13 +3,13 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.manage_group import ( +from conode.application.manage_group import ( CreateGroupInteractor, CreateGroupRequestDTO, DeleteGroupInteractor, ) -from prodik.domain.group import GroupId -from prodik.presentation.schemas.group import CreateGroupRequest, GroupSchema +from conode.domain.group import GroupId +from conode.presentation.schemas.group import CreateGroupRequest, GroupSchema router = APIRouter(tags=["groups"], prefix="/groups", route_class=DishkaRoute) diff --git a/src/prodik/presentation/views/node.py b/src/conode/presentation/views/node.py similarity index 87% rename from src/prodik/presentation/views/node.py rename to src/conode/presentation/views/node.py index bbeef6b..ad13dde 100644 --- a/src/prodik/presentation/views/node.py +++ b/src/conode/presentation/views/node.py @@ -3,30 +3,30 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.attach_node_to_group import ( +from conode.application.attach_node_to_group import ( AttachNodeInteractor, AttachNodeRequestDTO, ) -from prodik.application.detach_node_from_group import DetachNodeInteractor -from prodik.application.manage_node import ( +from conode.application.detach_node_from_group import DetachNodeInteractor +from conode.application.manage_node import ( CreateNodeInteractor, CreateNodeRequestDTO, DeleteNodeInteractor, ) -from prodik.application.receive_graph_statistics import ( +from conode.application.receive_graph_statistics import ( GetNodeNeighboursInteractor, GetNodeNeighboursRequestDTO, ) -from prodik.application.receive_node_info import GetNodesByGroupInteractor -from prodik.application.update_node import ( +from conode.application.receive_node_info import GetNodesByGroupInteractor +from conode.application.update_node import ( UpdateNodeInteractor, UpdateNodeRequestDTO, ) -from prodik.domain.context import ContextId -from prodik.domain.group import GroupId -from prodik.domain.node import NodeAssociationId, NodeId -from prodik.presentation.schemas.edge import EdgeSchema -from prodik.presentation.schemas.node import ( +from conode.domain.context import ContextId +from conode.domain.group import GroupId +from conode.domain.node import NodeAssociationId, NodeId +from conode.presentation.schemas.edge import EdgeSchema +from conode.presentation.schemas.node import ( AttachNodeRequest, CreateNodeRequest, NodeAssociationSchema, diff --git a/src/prodik/presentation/views/offer.py b/src/conode/presentation/views/offer.py similarity index 92% rename from src/prodik/presentation/views/offer.py rename to src/conode/presentation/views/offer.py index 75a6fef..b4b84c2 100644 --- a/src/prodik/presentation/views/offer.py +++ b/src/conode/presentation/views/offer.py @@ -3,14 +3,14 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.share_graph import ( +from conode.application.share_graph import ( AcceptOfferInteractor, DeclineOfferInteractor, SendOfferToCompanyInteractor, SendOfferToCompanyRequestDTO, ) -from prodik.domain.offer import OfferId -from prodik.presentation.schemas.offer import OfferSchema, SendOfferToCompanyRequest +from conode.domain.offer import OfferId +from conode.presentation.schemas.offer import OfferSchema, SendOfferToCompanyRequest router = APIRouter(tags=["offers"], prefix="/offers", route_class=DishkaRoute) diff --git a/src/prodik/presentation/views/role.py b/src/conode/presentation/views/role.py similarity index 95% rename from src/prodik/presentation/views/role.py rename to src/conode/presentation/views/role.py index 557568b..f329eb8 100644 --- a/src/prodik/presentation/views/role.py +++ b/src/conode/presentation/views/role.py @@ -3,7 +3,7 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.manage_role import ( +from conode.application.manage_role import ( CreatePermissionRequestDTO, CreateRoleInteractor, CreateRoleRequestDTO, @@ -12,8 +12,8 @@ UpdateRoleInteractor, UpdateRoleRequestDTO, ) -from prodik.domain.role import RoleId -from prodik.presentation.schemas.role import ( +from conode.domain.role import RoleId +from conode.presentation.schemas.role import ( CreateRoleRequest, PermissionSchema, RoleSchema, diff --git a/src/prodik/presentation/views/root.py b/src/conode/presentation/views/root.py similarity index 100% rename from src/prodik/presentation/views/root.py rename to src/conode/presentation/views/root.py diff --git a/src/prodik/presentation/views/user.py b/src/conode/presentation/views/user.py similarity index 67% rename from src/prodik/presentation/views/user.py rename to src/conode/presentation/views/user.py index 0d7dc95..4ea6216 100644 --- a/src/prodik/presentation/views/user.py +++ b/src/conode/presentation/views/user.py @@ -3,27 +3,21 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter -from prodik.application.manage_credentials import ( - UpdateCurrentUserPasswordInteractor, - UpdateCurrentUserPasswordRequestDTO, -) -from prodik.application.manage_profile import ( +from conode.application.manage_profile import ( UpdateCurrentUserProfileInteractor, UpdateCurrentUserProfileRequestDTO, ) -from prodik.application.manage_user_rights import ( +from conode.application.manage_user_rights import ( GiveRoleToUserInteractor, RevokeRoleFromUserInteractor, ) -from prodik.application.receive_user_info import ( +from conode.application.receive_user_info import ( GetCurrentUserInteractor, GetUserByUsernameInteractor, ) -from prodik.domain.role import RoleId -from prodik.domain.user import UserId -from prodik.presentation.schemas.auth import AuthResponse -from prodik.presentation.schemas.user import ( - UpdateCurrentUserPasswordRequest, +from conode.domain.role import RoleId +from conode.domain.user import UserId +from conode.presentation.schemas.user import ( UpdateCurrentUserProfileRequest, UserSchema, ) @@ -41,7 +35,6 @@ async def update_current_user_profile( username=request.username, first_name=request.first_name, last_name=request.last_name, - email=request.email, bio=request.bio, ), ) @@ -62,24 +55,6 @@ async def get_current_user_profile( ) -@router.put("/me/password") -async def update_current_user_password( - request: UpdateCurrentUserPasswordRequest, - interactor: FromDishka[UpdateCurrentUserPasswordInteractor], -) -> AuthResponse: - result = await interactor.execute( - UpdateCurrentUserPasswordRequestDTO( - old_password=request.old_password, - new_password=request.new_password, - ), - ) - return AuthResponse( - access_token=result.access_token, - refresh_token=result.access_token, - expires_in=result.expires_in, - ) - - @router.get("/{username}") async def get_user_by_username( username: str, diff --git a/src/prodik/py.typed b/src/conode/py.typed similarity index 100% rename from src/prodik/py.typed rename to src/conode/py.typed diff --git a/src/prodik/application/authorization/__init__.py b/src/prodik/application/authorization/__init__.py deleted file mode 100644 index b962f61..0000000 --- a/src/prodik/application/authorization/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .login import LoginInteractor, LoginRequestDTO -from .refresh import RefreshTokenInteractor -from .register import RegisterInteractor, RegisterRequestDTO - -__all__ = ( - "LoginInteractor", - "LoginRequestDTO", - "RefreshTokenInteractor", - "RegisterInteractor", - "RegisterRequestDTO", -) diff --git a/src/prodik/application/authorization/login.py b/src/prodik/application/authorization/login.py deleted file mode 100644 index c08ed5e..0000000 --- a/src/prodik/application/authorization/login.py +++ /dev/null @@ -1,102 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.errors import ( - AuthorizationNotFoundError, - InvalidCredentialsError, - UserNotFoundError, -) -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.authorization import Session, SessionId -from prodik.domain.user import Email - - -@dataclass(frozen=True, slots=True, kw_only=True) -class LoginRequestDTO: - email: str - password: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class LoginResponseDTO: - access_token: str - refresh_token: str - expires_in: int - - -@dataclass -class LoginInteractor: - user_repository: UserRepository - session_repository: SessionRepository - authorization_repository: LocalAuthorizationRepository - password_hasher: PasswordHasher - identity_provider: IdentityProvider - transaction_manager: TransactionManager - access_token_manager: AccessTokenManager - refresh_token_manager: RefreshTokenManager - - async def execute(self, request: LoginRequestDTO) -> LoginResponseDTO: - async with self.transaction_manager: - host = self.identity_provider.get_current_ip() - - user = await self.user_repository.get_by_email(Email(request.email)) - if user is None: - raise UserNotFoundError( - "User not found", - [{"key": "email", "value": request.email}], - ) - - authorization = await self.authorization_repository.get_by_user_id(user.id) - if authorization is None: - raise AuthorizationNotFoundError( - "Authorization not found", - [{"key": "user_id", "value": user.id}], - ) - - if not self.password_hasher.verify( - authorization.password, - request.password, - ): - raise InvalidCredentialsError( - "Invalid email or password", - [ - {"key": "email", "value": request.email}, - {"key": "password", "value": request.password}, - ], - ) - - access_token, expires_in = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - - session = await self.session_repository.get_by_host_and_user_id( - user.id, host - ) - if session is None: - session = Session.new( - session_id=SessionId(uuid4()), - user=user, - host=host, - token=refresh_token, - ) - await self.session_repository.create(session) - else: - prev_token = session.token - session.update_token(refresh_token) - await self.session_repository.update(prev_token, session) - - return LoginResponseDTO( - access_token=access_token, - refresh_token=refresh_token, - expires_in=expires_in, - ) diff --git a/src/prodik/application/authorization/refresh.py b/src/prodik/application/authorization/refresh.py deleted file mode 100644 index 9ccc4b5..0000000 --- a/src/prodik/application/authorization/refresh.py +++ /dev/null @@ -1,47 +0,0 @@ -from dataclasses import dataclass - -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.repositories import SessionRepository, UserRepository -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.application.interfaces.transaction_manager import TransactionManager - - -@dataclass(slots=True, frozen=True, kw_only=True) -class RefreshTokenResponse: - access_token: str - refresh_token: str - expires_in: int - - -@dataclass -class RefreshTokenInteractor: - user_repository: UserRepository - identity_provider: IdentityProvider - session_repository: SessionRepository - access_token_manager: AccessTokenManager - refresh_token_manager: RefreshTokenManager - transaction_manager: TransactionManager - - async def execute(self, token: str) -> RefreshTokenResponse: - async with self.transaction_manager: - user_meta = self.identity_provider.get_current_user_meta() - - user = await self.user_repository.get_by_id(user_meta["user_id"]) - session = await self.session_repository.get_by_token(token) - - access_token, expires_in = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - - prev_token = session.token - session.update_token(refresh_token) - - await self.session_repository.update(prev_token, session) - - return RefreshTokenResponse( - access_token=access_token, - refresh_token=refresh_token, - expires_in=expires_in, - ) diff --git a/src/prodik/application/authorization/register.py b/src/prodik/application/authorization/register.py deleted file mode 100644 index 7ed66bd..0000000 --- a/src/prodik/application/authorization/register.py +++ /dev/null @@ -1,105 +0,0 @@ -from dataclasses import dataclass -from uuid import uuid4 - -from prodik.application.errors import UserAlreadyExistsError -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.authorization import ( - LocalAuthorization, - LocalAuthorizationId, - Session, - SessionId, -) -from prodik.domain.user import Email, User, UserId, Username - - -@dataclass(frozen=True, slots=True, kw_only=True) -class RegisterRequestDTO: - first_name: str - last_name: str - username: str - password: str - email: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class RegisterResponseDTO: - access_token: str - refresh_token: str - expires_in: int - - -@dataclass -class RegisterInteractor: - user_repository: UserRepository - session_repository: SessionRepository - authorization_repository: LocalAuthorizationRepository - access_token_manager: AccessTokenManager - refresh_token_manager: RefreshTokenManager - transaction_manager: TransactionManager - identity_provider: IdentityProvider - password_hasher: PasswordHasher - - async def execute(self, request: RegisterRequestDTO) -> RegisterResponseDTO: - async with self.transaction_manager: - host = self.identity_provider.get_current_ip() - user = await self.user_repository.get_by_username_or_email( - username=Username(request.username), - email=Email(request.email), - ) - - if user is not None: - raise UserAlreadyExistsError( - "User with this username or email already exists", - [ - {"key": "email", "value": request.email}, - {"key": "username", "value": request.username}, - ], - ) - - user = User.new( - user_id=UserId(uuid4()), - first_name=request.first_name, - last_name=request.last_name, - username=request.username, - email=request.email, - bio="", - ) - - access_token, expires_in = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - - session = Session.new( - session_id=SessionId(uuid4()), - user=user, - host=host, - token=refresh_token, - ) - - hashed_password = self.password_hasher.hash(request.password) - - authorization = LocalAuthorization.new( - local_authorization_id=LocalAuthorizationId(uuid4()), - user=user, - password=hashed_password, - ) - - await self.user_repository.create(user=user) - await self.session_repository.create(session=session) - await self.authorization_repository.create(authorization=authorization) - - return RegisterResponseDTO( - access_token=access_token, - refresh_token=refresh_token, - expires_in=expires_in, - ) diff --git a/src/prodik/application/interfaces/repositories/authorization/__init__.py b/src/prodik/application/interfaces/repositories/authorization/__init__.py deleted file mode 100644 index 019918b..0000000 --- a/src/prodik/application/interfaces/repositories/authorization/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .local_authorization import LocalAuthorizationRepository -from .session import SessionRepository - -__all__ = ( - "LocalAuthorizationRepository", - "SessionRepository", -) diff --git a/src/prodik/application/interfaces/repositories/authorization/local_authorization.py b/src/prodik/application/interfaces/repositories/authorization/local_authorization.py deleted file mode 100644 index af786f3..0000000 --- a/src/prodik/application/interfaces/repositories/authorization/local_authorization.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Protocol - -from prodik.domain.authorization import LocalAuthorization -from prodik.domain.user import UserId - - -class LocalAuthorizationRepository(Protocol): - async def create(self, authorization: LocalAuthorization) -> None: ... - async def update(self, authorization: LocalAuthorization) -> None: ... - async def get_by_user_id(self, user_id: UserId) -> LocalAuthorization | None: ... - async def get_all_by_user_id(self, user_id: UserId) -> list[LocalAuthorization]: ... diff --git a/src/prodik/application/interfaces/repositories/authorization/session.py b/src/prodik/application/interfaces/repositories/authorization/session.py deleted file mode 100644 index 646ae3c..0000000 --- a/src/prodik/application/interfaces/repositories/authorization/session.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Protocol - -from prodik.domain.authorization import Session -from prodik.domain.user import UserId - - -class SessionRepository(Protocol): - async def create(self, session: Session) -> None: ... - async def update(self, prev_token: str, session: Session) -> None: ... - async def get_by_host_and_user_id( - self, user_id: UserId, host: str - ) -> Session | None: ... - async def get_by_token(self, token: str) -> Session: ... diff --git a/src/prodik/application/interfaces/token_managers/__init__.py b/src/prodik/application/interfaces/token_managers/__init__.py deleted file mode 100644 index 45d99f2..0000000 --- a/src/prodik/application/interfaces/token_managers/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .access_token_manager import ( - AccessTokenManager, - AccessTokenManagerResponse, - UserMeta, -) -from .refresh_token_manager import RefreshTokenManager - -__all__ = ( - "AccessTokenManager", - "AccessTokenManagerResponse", - "RefreshTokenManager", - "UserMeta", -) diff --git a/src/prodik/application/interfaces/token_managers/access_token_manager.py b/src/prodik/application/interfaces/token_managers/access_token_manager.py deleted file mode 100644 index 4ecab4e..0000000 --- a/src/prodik/application/interfaces/token_managers/access_token_manager.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import NamedTuple, Protocol, TypedDict - -from prodik.domain.user import User, UserId, UserSystemRole - - -class UserMeta(TypedDict): - system_role: UserSystemRole - user_id: UserId - revision: int - - -class AccessTokenManagerResponse(NamedTuple): - token: str - expires_in: int - - -class AccessTokenManager(Protocol): - def encode(self, user: User) -> AccessTokenManagerResponse: ... - def decode(self, token: str) -> UserMeta: ... diff --git a/src/prodik/application/interfaces/token_managers/refresh_token_manager.py b/src/prodik/application/interfaces/token_managers/refresh_token_manager.py deleted file mode 100644 index 78ddc8d..0000000 --- a/src/prodik/application/interfaces/token_managers/refresh_token_manager.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Protocol - - -class RefreshTokenManager(Protocol): - def encode(self) -> str: ... diff --git a/src/prodik/application/manage_credentials/__init__.py b/src/prodik/application/manage_credentials/__init__.py deleted file mode 100644 index c48a973..0000000 --- a/src/prodik/application/manage_credentials/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .update_current_user_password import ( - UpdateCurrentUserPasswordInteractor, - UpdateCurrentUserPasswordRequestDTO, -) - -__all__ = ("UpdateCurrentUserPasswordInteractor", "UpdateCurrentUserPasswordRequestDTO") diff --git a/src/prodik/application/manage_credentials/update_current_user_password.py b/src/prodik/application/manage_credentials/update_current_user_password.py deleted file mode 100644 index 17930ba..0000000 --- a/src/prodik/application/manage_credentials/update_current_user_password.py +++ /dev/null @@ -1,103 +0,0 @@ -from dataclasses import dataclass - -from prodik.application.errors import ( - AuthorizationNotFoundError, - InvalidOldPasswordError, - SessionNotFoundError, -) -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import AccessControlService - - -@dataclass(frozen=True, slots=True, kw_only=True) -class UpdateCurrentUserPasswordRequestDTO: - old_password: str - new_password: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class UpdateCurrentUserPasswordResponseDTO: - access_token: str - refresh_token: str - expires_in: int - - -@dataclass -class UpdateCurrentUserPasswordInteractor: - user_repository: UserRepository - session_repository: SessionRepository - local_authorization_repository: LocalAuthorizationRepository - transaction_manager: TransactionManager - identity_provider: IdentityProvider - password_hasher: PasswordHasher - access_token_manager: AccessTokenManager - refresh_token_manager: RefreshTokenManager - access_control_service: AccessControlService - - async def execute( - self, - request: UpdateCurrentUserPasswordRequestDTO, - ) -> UpdateCurrentUserPasswordResponseDTO: - async with self.transaction_manager: - host = self.identity_provider.get_current_ip() - user = await self.access_control_service.get_authorized_user() - - authorization = await self.local_authorization_repository.get_by_user_id( - user.id, - ) - if authorization is None: - raise AuthorizationNotFoundError( - "Authorization by user id not found", - [{"key": "user_id", "value": user.id}], - ) - - if not self.password_hasher.verify( - authorization.password, - request.old_password, - ): - raise InvalidOldPasswordError( - "Invalid old password", - [ - {"key": "old_password", "value": request.old_password}, - ], - ) - - hashed_password = self.password_hasher.hash(request.new_password) - authorization.update_password(hashed_password) - user.increment_revision() - - session = await self.session_repository.get_by_host_and_user_id( - user.id, host - ) - if session is None: - raise SessionNotFoundError( - "Session not found", - [{"key": "host", "value": host}], - ) - - access_token, expires_in = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - - prev_token = session.token - session.update_token(refresh_token) - - await self.user_repository.update(user) - await self.local_authorization_repository.update(authorization) - await self.session_repository.update(prev_token, session) - - return UpdateCurrentUserPasswordResponseDTO( - access_token=access_token, - refresh_token=refresh_token, - expires_in=expires_in, - ) diff --git a/src/prodik/bootstrap/di/providers/cache.py b/src/prodik/bootstrap/di/providers/cache.py deleted file mode 100644 index 318630a..0000000 --- a/src/prodik/bootstrap/di/providers/cache.py +++ /dev/null @@ -1,20 +0,0 @@ -from collections.abc import AsyncIterator - -from dishka import Provider, Scope, provide -from redis.asyncio import Redis - -from prodik.infrastructure.config import CacheConfig - - -class CacheConnectionProvider(Provider): - @provide(scope=Scope.APP) - async def get_redis_connection(self, config: CacheConfig) -> AsyncIterator[Redis]: - client = Redis( - host=config.host, - port=config.port, - decode_responses=True, - ) - - yield client - - await client.aclose() diff --git a/src/prodik/domain/authorization/__init__.py b/src/prodik/domain/authorization/__init__.py deleted file mode 100644 index 696af78..0000000 --- a/src/prodik/domain/authorization/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .local import LocalAuthorization, LocalAuthorizationId -from .oauth import OAuthAuthorization, OAuthAuthorizationId -from .session import Session, SessionId - -__all__ = ( - "LocalAuthorization", - "LocalAuthorizationId", - "OAuthAuthorization", - "OAuthAuthorizationId", - "Session", - "SessionId", -) diff --git a/src/prodik/domain/authorization/local/__init__.py b/src/prodik/domain/authorization/local/__init__.py deleted file mode 100644 index b45efae..0000000 --- a/src/prodik/domain/authorization/local/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .model import LocalAuthorization, LocalAuthorizationId - -__all__ = ( - "LocalAuthorization", - "LocalAuthorizationId", -) diff --git a/src/prodik/domain/authorization/local/model.py b/src/prodik/domain/authorization/local/model.py deleted file mode 100644 index 00f05ae..0000000 --- a/src/prodik/domain/authorization/local/model.py +++ /dev/null @@ -1,34 +0,0 @@ -from dataclasses import dataclass -from datetime import UTC, datetime -from typing import NewType, Self -from uuid import UUID - -from prodik.domain.shared import Entity -from prodik.domain.user import User, UserId - -LocalAuthorizationId = NewType("LocalAuthorizationId", UUID) - - -@dataclass -class LocalAuthorization(Entity[LocalAuthorizationId]): - user_id: UserId - password: str - - @classmethod - def new( - cls, - local_authorization_id: LocalAuthorizationId, - user: User, - password: str, - ) -> Self: - now = datetime.now(UTC) - return cls( - id=local_authorization_id, - user_id=user.id, - password=password, - created_at=now, - updated_at=now, - ) - - def update_password(self, password: str) -> None: - self.password = password diff --git a/src/prodik/domain/authorization/oauth/__init__.py b/src/prodik/domain/authorization/oauth/__init__.py deleted file mode 100644 index 27465e7..0000000 --- a/src/prodik/domain/authorization/oauth/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .model import OAuthAuthorization, OAuthAuthorizationId - -__all__ = ( - "OAuthAuthorization", - "OAuthAuthorizationId", -) diff --git a/src/prodik/domain/authorization/oauth/model.py b/src/prodik/domain/authorization/oauth/model.py deleted file mode 100644 index 30815a4..0000000 --- a/src/prodik/domain/authorization/oauth/model.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass -from datetime import UTC, datetime -from typing import NewType, Self -from uuid import UUID - -from prodik.domain.shared import Entity -from prodik.domain.user import User, UserId - -OAuthAuthorizationId = NewType("OAuthAuthorizationId", UUID) - - -@dataclass -class OAuthAuthorization(Entity[OAuthAuthorizationId]): - user_id: UserId - - @classmethod - def new(cls, oauth_authorization_id: OAuthAuthorizationId, user: User) -> Self: - now = datetime.now(UTC) - return cls( - id=oauth_authorization_id, - user_id=user.id, - created_at=now, - updated_at=now, - ) diff --git a/src/prodik/domain/authorization/session/__init__.py b/src/prodik/domain/authorization/session/__init__.py deleted file mode 100644 index 3b8f490..0000000 --- a/src/prodik/domain/authorization/session/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .model import Session, SessionId - -__all__ = ( - "Session", - "SessionId", -) diff --git a/src/prodik/domain/authorization/session/model.py b/src/prodik/domain/authorization/session/model.py deleted file mode 100644 index 3994814..0000000 --- a/src/prodik/domain/authorization/session/model.py +++ /dev/null @@ -1,32 +0,0 @@ -from dataclasses import dataclass -from datetime import UTC, datetime -from typing import NewType, Self -from uuid import UUID - -from prodik.domain.shared import Entity -from prodik.domain.user import User, UserId - -SessionId = NewType("SessionId", UUID) - - -@dataclass -class Session(Entity[SessionId]): - user_id: UserId - token: str - host: str - - @classmethod - def new(cls, session_id: SessionId, user: User, host: str, token: str) -> Self: - now = datetime.now(UTC) - return cls( - id=session_id, - host=host, - token=token, - user_id=user.id, - created_at=now, - updated_at=now, - ) - - def update_token(self, token: str) -> None: - self.token = token - self.touch() diff --git a/src/prodik/infrastructure/identity_provider.py b/src/prodik/infrastructure/identity_provider.py deleted file mode 100644 index 1acd7a6..0000000 --- a/src/prodik/infrastructure/identity_provider.py +++ /dev/null @@ -1,60 +0,0 @@ -from dataclasses import dataclass -from typing import Final - -import structlog -from fastapi import Request -from jwt import PyJWTError - -from prodik.application.errors import FailedToReadClientError, InvalidTokenError -from prodik.application.interfaces.identity_provider import IdentityProvider -from prodik.application.interfaces.token_managers import AccessTokenManager, UserMeta - -TOKEN_TYPE: Final = "Bearer" # noqa: S105 -TOKEN_SECTIONS: Final = 2 - -logger = structlog.get_logger() - - -@dataclass -class IdentityProviderImpl(IdentityProvider): - request: Request - access_token_manager: AccessTokenManager - - def get_current_ip(self) -> str: - if self.request.client is None: - raise FailedToReadClientError("Failed to read client error", None) - - return self.request.client.host - - def get_current_user_meta(self) -> UserMeta: - header = self.request.headers.get("Authorization") - if header is None: - raise InvalidTokenError("Authorization token not found", None) - - header_content = header.split(" ") - if len(header_content) != TOKEN_SECTIONS: - raise InvalidTokenError( - "Invalid authorization token format", - [{"key": "Authorization", "value": header}], - ) - - token_type, token = header_content - if token_type != TOKEN_TYPE: - raise InvalidTokenError( - "Invalid authorization token type", - [{"key": "Authorization", "value": token_type}], - ) - - try: - content = self.access_token_manager.decode(token) - except PyJWTError as e: - raise InvalidTokenError( - "Invalid authorization token", - [{"key": "Authorization", "value": token}], - ) from e - - return UserMeta( - system_role=content["system_role"], - user_id=content["user_id"], - revision=content["revision"], - ) diff --git a/src/prodik/infrastructure/repositories/authorization/__init__.py b/src/prodik/infrastructure/repositories/authorization/__init__.py deleted file mode 100644 index 8e0f909..0000000 --- a/src/prodik/infrastructure/repositories/authorization/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .local_authorization import LocalAuthorizationRepositoryImpl -from .session import SessionRepositoryImpl - -__all__ = ( - "LocalAuthorizationRepositoryImpl", - "SessionRepositoryImpl", -) diff --git a/src/prodik/infrastructure/repositories/authorization/local_authorization.py b/src/prodik/infrastructure/repositories/authorization/local_authorization.py deleted file mode 100644 index b5c5095..0000000 --- a/src/prodik/infrastructure/repositories/authorization/local_authorization.py +++ /dev/null @@ -1,76 +0,0 @@ -from dataclasses import dataclass - -import structlog -from sqlalchemy import insert, select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from prodik.application.interfaces.repositories import LocalAuthorizationRepository -from prodik.domain.authorization import LocalAuthorization -from prodik.domain.user import UserId - -logger = structlog.get_logger() - - -@dataclass -class LocalAuthorizationRepositoryImpl(LocalAuthorizationRepository): - session: AsyncSession - - async def create(self, authorization: LocalAuthorization) -> None: - logger.debug( - "Repository create local authorization", - authorization_id=authorization.id, - ) - await self.session.execute( - insert(LocalAuthorization).values( - id=authorization.id, - user_id=authorization.user_id, - password=authorization.password, - created_at=authorization.created_at, - updated_at=authorization.updated_at, - ), - ) - - async def update(self, authorization: LocalAuthorization) -> None: - logger.debug( - "Repository update local authorization", - authorization_id=authorization.id, - ) - await self.session.execute( - update(LocalAuthorization) - .where( - LocalAuthorization.id == authorization.id, # type: ignore - ) - .values( - user_id=authorization.user_id, - password=authorization.password, - ), - ) - - async def get_by_user_id(self, user_id: UserId) -> LocalAuthorization | None: - logger.debug("Repository get local authorization by user id", user_id=user_id) - result = await self.session.execute( - select(LocalAuthorization).where(LocalAuthorization.user_id == user_id), # type: ignore - ) - - authorization = result.scalar_one_or_none() - logger.debug( - "Repository fetched local authorization by user id", - found=authorization is not None, - ) - return authorization - - async def get_all_by_user_id(self, user_id: UserId) -> list[LocalAuthorization]: - logger.debug("Repository get local authorizations by user id", user_id=user_id) - result = await self.session.execute( - select(LocalAuthorization).where( - LocalAuthorization.user_id == user_id # type: ignore - ) - ) - - result_local_authorizations = list(result.scalars().all()) - - logger.debug( - "Repository fetched edges by context id", - found_count=len(result_local_authorizations), - ) - return result_local_authorizations diff --git a/src/prodik/infrastructure/repositories/authorization/session.py b/src/prodik/infrastructure/repositories/authorization/session.py deleted file mode 100644 index 6d177d8..0000000 --- a/src/prodik/infrastructure/repositories/authorization/session.py +++ /dev/null @@ -1,118 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -import structlog -from redis.asyncio import Redis - -from prodik.application.errors import SessionNotFoundError -from prodik.application.interfaces.repositories import SessionRepository -from prodik.domain.authorization import Session, SessionId -from prodik.domain.user import UserId -from prodik.infrastructure.config import CacheConfig - -logger = structlog.get_logger() - - -@dataclass -class SessionRepositoryImpl(SessionRepository): - client: Redis - config: CacheConfig - - async def create(self, session: Session) -> None: - logger.debug("Repository create session", session_id=session.id) - session_key = f"session:{session.id}" - - host_key = f"host:{session.host}:{session.user_id}" - token_key = f"token:{session.token}" - - await self.client.hset( - session_key, - mapping={ # type: ignore - "id": str(session.id), - "user_id": str(session.user_id), - "token": session.token, - "host": session.host, - "created_at": session.created_at.isoformat(), - "updated_at": session.updated_at.isoformat(), - }, - ) - - await self.client.set(host_key, str(session.id)) - await self.client.set(token_key, str(session.id)) - - await self.client.expire(session_key, self.config.ttl) - - async def update(self, prev_token: str, session: Session) -> None: - logger.debug("Repository update session", session_id=session.id) - - session_key = f"session:{session.id}" - token_key = f"token:{prev_token}" - - await self.client.hset( - session_key, - mapping={ # type: ignore - "token": session.token, - "updated_at": session.updated_at.isoformat(), - }, - ) - - await self.client.delete(token_key) - await self.client.set(token_key, str(session.id)) - - async def get_by_token(self, token: str) -> Session: - logger.debug("Repository get session by token") - - session_id = await self.client.get(f"token:{token}") - if session_id is None: - raise SessionNotFoundError( - "Session not found", - [{"key": "refresh_token", "value": token}], - ) - - data = await self.client.hgetall(f"session:{session_id}") # type: ignore - if data is None: - raise SessionNotFoundError( - "Session not found", - [{"key": "refresh_token", "value": token}], - ) - - session = Session( - id=SessionId(data["id"]), - user_id=UserId(data["user_id"]), - token=data["token"], - host=data["host"], - created_at=datetime.fromisoformat(data["created_at"]), - updated_at=datetime.fromisoformat(data["updated_at"]), - ) - - logger.debug("Repository fetched session by token", found=session is not None) - return session - - async def get_by_host_and_user_id( - self, user_id: UserId, host: str - ) -> Session | None: - logger.debug("Repository get session by host", host=host) - - session_id = await self.client.get(f"host:{host}:{user_id}") - if session_id is None: - return None - - data = await self.client.hgetall(f"session:{session_id}") # type: ignore - if data is None: - raise SessionNotFoundError( - "Session not found", - [{"key": "refresh_token", "value": session_id}], - ) - - session = Session( - id=SessionId(data["id"]), - user_id=UserId(data["user_id"]), - token=data["token"], - host=data["host"], - created_at=datetime.fromisoformat(data["created_at"]), - updated_at=datetime.fromisoformat(data["updated_at"]), - ) - - logger.debug("Repository fetched session by host", found=session is not None) - - return session diff --git a/src/prodik/infrastructure/token_managers/__init__.py b/src/prodik/infrastructure/token_managers/__init__.py deleted file mode 100644 index 775a8dc..0000000 --- a/src/prodik/infrastructure/token_managers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .access_token_manager import AccessTokenManagerImpl -from .refresh_token_manager import RefreshTokenManagerImpl - -__all__ = ( - "AccessTokenManagerImpl", - "RefreshTokenManagerImpl", -) diff --git a/src/prodik/infrastructure/token_managers/access_token_manager.py b/src/prodik/infrastructure/token_managers/access_token_manager.py deleted file mode 100644 index 0db6af3..0000000 --- a/src/prodik/infrastructure/token_managers/access_token_manager.py +++ /dev/null @@ -1,43 +0,0 @@ -from dataclasses import dataclass -from datetime import UTC, datetime, timedelta - -import jwt - -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - AccessTokenManagerResponse, - UserMeta, -) -from prodik.domain.user import User -from prodik.infrastructure.config import SecretsConfig - - -@dataclass -class AccessTokenManagerImpl(AccessTokenManager): - _config: SecretsConfig - - def encode(self, user: User) -> AccessTokenManagerResponse: - now = datetime.now(tz=UTC) - token = jwt.encode( - { - "sub": str(user.id), - "system_role": user.system_role, - "revision": user.token_revision, - "iat": now, - "exp": now + timedelta(seconds=self._config.expires_in), - }, - self._config.secret, - algorithm="HS256", - ) - return AccessTokenManagerResponse( - token=token, - expires_in=self._config.expires_in, - ) - - def decode(self, token: str) -> UserMeta: - data = jwt.decode(token, self._config.secret, algorithms=["HS256"]) - return UserMeta( - system_role=data["system_role"], - user_id=data["sub"], - revision=data["revision"], - ) diff --git a/src/prodik/infrastructure/token_managers/refresh_token_manager.py b/src/prodik/infrastructure/token_managers/refresh_token_manager.py deleted file mode 100644 index 6db2f96..0000000 --- a/src/prodik/infrastructure/token_managers/refresh_token_manager.py +++ /dev/null @@ -1,10 +0,0 @@ -import secrets -from dataclasses import dataclass - -from prodik.application.interfaces.token_managers import RefreshTokenManager - - -@dataclass -class RefreshTokenManagerImpl(RefreshTokenManager): - def encode(self) -> str: - return secrets.token_urlsafe(32) diff --git a/src/prodik/presentation/schemas/auth.py b/src/prodik/presentation/schemas/auth.py deleted file mode 100644 index 4ca992d..0000000 --- a/src/prodik/presentation/schemas/auth.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Annotated - -from pydantic import BaseModel, EmailStr, Field - -from prodik.domain.user.model import ( - MAX_ALLOWED_FIRST_NAME_LENGTH, - MAX_ALLOWED_LAST_NAME_LENGTH, - MAX_ALLOWED_USERNAME_LENGTH, - MIN_ALLOWED_FIRST_NAME_LENGTH, - MIN_ALLOWED_LAST_NAME_LENGTH, - MIN_ALLOWED_USERNAME_LENGTH, -) - - -class RegisterRequest(BaseModel): - username: Annotated[ - str, - Field( - min_length=MIN_ALLOWED_USERNAME_LENGTH, - max_length=MAX_ALLOWED_USERNAME_LENGTH, - ), - ] - first_name: Annotated[ - str, - Field( - min_length=MIN_ALLOWED_FIRST_NAME_LENGTH, - max_length=MAX_ALLOWED_FIRST_NAME_LENGTH, - ), - ] - last_name: Annotated[ - str, - Field( - min_length=MIN_ALLOWED_LAST_NAME_LENGTH, - max_length=MAX_ALLOWED_LAST_NAME_LENGTH, - ), - ] - password: Annotated[str, Field(min_length=7, max_length=100)] - email: EmailStr - - -class LoginRequest(BaseModel): - email: EmailStr - password: str - - -class AuthResponse(BaseModel): - access_token: str - refresh_token: str - expires_in: int - - -class RefreshTokenRequest(BaseModel): - refresh_token: str diff --git a/src/prodik/presentation/views/auth.py b/src/prodik/presentation/views/auth.py deleted file mode 100644 index 5b497fa..0000000 --- a/src/prodik/presentation/views/auth.py +++ /dev/null @@ -1,75 +0,0 @@ -from http import HTTPStatus - -from dishka.integrations.fastapi import DishkaRoute, FromDishka -from fastapi import APIRouter - -from prodik.application.authorization import ( - LoginInteractor, - LoginRequestDTO, - RefreshTokenInteractor, - RegisterInteractor, - RegisterRequestDTO, -) -from prodik.presentation.schemas.auth import ( - AuthResponse, - LoginRequest, - RefreshTokenRequest, - RegisterRequest, -) - -router = APIRouter(tags=["authorization"], route_class=DishkaRoute, prefix="/auth") - - -@router.post("/register", status_code=HTTPStatus.CREATED) -async def register( - request: RegisterRequest, - interactor: FromDishka[RegisterInteractor], -) -> AuthResponse: - result = await interactor.execute( - RegisterRequestDTO( - first_name=request.first_name, - last_name=request.last_name, - username=request.username, - password=request.password, - email=request.email, - ), - ) - - return AuthResponse( - access_token=result.access_token, - refresh_token=result.refresh_token, - expires_in=result.expires_in, - ) - - -@router.post("/login") -async def login( - request: LoginRequest, - interactor: FromDishka[LoginInteractor], -) -> AuthResponse: - result = await interactor.execute( - LoginRequestDTO( - password=request.password, - email=request.email, - ), - ) - - return AuthResponse( - access_token=result.access_token, - refresh_token=result.refresh_token, - expires_in=result.expires_in, - ) - - -@router.post("/refresh") -async def refresh_token( - request: RefreshTokenRequest, - interactor: FromDishka[RefreshTokenInteractor], -) -> AuthResponse: - result = await interactor.execute(request.refresh_token) - - return AuthResponse( - access_token=result.access_token, - refresh_token=result.refresh_token, - expires_in=result.expires_in, - ) diff --git a/tests/conftest.py b/tests/conftest.py index e9036be..1901153 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from types import TracebackType from unittest.mock import AsyncMock +import jwt import pytest from dishka import ( AsyncContainer, @@ -15,49 +16,45 @@ ) from dishka.integrations.fastapi import FastapiProvider, setup_dishka from httpx import ASGITransport, AsyncClient -from redis.asyncio import Redis from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncSession, create_async_engine, ) -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.password_hasher import PasswordHasher +from conode.application.interfaces.repositories import ( CompanyRepository, ContextRepository, EdgeRepository, GroupRepository, - LocalAuthorizationRepository, NodeAssociationRepository, NodeRepository, RolePermissionsRepository, RoleRepository, - SessionRepository, UserGrantRepository, UserRepository, ) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, +from conode.application.interfaces.token_manager import ( + TokenManager, + UserMeta, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import RoleManagmentService -from prodik.bootstrap.api.run import create_app -from prodik.bootstrap.di.providers import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import RoleManagmentService +from conode.bootstrap.api.run import create_app +from conode.bootstrap.di.providers import ( ApplicationProvider, - CacheConnectionProvider, InfrastructureProvider, ) -from prodik.infrastructure.config import ( +from conode.domain.user import User, UserSystemRole +from conode.infrastructure.config import ( APIConfig, - CacheConfig, Config, DatabaseConfig, SecretsConfig, load_config, ) -from prodik.infrastructure.persistence import start_mapper +from conode.infrastructure.persistence import start_mapper from tests.factories.models import ( CompanyFactory, ContextFactory, @@ -69,6 +66,44 @@ ) +@dataclass +class MockTokenManager(TokenManager): + config: SecretsConfig + + def decode(self, token: str) -> UserMeta: + payload = jwt.decode( + token, + key=self.config.secret, + algorithms=["HS256"], + ) + role = UserSystemRole.USER + if "realm-admin" in payload["resource_access"]["realm-management"]["roles"]: + role = UserSystemRole.ADMIN + + return UserMeta( + email=payload["email"], + first_name=payload["given_name"], + last_name=payload["family_name"], + username=payload["preferred_username"], + system_role=role, + ) + + def encode(self, user: User, *, admin: bool = False) -> str: + return jwt.encode( + payload={ + "email": user.email.value, + "given_name": user.first_name.value, + "family_name": user.last_name.value, + "preferred_username": user.username.value, + "resource_access": { + "realm-management": {"roles": ["realm-admin" if admin else "user"]} + }, + }, + algorithm="HS256", + key=self.config.secret, + ) + + @pytest.fixture(scope="session") def config() -> Config: return load_config("test.config.toml") @@ -79,28 +114,6 @@ def startup() -> None: start_mapper() -@pytest.fixture(scope="session", autouse=True) -async def clear_cache(config: Config) -> AsyncIterator[None]: - yield - redis = Redis(host=config.cache.host, port=config.cache.port) - await redis.flushdb() - await redis.aclose() - - -@pytest.fixture -async def session_repository(container: AsyncContainer) -> SessionRepository: - async with container() as test_container: - return await test_container.get(SessionRepository) # type: ignore[no-any-return] - - -@pytest.fixture -async def local_authorization_repository( - container: AsyncContainer, -) -> LocalAuthorizationRepository: - async with container() as test_container: - return await test_container.get(LocalAuthorizationRepository) # type: ignore[no-any-return] - - @pytest.fixture async def node_repository( container: AsyncContainer, @@ -125,12 +138,6 @@ async def group_repository( return await test_container.get(GroupRepository) # type: ignore[no-any-return] -@pytest.fixture -async def access_token_manager(container: AsyncContainer) -> AccessTokenManager: - async with container() as test_container: - return await test_container.get(AccessTokenManager) # type: ignore[no-any-return] - - @pytest.fixture async def user_repository(container: AsyncContainer) -> UserRepository: async with container() as test_container: @@ -214,13 +221,8 @@ async def context_factory(container: AsyncContainer) -> ContextFactory: async def user_factory(container: AsyncContainer) -> UserFactory: async with container() as test_container: return UserFactory( - local_authorization_repository=await test_container.get( - LocalAuthorizationRepository - ), - refresh_token_manager=await test_container.get(RefreshTokenManager), - access_token_manager=await test_container.get(AccessTokenManager), + token_manager=await test_container.get(TokenManager), transaction_manager=await test_container.get(TransactionManager), - session_repository=await test_container.get(SessionRepository), password_hasher=await test_container.get(PasswordHasher), user_repository=await test_container.get(UserRepository), ) @@ -301,17 +303,19 @@ class TestConnectionProvider(Provider): async def provide_async_session(self) -> AsyncSession: return session + class TestSecretsProvider(Provider): + provides = provide_all(WithParents[MockTokenManager], scope=Scope.REQUEST) + container = make_async_container( FastapiProvider(), ApplicationProvider(), TestConnectionProvider(), InfrastructureProvider(), - CacheConnectionProvider(), + TestSecretsProvider(), context={ APIConfig: config.api, DatabaseConfig: config.database, SecretsConfig: config.secrets, - CacheConfig: config.cache, }, ) diff --git a/tests/e2e/authentication/__init__.py b/tests/e2e/authentication/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/e2e/authentication/test_login.py b/tests/e2e/authentication/test_login.py deleted file mode 100644 index 2ea94de..0000000 --- a/tests/e2e/authentication/test_login.py +++ /dev/null @@ -1,70 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr -from httpx import AsyncClient - -from prodik.application.interfaces.repositories import SessionRepository -from tests.factories.models import UserFactory -from tests.factories.schemas import LoginRequestFactory - - -@pytest.mark.asyncio -async def test_login_ok( - transport: AsyncClient, - user_factory: UserFactory, - session_repository: SessionRepository, -) -> None: - user_factory_response = await user_factory.build() - - request = LoginRequestFactory.build( - email=user_factory_response.user.email.value, - password=user_factory_response.password, - ) - - response = await transport.post("/auth/login", json=request.model_dump()) - content = response.json() - - session = await session_repository.get_by_host_and_user_id( - user_factory_response.user.id, "127.0.0.1" - ) - - assert response.status_code == HTTPStatus.OK - assert session == IsPartialDataclass( - user_id=str(user_factory_response.user.id), - token=content["refresh_token"], - host="127.0.0.1", - ) - assert content == IsPartialDict( - access_token=IsStr, - refresh_token=IsStr, - expires_in=IsInt, - ) - - -@pytest.mark.asyncio -async def test_login_invalid_password( - transport: AsyncClient, - user_factory: UserFactory, - session_repository: SessionRepository, -) -> None: - user_factory_response = await user_factory.build() - - request = LoginRequestFactory.build( - email=user_factory_response.user.email.value, - password="", - ) - - response = await transport.post("/auth/login", json=request.model_dump()) - - session = await session_repository.get_by_token(user_factory_response.refresh_token) - - assert session is not None - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Invalid email or password", - meta=[ - {"key": "email", "value": request.email}, - {"key": "password", "value": request.password}, - ], - ) diff --git a/tests/e2e/authentication/test_refresh_token.py b/tests/e2e/authentication/test_refresh_token.py deleted file mode 100644 index 167e8c9..0000000 --- a/tests/e2e/authentication/test_refresh_token.py +++ /dev/null @@ -1,68 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr -from httpx import AsyncClient - -from prodik.application.interfaces.repositories import SessionRepository -from tests.factories.common import authorization_headers, generate_random_string -from tests.factories.models import UserFactory -from tests.factories.schemas import RefreshTokenRequestFactory - - -@pytest.mark.asyncio -async def test_refresh_token_ok( - transport: AsyncClient, - user_factory: UserFactory, - session_repository: SessionRepository, -) -> None: - user_factory_response = await user_factory.build() - - request = RefreshTokenRequestFactory.build( - refresh_token=user_factory_response.refresh_token - ) - - response = await transport.post( - "/auth/refresh", - headers=authorization_headers(user_factory_response.access_token), - json=request.model_dump(), - ) - content = response.json() - - session = await session_repository.get_by_host_and_user_id( - user_factory_response.user.id, "127.0.0.1" - ) - - assert response.status_code == HTTPStatus.OK - assert session == IsPartialDataclass( - user_id=str(user_factory_response.user.id), - token=content["refresh_token"], - host="127.0.0.1", - ) - assert content == IsPartialDict( - access_token=IsStr, - refresh_token=IsStr, - expires_in=IsInt, - ) - - -@pytest.mark.asyncio -async def test_refresh_token_without_active_session( - transport: AsyncClient, user_factory: UserFactory -) -> None: - user_factory_response = await user_factory.build() - - refresh_token = generate_random_string() - request = RefreshTokenRequestFactory.build(refresh_token=refresh_token) - - response = await transport.post( - "/auth/refresh", - headers=authorization_headers(user_factory_response.access_token), - json=request.model_dump(), - ) - - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json() == IsPartialDict( - detail="Session not found", - meta=[{"key": "refresh_token", "value": refresh_token}], - ) diff --git a/tests/e2e/authentication/test_register.py b/tests/e2e/authentication/test_register.py deleted file mode 100644 index e5515be..0000000 --- a/tests/e2e/authentication/test_register.py +++ /dev/null @@ -1,71 +0,0 @@ -from http import HTTPStatus - -import pytest -from dirty_equals import IsInt, IsPartialDataclass, IsPartialDict, IsStr -from httpx import AsyncClient - -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, -) -from prodik.application.interfaces.token_managers import AccessTokenManager -from tests.factories.models import UserFactory -from tests.factories.schemas import RegisterRequestFactory - - -@pytest.mark.asyncio -async def test_register_ok( - transport: AsyncClient, - session_repository: SessionRepository, - access_token_manager: AccessTokenManager, -) -> None: - request = RegisterRequestFactory.build() - - response = await transport.post("/auth/register", json=request.model_dump()) - content = response.json() - - session = await session_repository.get_by_token(content["refresh_token"]) - token_content = access_token_manager.decode(content["access_token"]) - - assert response.status_code == HTTPStatus.CREATED - - assert session == IsPartialDataclass( - user_id=token_content["user_id"], - token=content["refresh_token"], - host=IsStr, - ) - assert content == IsPartialDict( - access_token=IsStr, - refresh_token=IsStr, - expires_in=IsInt, - ) - - -@pytest.mark.asyncio -async def test_register_when_user_already_exists( - transport: AsyncClient, - user_factory: UserFactory, - local_authorization_repository: LocalAuthorizationRepository, -) -> None: - user_factory_response = await user_factory.build() - - request = RegisterRequestFactory.build( - email=user_factory_response.user.email.value, - username=user_factory_response.user.username.value, - ) - - response = await transport.post("/auth/register", json=request.model_dump()) - - local_authorizations = await local_authorization_repository.get_all_by_user_id( - user_factory_response.user.id - ) - - assert response.status_code == HTTPStatus.CONFLICT - assert len(local_authorizations) == 1 - assert response.json() == IsPartialDict( - detail="User with this username or email already exists", - meta=[ - {"key": "email", "value": user_factory_response.user.email.value}, - {"key": "username", "value": user_factory_response.user.username.value}, - ], - ) diff --git a/tests/e2e/company/test_register_company.py b/tests/e2e/company/test_register_company.py index b78e356..fba3489 100644 --- a/tests/e2e/company/test_register_company.py +++ b/tests/e2e/company/test_register_company.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict, IsStr from httpx import AsyncClient -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, RoleRepository, UserGrantRepository, diff --git a/tests/e2e/company/test_verify_company.py b/tests/e2e/company/test_verify_company.py index b3d1404..fb11883 100644 --- a/tests/e2e/company/test_verify_company.py +++ b/tests/e2e/company/test_verify_company.py @@ -3,7 +3,7 @@ import pytest from httpx import AsyncClient -from prodik.application.interfaces.repositories import CompanyRepository +from conode.application.interfaces.repositories import CompanyRepository from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, UserFactory diff --git a/tests/e2e/context/test_create_context.py b/tests/e2e/context/test_create_context.py index fadf6b8..26a0655 100644 --- a/tests/e2e/context/test_create_context.py +++ b/tests/e2e/context/test_create_context.py @@ -4,8 +4,8 @@ from dirty_equals import IsPartialDataclass, IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import ContextRepository -from prodik.domain.context import ContextDescription, ContextName +from conode.application.interfaces.repositories import ContextRepository +from conode.domain.context import ContextDescription, ContextName from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, UserFactory from tests.factories.schemas import CreateContextRequestFactory diff --git a/tests/e2e/context/test_delete_context.py b/tests/e2e/context/test_delete_context.py index e608156..9e3390a 100644 --- a/tests/e2e/context/test_delete_context.py +++ b/tests/e2e/context/test_delete_context.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import ContextRepository +from conode.application.interfaces.repositories import ContextRepository from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, ContextFactory, UserFactory diff --git a/tests/e2e/edge/test_create_edge.py b/tests/e2e/edge/test_create_edge.py index 6676ced..149c4fb 100644 --- a/tests/e2e/edge/test_create_edge.py +++ b/tests/e2e/edge/test_create_edge.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDataclass, IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import EdgeRepository +from conode.application.interfaces.repositories import EdgeRepository from tests.factories.common import authorization_headers from tests.factories.models import ( CompanyFactory, diff --git a/tests/e2e/edge/test_delete_edge.py b/tests/e2e/edge/test_delete_edge.py index f86811e..3f79c8d 100644 --- a/tests/e2e/edge/test_delete_edge.py +++ b/tests/e2e/edge/test_delete_edge.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import EdgeRepository +from conode.application.interfaces.repositories import EdgeRepository from tests.factories.common import authorization_headers from tests.factories.models import ( CompanyFactory, diff --git a/tests/e2e/edge/test_manipulate_edge_weight.py b/tests/e2e/edge/test_manipulate_edge_weight.py index 29f1184..1db7c98 100644 --- a/tests/e2e/edge/test_manipulate_edge_weight.py +++ b/tests/e2e/edge/test_manipulate_edge_weight.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDataclass, IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import EdgeRepository +from conode.application.interfaces.repositories import EdgeRepository from tests.factories.common import authorization_headers from tests.factories.models import ( CompanyFactory, diff --git a/tests/e2e/group/test_create_group.py b/tests/e2e/group/test_create_group.py index 0d46f5a..a9e0522 100644 --- a/tests/e2e/group/test_create_group.py +++ b/tests/e2e/group/test_create_group.py @@ -5,7 +5,7 @@ from dirty_equals import IsPartialDict, IsStr from httpx import AsyncClient -from prodik.application.interfaces.repositories import GroupRepository +from conode.application.interfaces.repositories import GroupRepository from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, GroupFactory, UserFactory from tests.factories.schemas import CreateGroupRequestFactory diff --git a/tests/e2e/group/test_delete_group.py b/tests/e2e/group/test_delete_group.py index 8cb99f7..c85ebe2 100644 --- a/tests/e2e/group/test_delete_group.py +++ b/tests/e2e/group/test_delete_group.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( GroupRepository, NodeAssociationRepository, ) diff --git a/tests/e2e/helpers/asserts.py b/tests/e2e/helpers/asserts.py index 469c5cb..4ef436e 100644 --- a/tests/e2e/helpers/asserts.py +++ b/tests/e2e/helpers/asserts.py @@ -1,12 +1,12 @@ from typing import Any -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, RoleRepository, UserGrantRepository, ) -from prodik.domain.role import RoleName -from prodik.domain.user import User +from conode.domain.role import RoleName +from conode.domain.user import User async def assert_user_have_correct_rights( diff --git a/tests/e2e/node/test_attach_node.py b/tests/e2e/node/test_attach_node.py index 072bf70..c2d390f 100644 --- a/tests/e2e/node/test_attach_node.py +++ b/tests/e2e/node/test_attach_node.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( NodeAssociationRepository, ) from tests.factories.common import authorization_headers diff --git a/tests/e2e/node/test_create_node.py b/tests/e2e/node/test_create_node.py index ff4f822..e295399 100644 --- a/tests/e2e/node/test_create_node.py +++ b/tests/e2e/node/test_create_node.py @@ -4,11 +4,11 @@ from dirty_equals import IsPartialDataclass, IsPartialDict, IsStr from httpx import AsyncClient -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( NodeAssociationRepository, NodeRepository, ) -from prodik.domain.node import NodeDescription, NodeName +from conode.domain.node import NodeDescription, NodeName from tests.factories.common import authorization_headers from tests.factories.models import CompanyFactory, GroupFactory, UserFactory from tests.factories.schemas import CreateNodeRequestFactory diff --git a/tests/e2e/node/test_delete_node.py b/tests/e2e/node/test_delete_node.py index b967c29..1251b02 100644 --- a/tests/e2e/node/test_delete_node.py +++ b/tests/e2e/node/test_delete_node.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( NodeAssociationRepository, NodeRepository, ) diff --git a/tests/e2e/node/test_detach_node.py b/tests/e2e/node/test_detach_node.py index f651b87..d5db0bf 100644 --- a/tests/e2e/node/test_detach_node.py +++ b/tests/e2e/node/test_detach_node.py @@ -4,7 +4,7 @@ from dirty_equals import IsPartialDataclass, IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import NodeAssociationRepository +from conode.application.interfaces.repositories import NodeAssociationRepository from tests.factories.common import authorization_headers from tests.factories.models import ( CompanyFactory, diff --git a/tests/e2e/node/test_update_node.py b/tests/e2e/node/test_update_node.py index 020b9cc..3a3a504 100644 --- a/tests/e2e/node/test_update_node.py +++ b/tests/e2e/node/test_update_node.py @@ -4,8 +4,8 @@ from dirty_equals import IsPartialDataclass, IsPartialDict from httpx import AsyncClient -from prodik.application.interfaces.repositories import NodeRepository -from prodik.domain.node import NodeDescription, NodeName +from conode.application.interfaces.repositories import NodeRepository +from conode.domain.node import NodeDescription, NodeName from tests.factories.common import authorization_headers from tests.factories.models import ( CompanyFactory, diff --git a/tests/e2e/user/test_update_current_profile.py b/tests/e2e/user/test_update_current_profile.py index d45fe3a..d807b42 100644 --- a/tests/e2e/user/test_update_current_profile.py +++ b/tests/e2e/user/test_update_current_profile.py @@ -4,8 +4,8 @@ from dirty_equals import IsPartialDataclass from httpx import AsyncClient -from prodik.application.interfaces.repositories import UserRepository -from prodik.domain.user import Bio, Email, FirstName, LastName, Username +from conode.application.interfaces.repositories import UserRepository +from conode.domain.user import Bio, FirstName, LastName, Username from tests.factories.common import authorization_headers from tests.factories.models import UserFactory from tests.factories.schemas import ( @@ -35,6 +35,5 @@ async def test_update_current_user_profile_ok( first_name=FirstName(request.first_name), last_name=LastName(request.last_name), username=Username(request.username), - email=Email(request.email), bio=Bio(request.bio), ) diff --git a/tests/factories/common.py b/tests/factories/common.py index 13e36e0..ec5c616 100644 --- a/tests/factories/common.py +++ b/tests/factories/common.py @@ -29,4 +29,4 @@ def generate_random_ip() -> str: def authorization_headers(access_token: str) -> dict[str, str]: - return {"Authorization": f"Bearer {access_token}"} + return {"X-Forwarded-Access-Token": access_token} diff --git a/tests/factories/models/company.py b/tests/factories/models/company.py index c966a43..a3d94c0 100644 --- a/tests/factories/models/company.py +++ b/tests/factories/models/company.py @@ -1,18 +1,18 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( CompanyRepository, RolePermissionsRepository, RoleRepository, UserGrantRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import RoleManagmentService -from prodik.domain.company import Company, CompanyId -from prodik.domain.grant import UserGrant, UserGrantId -from prodik.domain.role import EntityType, PermissionType -from prodik.domain.user import User +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import RoleManagmentService +from conode.domain.company import Company, CompanyId +from conode.domain.grant import UserGrant, UserGrantId +from conode.domain.role import EntityType, PermissionType +from conode.domain.user import User from tests.factories.common import generate_random_string diff --git a/tests/factories/models/context.py b/tests/factories/models/context.py index 831d464..7f314a2 100644 --- a/tests/factories/models/context.py +++ b/tests/factories/models/context.py @@ -1,10 +1,10 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ContextRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.company import Company -from prodik.domain.context import Context, ContextId +from conode.application.interfaces.repositories import ContextRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.company import Company +from conode.domain.context import Context, ContextId from tests.factories.common import generate_random_string diff --git a/tests/factories/models/edge.py b/tests/factories/models/edge.py index 09b2b58..4de9b25 100644 --- a/tests/factories/models/edge.py +++ b/tests/factories/models/edge.py @@ -1,12 +1,12 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import EdgeRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.company import Company -from prodik.domain.context import Context -from prodik.domain.edge import Edge, EdgeId -from prodik.domain.node import Node +from conode.application.interfaces.repositories import EdgeRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.company import Company +from conode.domain.context import Context +from conode.domain.edge import Edge, EdgeId +from conode.domain.node import Node @dataclass diff --git a/tests/factories/models/group.py b/tests/factories/models/group.py index c7b2ec7..f10d37b 100644 --- a/tests/factories/models/group.py +++ b/tests/factories/models/group.py @@ -1,10 +1,10 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import GroupRepository -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.company import Company -from prodik.domain.group import Group, GroupId +from conode.application.interfaces.repositories import GroupRepository +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.company import Company +from conode.domain.group import Group, GroupId from tests.factories.common import generate_random_string diff --git a/tests/factories/models/node.py b/tests/factories/models/node.py index 2cfae04..9a1cccf 100644 --- a/tests/factories/models/node.py +++ b/tests/factories/models/node.py @@ -1,14 +1,14 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( NodeAssociationRepository, NodeRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.company import Company -from prodik.domain.group import Group -from prodik.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.company import Company +from conode.domain.group import Group +from conode.domain.node import Node, NodeAssociation, NodeAssociationId, NodeId from tests.factories.common import generate_random_string diff --git a/tests/factories/models/role.py b/tests/factories/models/role.py index 40e0303..424a01f 100644 --- a/tests/factories/models/role.py +++ b/tests/factories/models/role.py @@ -1,18 +1,18 @@ from dataclasses import dataclass -from prodik.application.interfaces.repositories import ( +from conode.application.interfaces.repositories import ( RolePermissionsRepository, RoleRepository, ) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.application.services import ( +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.application.services import ( RoleManagmentService, RoleManagmentServiceResponse, ) -from prodik.domain.company import Company -from prodik.domain.context import Context -from prodik.domain.group import Group -from prodik.domain.role import EntityType, PermissionType +from conode.domain.company import Company +from conode.domain.context import Context +from conode.domain.group import Group +from conode.domain.role import EntityType, PermissionType from tests.factories.common import generate_random_string entity_types_map = { diff --git a/tests/factories/models/user.py b/tests/factories/models/user.py index 8d9ae90..06ea098 100644 --- a/tests/factories/models/user.py +++ b/tests/factories/models/user.py @@ -1,24 +1,11 @@ from dataclasses import dataclass from uuid import uuid4 -from prodik.application.interfaces.password_hasher import PasswordHasher -from prodik.application.interfaces.repositories import ( - LocalAuthorizationRepository, - SessionRepository, - UserRepository, -) -from prodik.application.interfaces.token_managers import ( - AccessTokenManager, - RefreshTokenManager, -) -from prodik.application.interfaces.transaction_manager import TransactionManager -from prodik.domain.authorization import ( - LocalAuthorization, - LocalAuthorizationId, - Session, - SessionId, -) -from prodik.domain.user import User, UserId, UserSystemRole +from conode.application.interfaces.password_hasher import PasswordHasher +from conode.application.interfaces.repositories import UserRepository +from conode.application.interfaces.token_manager import TokenManager +from conode.application.interfaces.transaction_manager import TransactionManager +from conode.domain.user import User, UserId, UserSystemRole from tests.factories.common import generate_random_string @@ -26,17 +13,12 @@ class UserFactoryResponse: user: User access_token: str - refresh_token: str - password: str @dataclass class UserFactory: - local_authorization_repository: LocalAuthorizationRepository - refresh_token_manager: RefreshTokenManager - access_token_manager: AccessTokenManager + token_manager: TokenManager transaction_manager: TransactionManager - session_repository: SessionRepository user_repository: UserRepository password_hasher: PasswordHasher @@ -60,31 +42,9 @@ async def build(self, *, admin: bool = False) -> UserFactoryResponse: if admin: user.system_role = UserSystemRole.ADMIN - access_token = self.access_token_manager.encode(user) - refresh_token = self.refresh_token_manager.encode() - password = generate_random_string() - hashed_password = self.password_hasher.hash(password) - - authorization = LocalAuthorization.new( - local_authorization_id=LocalAuthorizationId(uuid4()), - password=hashed_password, - user=user, - ) - - session = Session.new( - session_id=SessionId(uuid4()), - host="127.0.0.1", - token=refresh_token, - user=user, - ) - await self.user_repository.create(user) - await self.local_authorization_repository.create(authorization) - await self.session_repository.create(session) return UserFactoryResponse( user=user, - access_token=access_token.token, - refresh_token=refresh_token, - password=password, + access_token=self.token_manager.encode(user), ) diff --git a/tests/factories/schemas/__init__.py b/tests/factories/schemas/__init__.py index b5c7332..aa4078b 100644 --- a/tests/factories/schemas/__init__.py +++ b/tests/factories/schemas/__init__.py @@ -1,8 +1,3 @@ -from .authentication import ( - LoginRequestFactory, - RefreshTokenRequestFactory, - RegisterRequestFactory, -) from .company import RegisterCompanyRequestFactory from .context import CreateContextRequestFactory from .edge import CreateEdgeRequestFactory @@ -13,7 +8,6 @@ UpdateNodeRequestFactory, ) from .user import ( - UpdateCurrentUserPasswordRequestFactory, UpdateCurrentUserProfileRequestFactory, ) @@ -23,11 +17,7 @@ "CreateEdgeRequestFactory", "CreateGroupRequestFactory", "CreateNodeRequestFactory", - "LoginRequestFactory", - "RefreshTokenRequestFactory", "RegisterCompanyRequestFactory", - "RegisterRequestFactory", - "UpdateCurrentUserPasswordRequestFactory", "UpdateCurrentUserProfileRequestFactory", "UpdateNodeRequestFactory", ) diff --git a/tests/factories/schemas/authentication.py b/tests/factories/schemas/authentication.py deleted file mode 100644 index 4de7f4c..0000000 --- a/tests/factories/schemas/authentication.py +++ /dev/null @@ -1,17 +0,0 @@ -from polyfactory.factories.pydantic_factory import ModelFactory - -from prodik.presentation.schemas.auth import ( - LoginRequest, - RefreshTokenRequest, - RegisterRequest, -) - - -class RegisterRequestFactory(ModelFactory[RegisterRequest]): - __use_examples__ = True - - -class RefreshTokenRequestFactory(ModelFactory[RefreshTokenRequest]): ... - - -class LoginRequestFactory(ModelFactory[LoginRequest]): ... diff --git a/tests/factories/schemas/company.py b/tests/factories/schemas/company.py index 86aa767..269d2d6 100644 --- a/tests/factories/schemas/company.py +++ b/tests/factories/schemas/company.py @@ -1,6 +1,6 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.company import RegisterCompanyRequest +from conode.presentation.schemas.company import RegisterCompanyRequest class RegisterCompanyRequestFactory(ModelFactory[RegisterCompanyRequest]): diff --git a/tests/factories/schemas/context.py b/tests/factories/schemas/context.py index 280bca9..dd24a3e 100644 --- a/tests/factories/schemas/context.py +++ b/tests/factories/schemas/context.py @@ -1,6 +1,6 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.context import CreateContextRequest +from conode.presentation.schemas.context import CreateContextRequest class CreateContextRequestFactory(ModelFactory[CreateContextRequest]): diff --git a/tests/factories/schemas/edge.py b/tests/factories/schemas/edge.py index e3ef873..740e006 100644 --- a/tests/factories/schemas/edge.py +++ b/tests/factories/schemas/edge.py @@ -1,6 +1,6 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.edge import CreateEdgeRequest +from conode.presentation.schemas.edge import CreateEdgeRequest class CreateEdgeRequestFactory(ModelFactory[CreateEdgeRequest]): ... diff --git a/tests/factories/schemas/group.py b/tests/factories/schemas/group.py index a81dd88..7b34e74 100644 --- a/tests/factories/schemas/group.py +++ b/tests/factories/schemas/group.py @@ -1,6 +1,6 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.group import CreateGroupRequest +from conode.presentation.schemas.group import CreateGroupRequest class CreateGroupRequestFactory(ModelFactory[CreateGroupRequest]): diff --git a/tests/factories/schemas/node.py b/tests/factories/schemas/node.py index dcf4cfe..f84b51b 100644 --- a/tests/factories/schemas/node.py +++ b/tests/factories/schemas/node.py @@ -1,6 +1,6 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.node import ( +from conode.presentation.schemas.node import ( AttachNodeRequest, CreateNodeRequest, UpdateNodeRequest, diff --git a/tests/factories/schemas/user.py b/tests/factories/schemas/user.py index 35b29d3..4f95e88 100644 --- a/tests/factories/schemas/user.py +++ b/tests/factories/schemas/user.py @@ -1,16 +1,10 @@ from polyfactory.factories.pydantic_factory import ModelFactory -from prodik.presentation.schemas.user import ( - UpdateCurrentUserPasswordRequest, +from conode.presentation.schemas.user import ( UpdateCurrentUserProfileRequest, ) -class UpdateCurrentUserPasswordRequestFactory( - ModelFactory[UpdateCurrentUserPasswordRequest] -): ... - - class UpdateCurrentUserProfileRequestFactory( ModelFactory[UpdateCurrentUserProfileRequest] ): ... diff --git a/uv.lock b/uv.lock index 010b06b..4e2e906 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,8 @@ requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", "python_full_version == '3.14.*'", - "python_full_version < '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", ] [[package]] @@ -14,7 +15,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } @@ -45,7 +46,7 @@ name = "anyio" version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -217,6 +218,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "attrs" +version = "21.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/77/ebb15fc26d0f815839ecd897b919ed6d85c050feeb83e100e020df9153d2/attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd", size = 201839, upload-time = "2021-12-29T13:15:09.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/be/7abce643bfdf8ca01c48afa2ddf8308c2308b0c3b239a44e57d020afa0ef/attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", size = 60567, upload-time = "2021-12-29T13:15:06.703Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -226,19 +253,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore", version = "5.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "stevedore", version = "5.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "black" +version = "22.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.14'" }, + { name = "pathspec", marker = "python_full_version < '3.14'" }, + { name = "platformdirs", marker = "python_full_version < '3.14'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/59/e873cc6807fb62c11131e5258ca15577a3b7452abad08dc49286cf8245e8/black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", size = 553112, upload-time = "2022-12-09T15:57:04.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d9/60852a6fc2f85374db20a9767dacfe50c2172eb8388f46018c8daf836995/black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", size = 1556665, upload-time = "2022-12-09T16:04:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/975782465cc6b514f2c972421e29b933dfbb51d4a95948a4e0e94f36ea38/black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", size = 1205632, upload-time = "2022-12-09T16:14:38.465Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/6aa02d14785c4039b38bfed6f9ee28a952b2d101c64fc97b15811fa8bd04/black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f", size = 1536577, upload-time = "2022-12-09T16:04:12.721Z" }, + { url = "https://files.pythonhosted.org/packages/4c/49/420dcfccba3215dc4e5790fa47572ef14129df1c5e95dd87b5ad30211b01/black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", size = 1209873, upload-time = "2022-12-09T16:14:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/1f7f93c0555eaf4cbb628e26ba026e3256174a45bd9397ff1ea7cf96bad5/black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", size = 167343, upload-time = "2022-12-09T15:57:02.229Z" }, +] + [[package]] name = "black" version = "26.3.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.14'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pathspec", marker = "python_full_version >= '3.14'" }, + { name = "platformdirs", marker = "python_full_version >= '3.14'" }, + { name = "pytokens", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ @@ -270,6 +339,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] +[[package]] +name = "boltons" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, +] + +[[package]] +name = "bracex" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162, upload-time = "2026-06-30T00:43:35.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738, upload-time = "2026-06-30T00:43:34.196Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -361,18 +448,138 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + [[package]] name = "click" version = "8.3.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] +[[package]] +name = "click-option-group" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -382,6 +589,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "conode" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "alembic" }, + { name = "argon2-cffi" }, + { name = "asyncpg" }, + { name = "bandit" }, + { name = "cryptography" }, + { name = "dishka" }, + { name = "fastapi" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyjwt" }, + { name = "semgrep", version = "0.112.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "semgrep", version = "1.156.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "sqlalchemy" }, + { name = "structlog" }, + { name = "uvicorn" }, + { name = "zizmor" }, +] + +[package.optional-dependencies] +dev = [ + { name = "datamodel-code-generator" }, + { name = "dirty-equals" }, + { name = "httpx" }, + { name = "mypy" }, + { name = "polyfactory" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = "==1.18.4" }, + { name = "argon2-cffi", specifier = "==25.1.0" }, + { name = "asyncpg", specifier = "==0.31.0" }, + { name = "bandit", specifier = ">=1.9.4" }, + { name = "cryptography", specifier = "==46.0.5" }, + { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.54.0" }, + { name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.11" }, + { name = "dishka", specifier = "==1.7.2" }, + { name = "fastapi", specifier = "==0.128.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "polyfactory", marker = "extra == 'dev'", specifier = "==3.3.0" }, + { name = "pydantic", extras = ["email"], specifier = "==2.13.2" }, + { name = "pyjwt", specifier = "==2.11.0" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "semgrep", specifier = ">=0.112.1" }, + { name = "sqlalchemy", specifier = "==2.0.46" }, + { name = "structlog", specifier = "==25.5.0" }, + { name = "uvicorn", specifier = "==0.40.0" }, + { name = "zizmor", specifier = ">=1.27.0" }, +] +provides-extras = ["dev"] + [[package]] name = "coverage" version = "7.14.2" @@ -482,7 +755,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -551,21 +824,32 @@ version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, - { name = "black" }, + { name = "black", version = "22.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "black", version = "26.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "genson" }, { name = "inflect" }, { name = "isort" }, { name = "jinja2" }, - { name = "packaging" }, + { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pydantic" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.12'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/43/2640cd5293fb5430528166908d6439cf3321bc1c54de5fe58ef100b143a1/datamodel_code_generator-0.54.0.tar.gz", hash = "sha256:2b183598d049e265146a8224c35d1bb96a80a641ea8ecd2a82e6a0e97b56da6b", size = 824327, upload-time = "2026-02-14T16:19:05.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/d9/fd646ea4ae48e374817b2750f5e678a5bf6e10d8924f09cf4cce86a81607/datamodel_code_generator-0.54.0-py3-none-any.whl", hash = "sha256:3156df7a7e8fa5a7c9a6d50836e5ba5abe0532f6b71eee6d73a0c8e1fb5b7e47", size = 261838, upload-time = "2026-02-14T16:19:03.298Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "dirty-equals" version = "0.11" @@ -580,7 +864,7 @@ name = "dishka" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/d7/1be31f5ef32387059190353f9fa493ff4d07a1c75fa856c7566ca45e0800/dishka-1.7.2.tar.gz", hash = "sha256:47d4cb5162b28c61bf5541860e605ed5eaf5c667122299c7ef657c86fc8d5a49", size = 68132, upload-time = "2025-09-24T21:23:05.135Z" } wheels = [ @@ -609,12 +893,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -630,6 +930,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "face" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, +] + [[package]] name = "faker" version = "40.18.0" @@ -666,6 +978,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, ] +[[package]] +name = "glom" +version = "22.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "boltons", marker = "python_full_version < '3.14'" }, + { name = "face", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/69432deefa6f5283ec75b246d0540097ae26f618b915519ee3824c4c5dd6/glom-22.1.0.tar.gz", hash = "sha256:1510c6587a8f9c64a246641b70033cbc5ebde99f02ad245693678038e821aeb5", size = 189738, upload-time = "2022-01-24T09:34:04.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e8/68e274b2a30e1fdfd25bdc27194382be3f233929c8f727c0440d58ac074f/glom-22.1.0-py2.py3-none-any.whl", hash = "sha256:5339da206bf3532e01a83a35aca202960ea885156986d190574b779598e9e772", size = 100687, upload-time = "2022-01-24T09:34:02.391Z" }, +] + +[[package]] +name = "glom" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +dependencies = [ + { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "boltons", marker = "python_full_version >= '3.14'" }, + { name = "face", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + [[package]] name = "greenlet" version = "3.5.0" @@ -729,92 +1089,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "hiredis" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/97/d6/9bef6dc3052c168c93fbf7e6c0f2b12c45f0f741a2d30fd919096774343a/hiredis-3.3.1.tar.gz", hash = "sha256:da6f0302360e99d32bc2869772692797ebadd536e1b826d0103c72ba49d38698", size = 89101, upload-time = "2026-03-16T15:21:08.092Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d0/fe8d86b94a9c70100c4a9402d301422515aec4055b982b8c189f0c94a677/hiredis-3.3.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f525734382a47f9828c9d6a1501522c78d5935466d8e2be1a41ba40ca5bb922b", size = 81825, upload-time = "2026-03-16T15:19:05.397Z" }, - { url = "https://files.pythonhosted.org/packages/80/76/88edf234200e9592c19f0de7d7af37092151524553b9c29a029f81fe7c9f/hiredis-3.3.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:6e2e1024f0a021777740cb7c633a0efb2c4a4bc570f508223a8dcbcf79f99ef9", size = 46043, upload-time = "2026-03-16T15:19:06.928Z" }, - { url = "https://files.pythonhosted.org/packages/10/13/433c4dada704c7f1b1b8261e713483b4cdfa462b1fced1a910ca173c1832/hiredis-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1d68c6980d4690a4550bd3db6c03146f7be68ef5d08d38bb1fb68b3e9c32fe3", size = 41818, upload-time = "2026-03-16T15:19:08.423Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/1327de61e27e42ff11d3956690be9fa59899d8112a132a213f1350fe2ee9/hiredis-3.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0caf3fc8af0767794b335753781c3fa35f2a3e975c098edbc8f733d35d6a95e4", size = 167062, upload-time = "2026-03-16T15:19:09.601Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bf/b49485c0c4fc088875dc45cb7b86e5e703e0daebbd726f872d4c275fab0e/hiredis-3.3.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81a1669b6631976b1dc9d3d58ed1ab3333e9f52feb91a2a1fb8241101ac3b665", size = 178928, upload-time = "2026-03-16T15:19:10.592Z" }, - { url = "https://files.pythonhosted.org/packages/91/55/3d111ba2843d1d35786508bdfe0ce348fbe54ad3b3553ae70d22cd02a4d0/hiredis-3.3.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8139e9011117822391c5bcfd674c5948fb1e4b8cb9adf6f13d9890859ee3a1a", size = 176734, upload-time = "2026-03-16T15:19:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c6/b54c15a80c6192c0f7b518b2cbdc0c0b713c02428ba3099cba5dc2023e40/hiredis-3.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:042e57de8a2cae91e3e7c0af32960ea2c5107b2f27f68a740295861e68780a8a", size = 168797, upload-time = "2026-03-16T15:19:12.851Z" }, - { url = "https://files.pythonhosted.org/packages/1e/44/9aa4933c6dcce96ad6d3a598a742cf4bec383563b20a45a90a5d78dcc10a/hiredis-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:65f6ac06a9f0c32c254660ec6a9329d81d589e8f5d0a9837a941d5424a6be1ef", size = 163476, upload-time = "2026-03-16T15:19:13.83Z" }, - { url = "https://files.pythonhosted.org/packages/93/f9/810dce091dcf937e5850f2fb03de18d1cd2f4f75d75a0cdc375c333917d9/hiredis-3.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:002fc0201b9af1cc8960e27cdc501ad1f8cdd6dbadb2091c6ddbd4e5ace6cb77", size = 174187, upload-time = "2026-03-16T15:19:15.198Z" }, - { url = "https://files.pythonhosted.org/packages/b7/5c/9ccc16da757d9be131809a9b33022bbe95a6afcdf9dc9694756b8ebd70e6/hiredis-3.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9ebae74ce2b977c2fcb22d6a10aa0acb730022406977b2bcb6ddd6788f5c414a", size = 167144, upload-time = "2026-03-16T15:19:16.352Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/0fb0655601cd94db6b2445494528a23915b111d142db6f6d2a7ef741c646/hiredis-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a52b24cd710690c4a7e191c7e300136ad2ecb3c68ffe7e95b598e76de166e5e", size = 164898, upload-time = "2026-03-16T15:19:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/b4/91/f53da281112f469fe8b7eb5999b4c5e5f683b79fee797dbee7e88a697b14/hiredis-3.3.1-cp310-cp310-win32.whl", hash = "sha256:1ebc307a87b099d0877dbd2bdc0bae427258e7ec67f60a951e89027f8dc2568f", size = 20397, upload-time = "2026-03-16T15:19:18.458Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a3/bc6f97c215cedd0c15d9cda91358db492767f5257824cc0c7af6a64f1c38/hiredis-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:62cc62284541bb2a86c898c7d5e8388661cade91c184cb862095ed547e80588f", size = 22331, upload-time = "2026-03-16T15:19:19.277Z" }, - { url = "https://files.pythonhosted.org/packages/8b/71/b8e7da87ff0a270e086670da46732ff8e0af2fb4042afe1486846cf44ea7/hiredis-3.3.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:26f899cde0279e4b7d370716ff80320601c2bd93cdf3e774a42bdd44f65b41f8", size = 81823, upload-time = "2026-03-16T15:19:20.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/e0/8bdafc6251aada93c670eb1893335bb248e10faa784f54de6b9384c7d2cb/hiredis-3.3.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a2f049c3f3c83e886cd1f53958e2a1ebb369be626bef9e50d8b24d79864f1df6", size = 46043, upload-time = "2026-03-16T15:19:21.292Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e8/48e5eee6dffb2d5659f437231341bfbf00c53d9fdb5d069ea629f1b2fa96/hiredis-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f316cf2d0558f5027aab19dde7d7e4901c26c21fa95367bc37784e8f547bbf2", size = 41813, upload-time = "2026-03-16T15:19:22.404Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e0/8dcd593db6d0e91cd797fafc565995cd28bd9d7ae85807c820b5e245ab82/hiredis-3.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03baa381964b8df356d19ec4e3a6ae656044249a87b0def257fe1e08dbaf6094", size = 167570, upload-time = "2026-03-16T15:19:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/76/e5/e2d75ecc15db51117ebd260fab4059b8a4cbbf74eb89c407c6d437bd6413/hiredis-3.3.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304481241e081bc26f0778b2c2b99f9c43917e4e724a016dcc9439b7ab12c726", size = 179373, upload-time = "2026-03-16T15:19:24.739Z" }, - { url = "https://files.pythonhosted.org/packages/86/9a/4fafde37b86f70125bcd01e8af5e9f448fc99f4116db6d0e9ad214fc688a/hiredis-3.3.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8597c35c9e82f65fd5897c4a2188c65d7daf10607b102960137b23d261cd957b", size = 177501, upload-time = "2026-03-16T15:19:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/a3/73/413a17d6926c015683a608c148862f1dc7e8ad6f5c205b626607be9b9ddf/hiredis-3.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad940dc2db545dc978cb41cb9a683e2ff328f3ef581230b9ca40ff6c3d01d542", size = 169446, upload-time = "2026-03-16T15:19:27.35Z" }, - { url = "https://files.pythonhosted.org/packages/e3/39/aa8e41d5f728dfa99f2236c1176a6b348c7577fd68ca9960d20d251d3b29/hiredis-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:156be6a0c736ee145cfe0fb155d0e96cec8d4872cf8b4f76ad6a2ee6ab391d0a", size = 164009, upload-time = "2026-03-16T15:19:28.426Z" }, - { url = "https://files.pythonhosted.org/packages/c4/37/85a609a2cf2b6354749bcef8f488c3298976358601cb4906bbaf2eb53944/hiredis-3.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:583de2f16528e66081cbdfe510d8488c2de73039dc00aada7d22bd49d73a4a94", size = 174623, upload-time = "2026-03-16T15:19:29.466Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5e/75e0a76e4c9021f9914cfa1de8d98cff4acd0a0eb3344d31f43c02ec9375/hiredis-3.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c24c1460486b6b36083252c2db21a814becf8495ccd0e76b7286623e37239b63", size = 167649, upload-time = "2026-03-16T15:19:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/1212138ee61e9b72d3f561da60cf6dc15031c10117735938ac258613803f/hiredis-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a58a58cef0d911b1717154179a9ff47852249c536ea5966bde4370b6b20638ff", size = 165451, upload-time = "2026-03-16T15:19:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/46/36/cd776ef13b44afbb86c3d63c1a76b09d54cb1b545cce9e26fcd439d69606/hiredis-3.3.1-cp311-cp311-win32.whl", hash = "sha256:e0db44cf81e4d7b94f3776b9f89111f74ed6bbdbfd42a22bc4a5ce0644d3e060", size = 20399, upload-time = "2026-03-16T15:19:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/df/0e/5b2a73bea6d18e7ebda7ed73520854cdc176ba70a945bd541bdeeb3f8caa/hiredis-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:1f7bceb03a1b934872ffe3942eaeed7c7e09096e67b53f095b81f39c7a819113", size = 22336, upload-time = "2026-03-16T15:19:33.238Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/1a7d925d886211948ab9cca44221b1d9dd4d3481d015511e98794e37d369/hiredis-3.3.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:60543f3b068b16a86e99ed96b7fdae71cdc1d8abdfe9b3f82032a555e52ece7e", size = 82023, upload-time = "2026-03-16T15:19:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/13/2f/a6017fe1db47cd63a4aefc0dd21dd4dcb0c4e857bfbcfaa27329745f24a3/hiredis-3.3.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2611bfaaadc5e8d43fb7967f9bbf1110c8beaa83aee2f2d812c76f11cfb56c6a", size = 46215, upload-time = "2026-03-16T15:19:35.068Z" }, - { url = "https://files.pythonhosted.org/packages/77/4b/35a71d088c6934e162aa81c7e289fa3110a3aca84ab695d88dbd488c74a2/hiredis-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e3754ce60e1b11b0afad9a053481ff184d2ee24bea47099107156d1b84a84aa", size = 41861, upload-time = "2026-03-16T15:19:36.32Z" }, - { url = "https://files.pythonhosted.org/packages/1f/54/904bc723a95926977764fefd6f0d46067579bac38fffc32b806f3f2c05c0/hiredis-3.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e89dabf436ee79b358fd970dcbed6333a36d91db73f27069ca24a02fb138a404", size = 170196, upload-time = "2026-03-16T15:19:37.274Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/4e840cd4cb53c28578234708b08fb9ec9e41c2880acc0e269a7264e1b3af/hiredis-3.3.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f7e242eab698ad0be5a4b2ec616fa856569c57455cc67c625fd567726290e5f", size = 181808, upload-time = "2026-03-16T15:19:38.637Z" }, - { url = "https://files.pythonhosted.org/packages/87/0d/fc845f06f8203ab76c401d4d2b97f9fb768e644b053a40f441f7dcc71f2d/hiredis-3.3.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53148a4e21057541b6d8e493b2ea1b500037ddf34433c391970036f3cbce00e3", size = 180577, upload-time = "2026-03-16T15:19:39.749Z" }, - { url = "https://files.pythonhosted.org/packages/52/3a/859afe2620666bf6d58eb977870c47d98af4999d473b50528b323918f3f7/hiredis-3.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25132902d3eff38781e0d54f27a0942ec849e3c07dbdce83c4d92b7e43c8dce", size = 172507, upload-time = "2026-03-16T15:19:40.87Z" }, - { url = "https://files.pythonhosted.org/packages/60/a8/004349708ad8bf0d188d46049f846d3fe2d4a7a8d0d5a6a8ba024017d8b3/hiredis-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3fb6573efa15a29c12c0c0f7170b14e7c1347fe4bb39b6a15b779f46015cc929", size = 166339, upload-time = "2026-03-16T15:19:41.912Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/bfc6df29381830c99bfd9e97ed3b6d75d9303866a28c23d51ab8c50f63e3/hiredis-3.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:487658e1db83c1ee9fbbac6a43039ea76957767a5987ffb16b590613f9e68297", size = 176766, upload-time = "2026-03-16T15:19:42.981Z" }, - { url = "https://files.pythonhosted.org/packages/53/e7/f54aaad4559a413ec8b1043a89567a5a1f898426e4091b9af5e0f2120371/hiredis-3.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a1d190790ee39b8b7adeeb10fc4090dc4859eb4e75ed27bd8108710eef18f358", size = 170313, upload-time = "2026-03-16T15:19:44.082Z" }, - { url = "https://files.pythonhosted.org/packages/60/51/b80394db4c74d4cba342fa4208f690a2739c16f1125c2a62ba1701b8e2b7/hiredis-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a42c7becd4c9ec4ab5769c754eb61112777bdc6e1c1525e2077389e193b5f5aa", size = 167964, upload-time = "2026-03-16T15:19:45.237Z" }, - { url = "https://files.pythonhosted.org/packages/47/ef/5e438d1e058be57cdc1bafc1b1ec8ab43cc890c61447e88f8b878a0e32c3/hiredis-3.3.1-cp312-cp312-win32.whl", hash = "sha256:17ec8b524055a88b80d76c177dbbbe475a25c17c5bf4b67bdbdbd0629bcae838", size = 20532, upload-time = "2026-03-16T15:19:46.233Z" }, - { url = "https://files.pythonhosted.org/packages/e9/c6/39994b9c5646e7bf7d5e92170c07fd5f224ae9f34d95ff202f31845eb94b/hiredis-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:0fac4af8515e6cca74fc701169ae4dc9a71a90e9319c9d21006ec9454b43aa2f", size = 22381, upload-time = "2026-03-16T15:19:47.082Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4b/c7f4d6d6643622f296395269e24b02c69d4ac72822f052b8cae16fa3af03/hiredis-3.3.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:afe3c3863f16704fb5d7c2c6ff56aaf9e054f6d269f7b4c9074c5476178d1aba", size = 82027, upload-time = "2026-03-16T15:19:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/9b/45/198be960a7443d6eb5045751e929480929c0defbca316ce1a47d15187330/hiredis-3.3.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:f19ee7dc1ef8a6497570d91fa4057ba910ad98297a50b8c44ff37589f7c89d17", size = 46220, upload-time = "2026-03-16T15:19:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a4/6ab925177f289830008dbe1488a9858675e2e234f48c9c1653bd4d0eaddc/hiredis-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09f5e510f637f2c72d2a79fb3ad05f7b6211e057e367ca5c4f97bb3d8c9d71f4", size = 41858, upload-time = "2026-03-16T15:19:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c8/a0ddbb9e9c27fcb0022f7b7e93abc75727cb634c6a5273ca5171033dac78/hiredis-3.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b46e96b50dad03495447860510daebd2c96fd44ed25ba8ccb03e9f89eaa9d34", size = 170095, upload-time = "2026-03-16T15:19:51.216Z" }, - { url = "https://files.pythonhosted.org/packages/94/06/618d509cc454912028f71995f3dd6eb54606f0aa8163ff79c5b7ec1f2bda/hiredis-3.3.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b4fe7f38aa8956fcc1cea270e62601e0e11066aff78e384be70fd283d30293b6", size = 181745, upload-time = "2026-03-16T15:19:52.72Z" }, - { url = "https://files.pythonhosted.org/packages/06/14/75b2deb62a61fc75a41ce1a6a781fe239133bbc88fef404d32a148ad152a/hiredis-3.3.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b96da7e365d6488d2a75266a662cbe3cc14b28c23dd9b0c9aa04b5bc5c20192", size = 180465, upload-time = "2026-03-16T15:19:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8c/8e03dcbfde8e2ca3f880fce06ad0877b3f098ed5fdfb17cf3b821a32323a/hiredis-3.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52d5641027d6731bc7b5e7d126a5158a99784a9f8c6de3d97ca89aca4969e9f8", size = 172419, upload-time = "2026-03-16T15:19:54.959Z" }, - { url = "https://files.pythonhosted.org/packages/03/05/843005d68403a3805309075efc6638360a3ababa6cb4545163bf80c8e7f7/hiredis-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eddeb9a153795cf6e615f9f3cef66a1d573ff3b6ee16df2b10d1d1c2f2baeaa8", size = 166398, upload-time = "2026-03-16T15:19:56.36Z" }, - { url = "https://files.pythonhosted.org/packages/f5/23/abe2476244fd792f5108009ec0ae666eaa5b2165ca19f2e86638d8324ac9/hiredis-3.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:011a9071c3df4885cac7f58a2623feac6c8e2ad30e6ba93c55195af05ce61ff5", size = 176844, upload-time = "2026-03-16T15:19:57.462Z" }, - { url = "https://files.pythonhosted.org/packages/c6/47/e1cdccc559b98e548bcff0868c3938d375663418c0adca465895ee1f72e7/hiredis-3.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:264ee7e9cb6c30dc78da4ecf71d74cf14ca122817c665d838eda8b4384bce1b0", size = 170366, upload-time = "2026-03-16T15:19:58.548Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e1/fda8325f51d06877e8e92500b15d4aff3855b4c3c91dbd9636a82e4591f2/hiredis-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d1434d0bcc1b3ef048bae53f26456405c08aeed9827e65b24094f5f3a6793f1", size = 168023, upload-time = "2026-03-16T15:19:59.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/21/2839d1625095989c116470e2b6841bbe1a2a5509585e82a4f3f5cd47f511/hiredis-3.3.1-cp313-cp313-win32.whl", hash = "sha256:f915a34fb742e23d0d61573349aa45d6f74037fde9d58a9f340435eff8d62736", size = 20535, upload-time = "2026-03-16T15:20:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/84/f9/534c2a89b24445a9a9623beb4697fd72b8c8f16286f6f3bda012c7af004a/hiredis-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:d8e56e0d1fe607bfff422633f313aec9191c3859ab99d11ff097e3e6e068000c", size = 22383, upload-time = "2026-03-16T15:20:01.865Z" }, - { url = "https://files.pythonhosted.org/packages/03/72/0450d6b449da58120c5497346eb707738f8f67b9e60c28a8ef90133fc81f/hiredis-3.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:439f9a5cc8f9519ce208a24cdebfa0440fef26aa682a40ba2c92acb10a53f5e0", size = 82112, upload-time = "2026-03-16T15:20:02.865Z" }, - { url = "https://files.pythonhosted.org/packages/22/c0/0be33a29bcd463e6cbb0282515dd4d0cdfe33c30c7afc6d4d8c460e23266/hiredis-3.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3724f0e58c6ff76fd683429945491de71324ab1bc0ad943a8d68cb0932d24075", size = 46238, upload-time = "2026-03-16T15:20:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/f999854bfaf3bcbee0f797f24706c182ecfaca825f6a582f6281a6aa97e0/hiredis-3.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29fe35e3c6fe03204e75c86514f452591957a1e06b05d86e10d795455b71c355", size = 41891, upload-time = "2026-03-16T15:20:04.939Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/cd9ab90fec3a301d864d8ab6167aea387add8e2287969d89cbcd45d6b0e0/hiredis-3.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d42f3a13290f89191568fc113d95a3d2c8759cdd8c3672f021d8b7436f909e75", size = 170485, upload-time = "2026-03-16T15:20:06.284Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9a/1ddf9ea236a292963146cbaf6722abeb9d503ca47d821267bb8b3b81c4f7/hiredis-3.3.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2afc675b831f7552da41116fffffca4340f387dc03f56d6ec0c7895ab0b59a10", size = 182030, upload-time = "2026-03-16T15:20:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b8/e070a1dbf8a1bbb8814baa0b00836fbe3f10c7af8e11f942cc739c64e062/hiredis-3.3.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4106201cd052d9eabe3cb7b5a24b0fe37307792bda4fcb3cf6ddd72f697828e8", size = 180543, upload-time = "2026-03-16T15:20:09.096Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bb/b5f4f98e44626e2446cd8a52ce6cb1fc1c99786b6e2db3bf09cea97b90cd/hiredis-3.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8887bf0f31e4b550bd988c8863b527b6587d200653e9375cd91eea2b944b7424", size = 172356, upload-time = "2026-03-16T15:20:10.245Z" }, - { url = "https://files.pythonhosted.org/packages/ef/93/73a77b54ba94e82f76d02563c588d8a062513062675f483a033a43015f2c/hiredis-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ac7697365dbe45109273b34227fee6826b276ead9a4a007e0877e1d3f0fcf21", size = 166433, upload-time = "2026-03-16T15:20:11.789Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c2/1b2dcbe5dc53a46a8cb05bed67d190a7e30bad2ad1f727ebe154dfeededd/hiredis-3.3.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2b6da6e07359107c653a809b3cff2d9ccaeedbafe33c6f16434aef6f53ce4a2b", size = 177220, upload-time = "2026-03-16T15:20:12.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/09/f4314cf096552568b5ea785ceb60c424771f4d35a76c410ad39d258f74bc/hiredis-3.3.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ce334915f5d31048f76a42c607bf26687cf045eb1bc852b7340f09729c6a64fc", size = 170475, upload-time = "2026-03-16T15:20:14.519Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/3f56e438efc8fc27ed4a3dbad58c0280061466473ec35d8f86c90c841a84/hiredis-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee11fd431f83d8a5b29d370b9d79a814d3218d30113bdcd44657e9bdf715fc92", size = 167913, upload-time = "2026-03-16T15:20:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/56/34/053e5ee91d6dc478faac661996d1fd4886c5acb7a1b5ac30e7d3c794bb51/hiredis-3.3.1-cp314-cp314-win32.whl", hash = "sha256:e0356561b4a97c83b9ee3de657a41b8d1a1781226853adaf47b550bb988fda6f", size = 21167, upload-time = "2026-03-16T15:20:17.013Z" }, - { url = "https://files.pythonhosted.org/packages/ea/33/06776c641d17881a9031e337e81b3b934c38c2adbb83c85062d6b5f83b72/hiredis-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:80aba5f85d6227faee628ae28d1c3b69c661806a0636548ac56c68782606454f", size = 23000, upload-time = "2026-03-16T15:20:17.966Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5a/94f9a505b2ff5376d4a05fb279b69d89bafa7219dd33f6944026e3e56f80/hiredis-3.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:907f7b5501a534030738f0f27459a612d2266fd0507b007bb8f3e6de08167920", size = 83039, upload-time = "2026-03-16T15:20:19.316Z" }, - { url = "https://files.pythonhosted.org/packages/93/ae/d3752a8f03a1fca43d402389d2a2d234d3db54c4d1f07f26c1041ca3c5de/hiredis-3.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:de94b409f49eb6a588ebdd5872e826caec417cd77c17af0fb94f2128427f1a2a", size = 46703, upload-time = "2026-03-16T15:20:20.401Z" }, - { url = "https://files.pythonhosted.org/packages/9f/76/e32c868a2fa23cd82bacaffd38649d938173244a0e717ec1c0c76874dbdd/hiredis-3.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79cd03e7ff550c17758a7520bf437c156d3d4c8bb74214deeafa69cda49c85a4", size = 42379, upload-time = "2026-03-16T15:20:21.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f6/d687d36a74ce6cf448826cf2e8edfc1eb37cc965308f74eb696aa97c69df/hiredis-3.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ffa7ba2e2da1f806f3181b9730b3e87ba9dbfec884806725d4584055ba3faa6", size = 180311, upload-time = "2026-03-16T15:20:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/db/ac/f520dc0066a62a15aa920c7dd0a2028c213f4862d5f901409ae92ee5d785/hiredis-3.3.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ee37fe8cf081b72dea72f96a0ee604f492ec02252eb77dc26ff6eec3f997b580", size = 190488, upload-time = "2026-03-16T15:20:24.357Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f5/ae10fff82d0f291e90c41bf10a5d6543a96aae00cccede01bf2b6f7e178d/hiredis-3.3.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9bfdeff778d3f7ff449ca5922ab773899e7d31e26a576028b06a5e9cf0ed8c34", size = 189210, upload-time = "2026-03-16T15:20:25.51Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8f/5be4344e542aa8d349a03d05486c59d9ca26f69c749d11e114bf34b84d50/hiredis-3.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:027ce4fabfeff5af5b9869d5524770877f9061d118bc36b85703ae3faf5aad8e", size = 180971, upload-time = "2026-03-16T15:20:26.631Z" }, - { url = "https://files.pythonhosted.org/packages/41/a2/29e230226ec2a31f13f8a832fbafe366e263f3b090553ebe49bb4581a7bd/hiredis-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dcea8c3f53674ae68e44b12e853b844a1d315250ca6677b11ec0c06aff85e86c", size = 175314, upload-time = "2026-03-16T15:20:27.848Z" }, - { url = "https://files.pythonhosted.org/packages/89/2e/bf241707ad86b9f3ebfbc7ab89e19d5ec243ff92ca77644a383622e8740b/hiredis-3.3.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0b5ff2f643f4b452b0597b7fe6aa35d398cb31d8806801acfafb1558610ea2aa", size = 185652, upload-time = "2026-03-16T15:20:29.364Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c1/b39170d8bcccd01febd45af4ac6b43ff38e134a868e2ec167a82a036fb35/hiredis-3.3.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3586c8a5f56d34b9dddaaa9e76905f31933cac267251006adf86ec0eef7d0400", size = 179033, upload-time = "2026-03-16T15:20:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/b7/3a/4fe39a169115434f911abff08ff485b9b6201c168500e112b3f6a8110c0a/hiredis-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a110d19881ca78a88583d3b07231e7c6864864f5f1f3491b638863ea45fa8708", size = 176126, upload-time = "2026-03-16T15:20:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/44/99/c1d0b0bc4f9e9150e24beb0dca2e186e32d5e749d0022e0d26453749ed51/hiredis-3.3.1-cp314-cp314t-win32.whl", hash = "sha256:98fd5b39410e9d69e10e90d0330e35650becaa5dd2548f509b9598f1f3c6124d", size = 22028, upload-time = "2026-03-16T15:20:33.33Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/191e6741addc97bcf5e755661f8c82f0fd0aa35f07ece56e858da689b57e/hiredis-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ab1f646ff531d70bfd25f01e60708dfa3d105eb458b7dedd9fe9a443039fd809", size = 23811, upload-time = "2026-03-16T15:20:34.292Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -843,6 +1117,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -852,6 +1135,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "inflect" version = "7.5.0" @@ -895,6 +1190,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.17.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pyrsistent", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785, upload-time = "2022-11-29T20:37:47.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/97/c698bd9350f307daad79dd740806e1a59becd693bd11443a0f531e3229b3/jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6", size = 90379, upload-time = "2022-11-29T20:37:45.842Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +dependencies = [ + { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.14'" }, + { name = "referencing", marker = "python_full_version >= '3.14'" }, + { name = "rpds-py", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "librt" version = "0.11.0" @@ -992,6 +1335,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1077,6 +1432,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mcp" +version = "1.23.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14'" }, + { name = "httpx", marker = "python_full_version >= '3.14'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.14'" }, + { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.14'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.14'" }, + { name = "python-multipart", marker = "python_full_version >= '3.14'" }, + { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.14'" }, + { name = "starlette", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.14'" }, + { name = "uvicorn", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "more-itertools" version = "11.0.2" @@ -1095,7 +1484,7 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } @@ -1155,23 +1544,187 @@ wheels = [ ] [[package]] -name = "packaging" -version = "26.2" +name = "opentelemetry-api" +version = "1.37.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, ] [[package]] -name = "pathspec" -version = "1.1.1" +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-proto", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "wrapt", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-util-http", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.14'" }, + { name = "wrapt", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, +] + +[[package]] +name = "packaging" +version = "21.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "pyparsing", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/9e/d1a7217f69310c1db8fdf8ab396229f55a699ce34a203691794c5d1cad0c/packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", size = 84848, upload-time = "2021-11-18T00:39:13.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/8e/8de486cbd03baba4deef4142bd643a3e7bbe954a784dc1bb17142572d127/packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522", size = 40750, upload-time = "2021-11-18T00:39:10.932Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "peewee" +version = "3.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" @@ -1204,64 +1757,19 @@ wheels = [ ] [[package]] -name = "prodik" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "alembic" }, - { name = "argon2-cffi" }, - { name = "asyncpg" }, - { name = "cryptography" }, - { name = "dishka" }, - { name = "fastapi" }, - { name = "pydantic", extra = ["email"] }, - { name = "pyjwt" }, - { name = "redis", extra = ["hiredis"] }, - { name = "sqlalchemy" }, - { name = "structlog" }, - { name = "uvicorn" }, -] - -[package.optional-dependencies] -dev = [ - { name = "datamodel-code-generator" }, - { name = "dirty-equals" }, - { name = "httpx" }, - { name = "mypy" }, - { name = "polyfactory" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-xdist" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "alembic", specifier = "==1.18.4" }, - { name = "argon2-cffi", specifier = "==25.1.0" }, - { name = "asyncpg", specifier = "==0.31.0" }, - { name = "cryptography", specifier = "==46.0.5" }, - { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.54.0" }, - { name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.11" }, - { name = "dishka", specifier = "==1.7.2" }, - { name = "fastapi", specifier = "==0.128.0" }, - { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, - { name = "mypy", marker = "extra == 'dev'" }, - { name = "polyfactory", marker = "extra == 'dev'", specifier = "==3.3.0" }, - { name = "pydantic", extras = ["email"], specifier = "==2.13.2" }, - { name = "pyjwt", specifier = "==2.11.0" }, - { name = "pytest", marker = "extra == 'dev'" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, - { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, - { name = "redis", extras = ["hiredis"], specifier = "==7.4.0" }, - { name = "ruff", marker = "extra == 'dev'" }, - { name = "sqlalchemy", specifier = "==2.0.46" }, - { name = "structlog", specifier = "==25.5.0" }, - { name = "uvicorn", specifier = "==0.40.0" }, +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] -provides-extras = ["dev"] [[package]] name = "pycparser" @@ -1408,6 +1916,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/f3/0a7087e5f861d66ca64ce927230b397cc264c87b712156e6a93b26a459c8/pydantic_core-2.46.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:154dbfdfb11b8cbd8ff4d00d0b81e3d19f4cb4bedd5aa9f091060ba071474c6a", size = 2192159, upload-time = "2026-04-17T09:11:20.123Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.14'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1426,18 +1948,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "python_full_version >= '3.14'" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642, upload-time = "2023-10-25T21:06:56.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416, upload-time = "2023-10-25T21:06:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021, upload-time = "2023-10-25T21:06:06.953Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747, upload-time = "2023-10-25T21:06:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524, upload-time = "2023-10-25T21:06:10.728Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780, upload-time = "2023-10-25T21:06:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310, upload-time = "2023-10-25T21:06:13.598Z" }, + { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481, upload-time = "2023-10-25T21:06:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222, upload-time = "2023-10-25T21:06:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002, upload-time = "2023-10-25T21:06:18.727Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850, upload-time = "2023-10-25T21:06:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775, upload-time = "2023-10-25T21:06:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306, upload-time = "2023-10-25T21:06:22.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537, upload-time = "2023-10-25T21:06:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615, upload-time = "2023-10-25T21:06:25.815Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335, upload-time = "2023-10-25T21:06:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510, upload-time = "2023-10-25T21:06:30.718Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865, upload-time = "2023-10-25T21:06:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239, upload-time = "2023-10-25T21:06:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106, upload-time = "2023-10-25T21:06:54.387Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version < '3.14'" }, + { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pluggy", marker = "python_full_version < '3.14'" }, + { name = "pygments", marker = "python_full_version < '3.14'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "pytest" version = "9.0.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "iniconfig", marker = "python_full_version >= '3.14'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pluggy", marker = "python_full_version >= '3.14'" }, + { name = "pygments", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -1450,7 +2037,8 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, - { name = "pytest" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } @@ -1465,7 +2053,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, - { name = "pytest" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -1478,13 +2067,44 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-lsp-jsonrpc" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ujson", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/45/1c2a272950679af529f7360af6ee567ef266f282e451be926329e8d50d84/python-lsp-jsonrpc-1.0.0.tar.gz", hash = "sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd", size = 10011, upload-time = "2021-04-14T21:19:20.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/ee/754bfd5f6bfe7162c10d3ecb0aeef6f882f91d3231596c83f761a75efd0b/python_lsp_jsonrpc-1.0.0-py3-none-any.whl", hash = "sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7", size = 8507, upload-time = "2021-04-14T21:19:18.698Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -1524,6 +2144,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1589,20 +2231,251 @@ wheels = [ ] [[package]] -name = "redis" -version = "7.4.0" +name = "referencing" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, + { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "rpds-py", marker = "python_full_version >= '3.14'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[package.optional-dependencies] -hiredis = [ - { name = "hiredis" }, +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.17.40" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/d6/eb2833ccba5ea36f8f4de4bcfa0d1a91eb618f832d430b70e3086821f251/ruamel.yaml-0.17.40.tar.gz", hash = "sha256:6024b986f06765d482b5b07e086cc4b4cd05dd22ddcbc758fa23d54873cf313d", size = 137672, upload-time = "2023-10-20T12:53:56.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/79/5e2cffa1c77432f11cd93a5351f30732c997a239d3a3090856a72d6d8ba7/ruamel.yaml-0.17.40-py3-none-any.whl", hash = "sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c", size = 113666, upload-time = "2023-10-20T12:53:52.628Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e9/39ec4d4b3f91188fad1842748f67d4e749c77c37e353c4e545052ee8e893/ruamel.yaml.clib-0.2.14.tar.gz", hash = "sha256:803f5044b13602d58ea378576dd75aa759f52116a0232608e8fdada4da33752e", size = 225394, upload-time = "2025-09-22T19:51:23.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/56/35a0a752415ae01992c68f5a6513bdef0e1b6fbdb60d7619342ce12346a0/ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f8b2acb0ffdd2ce8208accbec2dca4a06937d556fdcaefd6473ba1b5daa7e3c4", size = 269216, upload-time = "2025-09-23T14:24:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/98/6a/9a68184ab93619f4607ff1675e4ef01e8accfcbff0d482f4ca44c10d8eab/ruamel.yaml.clib-0.2.14-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:aef953f3b8bd0b50bd52a2e52fb54a6a2171a1889d8dea4a5959d46c6624c451", size = 137092, upload-time = "2025-09-22T19:50:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3f/cfed5f088628128a9ec66f46794fd4d165642155c7b78c26d83b16c6bf7b/ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a0ac90efbc7a77b0d796c03c8cc4e62fd710b3f1e4c32947713ef2ef52e09543", size = 633768, upload-time = "2025-09-22T19:50:31.228Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d5/5ce2cc156c1da48160171968d91f066d305840fbf930ee955a509d025a44/ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf6b699223afe6c7fe9f2ef76e0bfa6dd892c21e94ce8c957478987ade76cd8", size = 721253, upload-time = "2025-09-22T19:50:28.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/71/d0b56bc902b38ebe4be8e270f730f929eec4edaf8a0fa7028f4ef64fa950/ruamel.yaml.clib-0.2.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d73a0187718f6eec5b2f729b0f98e4603f7bd9c48aa65d01227d1a5dcdfbe9e8", size = 683823, upload-time = "2025-09-22T19:50:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/1f37449dd89c540218598316ccafc1a0aed60215e72efa315c5367cfd015/ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81f6d3b19bc703679a5705c6a16dabdc79823c71d791d73c65949be7f3012c02", size = 690370, upload-time = "2025-09-23T18:42:46.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/53/c498b30f35efcd9f47cb084d7ad9374f2b907470f73913dec6396b81397d/ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b28caeaf3e670c08cb7e8de221266df8494c169bd6ed8875493fab45be9607a4", size = 703578, upload-time = "2025-09-22T19:50:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/34/79/492cfad9baed68914840c39e5f3c1cc251f51a897ddb3f532601215cbb12/ruamel.yaml.clib-0.2.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94f3efb718f8f49b031f2071ec7a27dd20cbfe511b4dfd54ecee54c956da2b31", size = 722544, upload-time = "2025-09-22T19:50:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/479ebfd5ba396e209ade90f7282d84b90c57b3e07be8dc6fcd02a6df7ffc/ruamel.yaml.clib-0.2.14-cp310-cp310-win32.whl", hash = "sha256:27c070cf3888e90d992be75dd47292ff9aa17dafd36492812a6a304a1aedc182", size = 100375, upload-time = "2025-09-22T19:50:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/57/31/a044520fdb3bd409889f67f1efebda0658033c7ab3f390cee37531cc9a9e/ruamel.yaml.clib-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:4f4a150a737fccae13fb51234d41304ff2222e3b7d4c8e9428ed1a6ab48389b8", size = 118129, upload-time = "2025-09-22T19:50:35.545Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/3c51e9578b8c36fcc4bdd271a1a5bb65963a74a4b6ad1a989768a22f6c2a/ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5bae1a073ca4244620425cd3d3aa9746bde590992b98ee8c7c8be8c597ca0d4e", size = 270207, upload-time = "2025-09-23T14:24:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/4a/16/cb02815bc2ae9c66760c0c061d23c7358f9ba51dae95ac85247662b7fbe2/ruamel.yaml.clib-0.2.14-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:0a54e5e40a7a691a426c2703b09b0d61a14294d25cfacc00631aa6f9c964df0d", size = 137780, upload-time = "2025-09-22T19:50:37.734Z" }, + { url = "https://files.pythonhosted.org/packages/31/c6/fc687cd1b93bff8e40861eea46d6dc1a6a778d9a085684e4045ff26a8e40/ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:10d9595b6a19778f3269399eff6bab642608e5966183abc2adbe558a42d4efc9", size = 641590, upload-time = "2025-09-22T19:50:41.978Z" }, + { url = "https://files.pythonhosted.org/packages/45/5d/65a2bc08b709b08576b3f307bf63951ee68a8e047cbbda6f1c9864ecf9a7/ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba72975485f2b87b786075e18a6e5d07dc2b4d8973beb2732b9b2816f1bad70", size = 738090, upload-time = "2025-09-22T19:50:39.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d0/a70a03614d9a6788a3661ab1538879ed2aae4e84d861f101243116308a37/ruamel.yaml.clib-0.2.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29757bdb7c142f9595cc1b62ec49a3d1c83fab9cef92db52b0ccebaad4eafb98", size = 700744, upload-time = "2025-09-22T19:50:40.811Z" }, + { url = "https://files.pythonhosted.org/packages/77/30/c93fa457611f79946d5cb6cc97493ca5425f3f21891d7b1f9b44eaa1b38e/ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:557df28dbccf79b152fe2d1b935f6063d9cc431199ea2b0e84892f35c03bb0ee", size = 742321, upload-time = "2025-09-23T18:42:48.916Z" }, + { url = "https://files.pythonhosted.org/packages/40/85/e2c54ad637117cd13244a4649946eaa00f32edcb882d1f92df90e079ab00/ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:26a8de280ab0d22b6e3ec745b4a5a07151a0f74aad92dd76ab9c8d8d7087720d", size = 743805, upload-time = "2025-09-22T19:50:43.58Z" }, + { url = "https://files.pythonhosted.org/packages/81/50/f899072c38877d8ef5382e0b3d47f8c4346226c1f52d6945d6f64fec6a2f/ruamel.yaml.clib-0.2.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e501c096aa3889133d674605ebd018471bc404a59cbc17da3c5924421c54d97c", size = 769529, upload-time = "2025-09-22T19:50:45.707Z" }, + { url = "https://files.pythonhosted.org/packages/99/7c/96d4b5075e30c65ea2064e40c2d657c7c235d7b6ef18751cf89a935b9041/ruamel.yaml.clib-0.2.14-cp311-cp311-win32.whl", hash = "sha256:915748cfc25b8cfd81b14d00f4bfdb2ab227a30d6d43459034533f4d1c207a2a", size = 100256, upload-time = "2025-09-22T19:50:48.26Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8c/73ee2babd04e8bfcf1fd5c20aa553d18bf0ebc24b592b4f831d12ae46cc0/ruamel.yaml.clib-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:4ccba93c1e5a40af45b2f08e4591969fa4697eae951c708f3f83dcbf9f6c6bb1", size = 118234, upload-time = "2025-09-22T19:50:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/b4/42/ccfb34a25289afbbc42017e4d3d4288e61d35b2e00cfc6b92974a6a1f94b/ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6aeadc170090ff1889f0d2c3057557f9cd71f975f17535c26a5d37af98f19c27", size = 271775, upload-time = "2025-09-23T14:24:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/82/73/e628a92e80197ff6a79ab81ec3fa00d4cc082d58ab78d3337b7ba7043301/ruamel.yaml.clib-0.2.14-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5e56ac47260c0eed992789fa0b8efe43404a9adb608608631a948cee4fc2b052", size = 138842, upload-time = "2025-09-22T19:50:49.156Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c5/346c7094344a60419764b4b1334d9e0285031c961176ff88ffb652405b0c/ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a911aa73588d9a8b08d662b9484bc0567949529824a55d3885b77e8dd62a127a", size = 647404, upload-time = "2025-09-22T19:50:52.921Z" }, + { url = "https://files.pythonhosted.org/packages/df/99/65080c863eb06d4498de3d6c86f3e90595e02e159fd8529f1565f56cfe2c/ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05ba88adf3d7189a974b2de7a9d56731548d35dc0a822ec3dc669caa7019b29", size = 753141, upload-time = "2025-09-22T19:50:50.294Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e3/0de85f3e3333f8e29e4b10244374a202a87665d1131798946ee22cf05c7c/ruamel.yaml.clib-0.2.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb04c5650de6668b853623eceadcdb1a9f2fee381f5d7b6bc842ee7c239eeec4", size = 703477, upload-time = "2025-09-22T19:50:51.508Z" }, + { url = "https://files.pythonhosted.org/packages/d9/25/0d2f09d8833c7fd77ab8efeff213093c16856479a9d293180a0d89f6bed9/ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df3ec9959241d07bc261f4983d25a1205ff37703faf42b474f15d54d88b4f8c9", size = 741157, upload-time = "2025-09-23T18:42:50.408Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/959f10c2e2153cbdab834c46e6954b6dd9e3b109c8f8c0a3cf1618310985/ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbc08c02e9b147a11dfcaa1ac8a83168b699863493e183f7c0c8b12850b7d259", size = 745859, upload-time = "2025-09-22T19:50:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6b/e580a7c18b485e1a5f30a32cda96b20364b0ba649d9d2baaf72f8bd21f83/ruamel.yaml.clib-0.2.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c099cafc1834d3c5dac305865d04235f7c21c167c8dd31ebc3d6bbc357e2f023", size = 770200, upload-time = "2025-09-22T19:50:55.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/44/3455eebc761dc8e8fdced90f2b0a3fa61e32ba38b50de4130e2d57db0f21/ruamel.yaml.clib-0.2.14-cp312-cp312-win32.whl", hash = "sha256:b5b0f7e294700b615a3bcf6d28b26e6da94e8eba63b079f4ec92e9ba6c0d6b54", size = 98829, upload-time = "2025-09-22T19:50:58.895Z" }, + { url = "https://files.pythonhosted.org/packages/76/ab/5121f7f3b651db93de546f8c982c241397aad0a4765d793aca1dac5eadee/ruamel.yaml.clib-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:a37f40a859b503304dd740686359fcf541d6fb3ff7fc10f539af7f7150917c68", size = 115570, upload-time = "2025-09-22T19:50:57.981Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ae/e3811f05415594025e96000349d3400978adaed88d8f98d494352d9761ee/ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7e4f9da7e7549946e02a6122dcad00b7c1168513acb1f8a726b1aaf504a99d32", size = 269205, upload-time = "2025-09-23T14:24:15.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/06/7d51f4688d6d72bb72fa74254e1593c4f5ebd0036be5b41fe39315b275e9/ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:dd7546c851e59c06197a7c651335755e74aa383a835878ca86d2c650c07a2f85", size = 137417, upload-time = "2025-09-22T19:50:59.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/08/b4499234a420ef42960eeb05585df5cc7eb25ccb8c980490b079e6367050/ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:1c1acc3a0209ea9042cc3cfc0790edd2eddd431a2ec3f8283d081e4d5018571e", size = 642558, upload-time = "2025-09-22T19:51:03.388Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ba/1975a27dedf1c4c33306ee67c948121be8710b19387aada29e2f139c43ee/ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2070bf0ad1540d5c77a664de07ebcc45eebd1ddcab71a7a06f26936920692beb", size = 744087, upload-time = "2025-09-22T19:51:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/20/15/8a19a13d27f3bd09fa18813add8380a29115a47b553845f08802959acbce/ruamel.yaml.clib-0.2.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd8fe07f49c170e09d76773fb86ad9135e0beee44f36e1576a201b0676d3d1d", size = 699709, upload-time = "2025-09-22T19:51:02.075Z" }, + { url = "https://files.pythonhosted.org/packages/19/ee/8d6146a079ad21e534b5083c9ee4a4c8bec42f79cf87594b60978286b39a/ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ff86876889ea478b1381089e55cf9e345707b312beda4986f823e1d95e8c0f59", size = 708926, upload-time = "2025-09-23T18:42:51.707Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/426b714abdc222392e68f3b8ad323930d05a214a27c7e7a0f06c69126401/ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1f118b707eece8cf84ecbc3e3ec94d9db879d85ed608f95870d39b2d2efa5dca", size = 740202, upload-time = "2025-09-22T19:51:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ac/3c5c2b27a183f4fda8a57c82211721c016bcb689a4a175865f7646db9f94/ruamel.yaml.clib-0.2.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b30110b29484adc597df6bd92a37b90e63a8c152ca8136aad100a02f8ba6d1b6", size = 765196, upload-time = "2025-09-22T19:51:05.916Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/06f56a71fd55021c993ed6e848c9b2e5e9cfce180a42179f0ddd28253f7c/ruamel.yaml.clib-0.2.14-cp313-cp313-win32.whl", hash = "sha256:f4e97a1cf0b7a30af9e1d9dad10a5671157b9acee790d9e26996391f49b965a2", size = 98635, upload-time = "2025-09-22T19:51:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/51/79/76aba16a1689b50528224b182f71097ece338e7a4ab55e84c2e73443b78a/ruamel.yaml.clib-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:090782b5fb9d98df96509eecdbcaffd037d47389a89492320280d52f91330d78", size = 115238, upload-time = "2025-09-22T19:51:07.081Z" }, + { url = "https://files.pythonhosted.org/packages/21/e2/a59ff65c26aaf21a24eb38df777cb9af5d87ba8fc8107c163c2da9d1e85e/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7df6f6e9d0e33c7b1d435defb185095386c469109de723d514142632a7b9d07f", size = 271441, upload-time = "2025-09-23T14:24:16.498Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" }, ] [[package]] @@ -1630,6 +2503,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "semgrep" +version = "0.112.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "boltons", marker = "python_full_version < '3.14'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "click-option-group", marker = "python_full_version < '3.14'" }, + { name = "colorama", marker = "python_full_version < '3.14'" }, + { name = "defusedxml", marker = "python_full_version < '3.14'" }, + { name = "glom", version = "22.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "jsonschema", version = "4.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "peewee", marker = "python_full_version < '3.14'" }, + { name = "python-lsp-jsonrpc", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "ruamel-yaml", version = "0.17.40", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "wcmatch", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/00/6850f808965fc531c7287e7b8ac71daa13ce6c819ac23acb35beff5c2710/semgrep-0.112.1.tar.gz", hash = "sha256:2140a0fd16f3ac0a0e330980613ba65fe58faea7808a965e219c4199bab96102", size = 195076, upload-time = "2022-09-08T02:11:29.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ac/263e8e91705635a09d6bf3cf2a2c3c082e2d9518f11fea9a0551c31bf8d0/semgrep-0.112.1-cp37.cp38.cp39.py37.py38.py39-none-any.whl", hash = "sha256:2a62bc6321d371dadc41df2c68678e87df807ddfa3d1faafb40bd0998abfe503", size = 25614590, upload-time = "2022-09-08T02:11:18.378Z" }, + { url = "https://files.pythonhosted.org/packages/12/01/9fefe9cc72117c0a70467d3395dafc0044205bf25f89d1d8d3e6dbc524d3/semgrep-0.112.1-cp37.cp38.cp39.py37.py38.py39-none-macosx_10_14_x86_64.whl", hash = "sha256:b690a84cbaa6a4670ff6c11a674818b8ad99736e89520a3c47d1542cc45e6d05", size = 22232420, upload-time = "2022-09-08T02:11:22.832Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2d/091319e7146c6dc33bf8123e5e8186803bd330f1f7b3ac7995c2fa95933f/semgrep-0.112.1-cp37.cp38.cp39.py37.py38.py39-none-macosx_11_0_arm64.whl", hash = "sha256:bc2437f12e41ac5e28a303e463098565a3d948c7744f6745bdb0fea341ccdae1", size = 25508160, upload-time = "2022-09-08T02:11:26.813Z" }, +] + +[[package]] +name = "semgrep" +version = "1.156.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +dependencies = [ + { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "boltons", marker = "python_full_version >= '3.14'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "click-option-group", marker = "python_full_version >= '3.14'" }, + { name = "colorama", marker = "python_full_version >= '3.14'" }, + { name = "exceptiongroup", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "glom", version = "25.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "mcp", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-instrumentation-requests", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-instrumentation-threading", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.14'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "peewee", marker = "python_full_version >= '3.14'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.14'" }, + { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "rich", marker = "python_full_version >= '3.14'" }, + { name = "ruamel-yaml", version = "0.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "ruamel-yaml-clib", marker = "python_full_version >= '3.14'" }, + { name = "semantic-version", marker = "python_full_version >= '3.14'" }, + { name = "tomli", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "wcmatch", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/8d3079d353686db515a1188d6110301e1edff75abcffbfaee99a55f21344/semgrep-1.156.0.tar.gz", hash = "sha256:6c58c499ad86385c4786f04fe387d788113f1b9c95bc7b4c38941bf078cf5d9a", size = 52953549, upload-time = "2026-03-17T21:53:47.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/d4/9f8efd20f96cb9ff74d5dd013b1938b9246001c4b61e8e98501f236af71a/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:5bd2924958af5d4e199fe82c4ac9c7be4ae4fd20a3a46ce9f71237f8408f5b66", size = 42194313, upload-time = "2026-03-17T21:53:25.492Z" }, + { url = "https://files.pythonhosted.org/packages/66/a9/3d4082f30bca4ba738d391e241174c2297be87bb5ca37a3911b164289694/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:ff57b35def987ec3f21748051fdb89ae57574984bf8108b03d79473da49e93f0", size = 45891219, upload-time = "2026-03-17T21:53:28.903Z" }, + { url = "https://files.pythonhosted.org/packages/60/32/dca58fbb76a0d5ba16ed366e3478c50c737bd3da8788eb0d213d60562075/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7e9ae909fa8c26220c88392af9c22479c0ef51e170eeda8b2c81382b431afb76", size = 60020097, upload-time = "2026-03-17T21:53:31.923Z" }, + { url = "https://files.pythonhosted.org/packages/92/0c/b00156bee40cf9e594c2bd10672232e33d9a7d1bc8b5a6dd697097c8d6be/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:a7f68544bcd33fac9bc519e6a6c4759d612836422ce87bc08dd014481c3e8fd0", size = 57680875, upload-time = "2026-03-17T21:53:34.967Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/1a82d535e5a54e9617efcf3b8e9ffecff091566471946dd9369cd586c3bd/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_0_aarch64.whl", hash = "sha256:9604dddeebee8a3acc89f6ac25ad646e222a753a04fb944c2c5635f615b3a242", size = 60020098, upload-time = "2026-03-17T21:53:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3c/3d48471dbfcb66cfb22b175d2221bc538355edea988f62cf1abb886afd8c/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_0_x86_64.whl", hash = "sha256:b68d14aced6c6f389fc7524e314b5638aa11ba50f35d3d3d97488e0930a21beb", size = 57680873, upload-time = "2026-03-17T21:53:41.211Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3d/f6f42ee18039e0f655ef5ddcd68ef2447b4c922178879fd1e9a75b846d41/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:a9a683f94f99629d345ac73a215859fc5ce6c58fe67ad2f78671de3874a260e3", size = 53820998, upload-time = "2026-03-17T21:53:44.318Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.46" @@ -1686,6 +2650,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14'" }, + { name = "starlette", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, +] + [[package]] name = "starlette" version = "0.50.0" @@ -1699,6 +2676,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "stevedore" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, +] + +[[package]] +name = "stevedore" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version >= '3.11' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, +] + [[package]] name = "structlog" version = "25.5.0" @@ -1711,10 +2714,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, ] +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, +] + [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, @@ -1765,6 +2785,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tqdm" +version = "4.69.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, +] + [[package]] name = "typeguard" version = "4.5.2" @@ -1807,12 +2839,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] +[[package]] +name = "ujson" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/7a/c8bb37c8f6f3623d60c33d15d18cd6d6655d0f9c3eb31a9969f76361b199/ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75", size = 7166784, upload-time = "2026-06-14T22:36:50.209Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/a4/ff15f528d386f47b1972859f25587da017d5be84c75076b380fedde318fe/ujson-5.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:770643b4752266c5a466149848b78c3874940926a4ecef304f518b2b6cdb432f", size = 56497, upload-time = "2026-06-14T22:34:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d2/92e83af35cd65d3b7c47fb0e24927aa2b478897af8b0dd0ee19abcd50c03/ujson-5.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8e8c1203cb1a27720debc334f840a9170da741503522f86999710cb4738fbe3", size = 54301, upload-time = "2026-06-14T22:35:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/96/9c/3eb2d21778dbd0a9f1adae7ce73e4f6c981efa7fab534eb28afde31fd25e/ujson-5.13.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:65c1813fdd742fe3c249d9c417fa490e5b54e8a91bf343a88486ec50d175c444", size = 59968, upload-time = "2026-06-14T22:35:01.612Z" }, + { url = "https://files.pythonhosted.org/packages/9a/52/f5b72747349c66552396166a13424a717df70e76a18ac5bbedc84c244407/ujson-5.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5aa9bf16f0131812720dd4ae70bc1a0cf68f79e93c07c66100d328e75944b567", size = 53434, upload-time = "2026-06-14T22:35:02.644Z" }, + { url = "https://files.pythonhosted.org/packages/4b/73/103c2c6df7bfe57e01fb6006551188b4dc9f873cb5f72146bd5b177b75f9/ujson-5.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82c17b904c03c2b9629486ec91a8fa46a15f10d03504284f54ed7257a917d9f1", size = 54976, upload-time = "2026-06-14T22:35:03.759Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ba/44fcf17cc93be7c6647fef2e556d703791659280e6409623dea854e6db7d/ujson-5.13.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0b5f6983b2469db00e540b68fa8297b7a0ccd0d5173c60cc3e84f336b09395f", size = 58239, upload-time = "2026-06-14T22:35:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/0d/60/10f6c92b549cff72e555a900ee7128b992ebf510bad0eebd946e72b68ecb/ujson-5.13.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b877fe7926107d25d54b655c5b8dd94b294b22c157233163ad29fdb54ac10cf4", size = 57876, upload-time = "2026-06-14T22:35:05.998Z" }, + { url = "https://files.pythonhosted.org/packages/71/5e/b8b2806996c75d4c34cf92ec04c5fff58a645a32632f27836886a718cb87/ujson-5.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3cae1c811e787b9500e2830af8632dcb32a78dea5baf15aac51d681c59a59dd", size = 1037734, upload-time = "2026-06-14T22:35:07.081Z" }, + { url = "https://files.pythonhosted.org/packages/d4/02/baaf3ad0bb4b2c3bec3d9ff8a59768d39d20201146742e8581f8a0f87e77/ujson-5.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:820b78a6a183ab6591b2ea888020032ef0fe328d852af9a5c8d8084ababb2218", size = 1197048, upload-time = "2026-06-14T22:35:08.319Z" }, + { url = "https://files.pythonhosted.org/packages/8e/aa/a945aeba9d463e335169b3c0ee60adc147e22e80bb636fe0f185bed1247f/ujson-5.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1209c985d1f4c2ae085ced7325509650cdb3533bb7294558dd1f26d48377942", size = 1090118, upload-time = "2026-06-14T22:35:10.014Z" }, + { url = "https://files.pythonhosted.org/packages/cc/68/57e084026ab657ab0443017fd0aa51009300bbd2eb09f5113af113703e8b/ujson-5.13.0-cp310-cp310-win32.whl", hash = "sha256:326553ae6c063c8246974906a6c137a0780fe46d143abebc52cd2cadda0f1814", size = 40008, upload-time = "2026-06-14T22:35:11.276Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/da9bb80765a5ea582ff8c1bd2f3c3909c05733314c55652d1c0bd34514f8/ujson-5.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:0354d6b50b0d153ed7c629845b18a953d0c727b7e768fd94a94e0602abfa1f29", size = 40947, upload-time = "2026-06-14T22:35:12.323Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b1/eaf4308ff7e7dc5aba78d7479102a69fa5fbe65e1b02f16c62b553a0b506/ujson-5.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:7b4a05f61a96553995da6a4d502e07bc8aa5d07a90031df006239c6b00ce1c83", size = 38975, upload-time = "2026-06-14T22:35:13.218Z" }, + { url = "https://files.pythonhosted.org/packages/58/dc/2fcf821896803248122835c800e74f4582de9d6092efb37152acd7f79bdc/ujson-5.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b7badefa73f96bad9e295ea22bd06967b851c8aad68c74196437e3584f25de5", size = 56496, upload-time = "2026-06-14T22:35:14.362Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/83db69f96dc12509c9510084c0389c4aff4dcf427f9b613d1a23abd446ce/ujson-5.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:09effd42924a80df20a63b31a1ede905e66b0ce24aafe7a4cbedb05c783f8bb3", size = 54300, upload-time = "2026-06-14T22:35:15.522Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/6d87206988172015781b9ff842c0a7eca897c026b2e7a95e11d86d46ddf5/ujson-5.13.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:89d0bfc986d02b4ce76b00e0f560bc8d30dfe8c05a1bfd8529e085eb6c1a77d9", size = 59976, upload-time = "2026-06-14T22:35:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/d28ca5e6c3d8c467a4c6296404067f4eee9d67dfeeebeb3c5fb0bd6cb958/ujson-5.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cdd9618e07b3b142a02f0ab8227fd52453688b8e8e60ac0511f13a25fa8009db", size = 53476, upload-time = "2026-06-14T22:35:17.664Z" }, + { url = "https://files.pythonhosted.org/packages/a9/99/d6bb0e18188954326b38499ae61b04ce8ead6425b8844384e537a491267b/ujson-5.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0806683e8171ec06817e6af22f14ba0cd2f16def8a2ffb22a28a10615249355d", size = 54962, upload-time = "2026-06-14T22:35:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5f/8f1ce659a59ef9510fa47b95773442049a383c533a645b2da8beeeec90f1/ujson-5.13.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1cf46c79498c81f088cad4165b1669a78bba7bfbeb778c7cc1aff316e062b0d", size = 58265, upload-time = "2026-06-14T22:35:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9d11af9d1d19ea239b0d289cf62a611cb4f2da9ec0d46b826767f4ab1768/ujson-5.13.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2282039eb26f08ed1a1381360395f8a310f39a45ef7314cb0f258a7d2917ea3", size = 57874, upload-time = "2026-06-14T22:35:20.896Z" }, + { url = "https://files.pythonhosted.org/packages/25/e2/7891c4a1c954307ab6a575686897a4963aad36c284742216f52dc40cba4a/ujson-5.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c75eb7fac0fe92925b959cf2fa18f88d9fb76b10781fd2a6ccb895d5fb89171", size = 1037746, upload-time = "2026-06-14T22:35:21.964Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/8a4ce87d3eaaee398f4238f74f128c6eb34269f041659995b2c09710ae7c/ujson-5.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:12078e81def2790140583abefc1b979f3c77be15d53c524bf0e232f669822052", size = 1197022, upload-time = "2026-06-14T22:35:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fb/a1d0a6a83a13adee3a13cad308dcd3251ac53c4bd781f737242ad2f10d19/ujson-5.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12192a37c6c0e476554b62647acdf6139a47b6f13d8bad8dd2316763c4cee12", size = 1090116, upload-time = "2026-06-14T22:35:24.421Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6c/4aed5dce0161d6b8c95c5da760477602a05e2a540a762424395acad9aec0/ujson-5.13.0-cp311-cp311-win32.whl", hash = "sha256:ae53b3f046529c193d533ca8111492330b204d6007611cdfa20e8b764c7c1389", size = 39974, upload-time = "2026-06-14T22:35:25.779Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c5/f9f3cf19f6ac29e07782eafed562fe0a7cb451b44bd1664c58fe8974235b/ujson-5.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2275bbaaea3eddd2e8ec0863e28a420f5f520b14a760fc3f1e49fd07a974448c", size = 40941, upload-time = "2026-06-14T22:35:26.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/ec/46058bbbbe45e054cdb9f1dc0bf416fbbd5fec06bf3b4987c2437e496350/ujson-5.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:32a59e7151fe2fec8fdf9a565ee66fcf87918d48827e21cdab5e15ff9f274b78", size = 38974, upload-time = "2026-06-14T22:35:27.871Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ae/b66deca15da1f7faf6952d8eddf55978482bcbfd294ed2afe2c526ea325f/ujson-5.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf81570ac056cb058f9117b52ca5dd800bfe9381d0076d0bb30a08a54591d654", size = 56743, upload-time = "2026-06-14T22:35:28.863Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/b03bcc9eaf4621ac9008dec90918d8fb4839d611666cb99eb255696c67fe/ujson-5.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7edf16359c52ed53406e216565d83e6b98c23c3cb9a0a01673f2493f8fb15edf", size = 54390, upload-time = "2026-06-14T22:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/f98c6c1a4ed9d92d39d5d2d133f2b6fce5da11ea50c341117aedde8011c4/ujson-5.13.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:24539618fb3243cfdf27dab9a850acab80798a01501e9586b61fb9ecd016a891", size = 60047, upload-time = "2026-06-14T22:35:30.857Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/f68e14cf476d149945211142f4c20782c1f232c489e8edcc4f4b58ce4997/ujson-5.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fdde6341d213b29f413b5fa9fad1392d5408074c75f0900ed949e97e546fa5df", size = 53437, upload-time = "2026-06-14T22:35:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1a/5718237cf4141e5be46ff371387e90b01f27774cb6f0f79ff4803a2430ca/ujson-5.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:229faf041ef249ee3fd57bac1cedb123d2718ab63f6ccd50eca95ea902eb0dca", size = 55057, upload-time = "2026-06-14T22:35:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6f/7f55c1e9e0be87beebaed553fa186ad5f6d5d639cbaa9d49f78f2f91c3a9/ujson-5.13.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d02f31c2f59cc6a1c2c3633b377701fc2d8e876cc01950735d7a01132ccc233", size = 58186, upload-time = "2026-06-14T22:35:34.055Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c4/9a34ade542426f56a0bc042f774073d1c247ae7575363c27587788cb2b2f/ujson-5.13.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea7204e9fa7538bfbb1396e1ee8c2bbcd3818b3633ef5bb14d4fdea52994d14d", size = 57935, upload-time = "2026-06-14T22:35:35.05Z" }, + { url = "https://files.pythonhosted.org/packages/36/06/407633f0709e168107f56368bd5a0fa8fe07acd7f1d3000710bc0bb07470/ujson-5.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c5a2478a3a1fa4421f7e035b87194eea0cf44c7971a3f32ad1b42a0dfd63c03", size = 1037685, upload-time = "2026-06-14T22:35:36.022Z" }, + { url = "https://files.pythonhosted.org/packages/c3/df/eb5bd92dc1b23254fea5b2022007baff5491a7478bfcf7e9260d3a10f1ac/ujson-5.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b535e0970c96957e999cfe5ec89361f0e8d0bb987fb5d5144f6f495cb3ed9e19", size = 1197141, upload-time = "2026-06-14T22:35:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1c/65f2ce1a0411ec9a87339db01f0d5d554a49c4248ec68ab52a1b7e14e9c4/ujson-5.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d0ad1207694988498fca7e0bb28eba7564fa33261d2f9fdf66a3aaab376b803", size = 1090225, upload-time = "2026-06-14T22:35:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/73/53/310aabff0704f9c7ef0d3f431ce8b8e3147c3cca25334a205615c511f65e/ujson-5.13.0-cp312-cp312-win32.whl", hash = "sha256:d6bc9fa43a49e403c68c7eb164eef0feee9dd29474a7c6e0d3b6267025371990", size = 40075, upload-time = "2026-06-14T22:35:40.44Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/d3536d8945d1bb00248d998c8dcbe678a884681ad181072daecfafe4eea6/ujson-5.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6692d49ff970aaa5008f4a6fe06974bc91fd957bf13173f765e46d8ba44906ea", size = 41097, upload-time = "2026-06-14T22:35:41.39Z" }, + { url = "https://files.pythonhosted.org/packages/72/a1/4b147c06ee5bb14bec6e26786358c8510c4d75e28b88146a6ac7620f1f71/ujson-5.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:5737ffe0740a788b0e6255f0ffb281db49305fd6e6a587be44c73d9e92b554c4", size = 38875, upload-time = "2026-06-14T22:35:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/fe8a467d8ff5821e076b96f398d3acfe3cd568d900e6ccb41b215592b152/ujson-5.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8", size = 56746, upload-time = "2026-06-14T22:35:43.398Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/c7ab82d6471dfa7e4fd68ae6ff2c6a50d077c05d6ecdea0cec8af635b2c4/ujson-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310", size = 54388, upload-time = "2026-06-14T22:35:44.586Z" }, + { url = "https://files.pythonhosted.org/packages/10/e6/4e9e998d991ff88bbc93b21daa63bba2baa61c6f952dbcec937cf7304ebe/ujson-5.13.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9", size = 60051, upload-time = "2026-06-14T22:35:45.567Z" }, + { url = "https://files.pythonhosted.org/packages/9c/11/876dff43f05417a01c6119f0fa10e01f1226631c5927ef08f56876b2bb67/ujson-5.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f", size = 53438, upload-time = "2026-06-14T22:35:46.623Z" }, + { url = "https://files.pythonhosted.org/packages/09/02/f9dbf6c3e46d700eb1d9ed637567221a06eeb1ec289633be992ef54d7a34/ujson-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460", size = 55060, upload-time = "2026-06-14T22:35:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3d/7e49a70265a1e5ed1b5e8edd5f54d57ae41e2134faeae9b16f6f5a0eae20/ujson-5.13.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21", size = 58189, upload-time = "2026-06-14T22:35:48.617Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/b64278f67e19052f09810576c7e50b3da8d4f5218b226046324d4d5c24b4/ujson-5.13.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4", size = 57941, upload-time = "2026-06-14T22:35:49.674Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/51513357a5c75bf3e5bae46accfdb3e6e6f5caeb72ca8b253ec45ba853fb/ujson-5.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede", size = 1037688, upload-time = "2026-06-14T22:35:50.699Z" }, + { url = "https://files.pythonhosted.org/packages/54/5a/dc6afe071d6b977390d2dc41e15800a2716f317988dd03187cffe7b4d624/ujson-5.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1", size = 1197141, upload-time = "2026-06-14T22:35:52.052Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/b473d101412c68527cb502a8728f96ab307aa7bfa75d6ea2037e2c7f74e8/ujson-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011", size = 1090235, upload-time = "2026-06-14T22:35:53.589Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0a/8e583cce90f9f91ca1bedb3e628b6f5642aed91feb29b197431268d4c4be/ujson-5.13.0-cp313-cp313-win32.whl", hash = "sha256:b63d3820f978bc8e98cc3f1fe26a33b0d2ea237733a23fe5e9cb5d51f466bd97", size = 40069, upload-time = "2026-06-14T22:35:55.019Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b5/1fe203bc294e98fdd65606883692ad8dc0aaac73838b89c99c3513404424/ujson-5.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:17a59d5cf23ef98f7c9314524976b4b288374d83200add01d953024fb06404f9", size = 41098, upload-time = "2026-06-14T22:35:55.966Z" }, + { url = "https://files.pythonhosted.org/packages/50/5e/aceadce24fdb7cbc67f02286b1d4e91a575aaef5afb876c9908d6e6e5769/ujson-5.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:b49516fbe803ff30d6caa9ccc3799ec7f968992747ce3099eae4758928577b53", size = 38877, upload-time = "2026-06-14T22:35:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/b5139d696f5328f3cab70b9ec046f15e3f49497a4de6280974640602f539/ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b", size = 56897, upload-time = "2026-06-14T22:35:57.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/55/477183aeddfdf0f88ae039ffee0ed866cfb993da0c0c9aa915807554aef8/ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5", size = 54451, upload-time = "2026-06-14T22:35:58.932Z" }, + { url = "https://files.pythonhosted.org/packages/ea/63/55e5f23e156b4c8bca095d828b4cd3180c0b42aa3501ef88836d79606fea/ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a", size = 60053, upload-time = "2026-06-14T22:35:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/08c6cf5548bd6f4bb557c9fa7e8edf87324bb04c17249d1966028d61dde0/ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0", size = 53481, upload-time = "2026-06-14T22:36:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b3/0ac9a03551467784067f505df1bb875c639ba32f1da79ce467ab15911ada/ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71", size = 55058, upload-time = "2026-06-14T22:36:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/ba/be/ec91029aec067174473d022fa0f6c3c1431a173f888d7599739f05c668eb/ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e", size = 58225, upload-time = "2026-06-14T22:36:03.321Z" }, + { url = "https://files.pythonhosted.org/packages/29/33/a948f329252ece3f9c93d177243de6e677927ebc6ac44256742dbbef3c39/ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97", size = 57930, upload-time = "2026-06-14T22:36:04.385Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0c/c33655218b8e0a8adbf066de0b999cae5c324061f3eaa4dda17423145d9e/ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285", size = 1037728, upload-time = "2026-06-14T22:36:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/d286947525ea7ce3f2d8dc55c15b9ffbe425bc455c96af7b8f8a402599a9/ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d", size = 1197146, upload-time = "2026-06-14T22:36:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3c/9eb916377050b0785f048a34588c1c390ddd41ae00b78db68ee1ad022356/ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c", size = 1090223, upload-time = "2026-06-14T22:36:08.329Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/242fd97a2628b842d4bfaa9b18e1f68187f934d67503291ebbaab1254637/ujson-5.13.0-cp314-cp314-win32.whl", hash = "sha256:576f35c35b918d67d41b933878062ec0a5c9f4d1e9e14e04aeef35384963feae", size = 41223, upload-time = "2026-06-14T22:36:09.644Z" }, + { url = "https://files.pythonhosted.org/packages/23/f3/7f2bd9ca0c507142d0c22347b3d6f8803be1d8851c31707e57f5923fdbea/ujson-5.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:d5e206e9f849ead27e51ef8da44e52b38da7c6dbd929a7340ab44533edcda8d7", size = 42265, upload-time = "2026-06-14T22:36:11.043Z" }, + { url = "https://files.pythonhosted.org/packages/b0/29/3e9a8fba321c031315f6d263510969a5d01f41fc471b5be107e413c1b2f8/ujson-5.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc470179775468f9a007d3a6a2734624248c94bf47c6645e808c7e50a5070d1a", size = 40205, upload-time = "2026-06-14T22:36:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/1c543837c6a3c6672361882a0fa269bd02daf9cc4c0ca88a9dccd9df98d9/ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979", size = 57402, upload-time = "2026-06-14T22:36:13.238Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/39862f0f7174ff07cfd1e2d0c9065ded34aeebdb7db8daf2f0e5bf89b46f/ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267", size = 54973, upload-time = "2026-06-14T22:36:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/02/66/f53d3b32c3f177f846ca6b624e832f29000d8a213a2d8768e254bd470ced/ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a", size = 60683, upload-time = "2026-06-14T22:36:15.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d4/dddc4646d2633c85c938c2ded7d5a9711cdad5be1e13b31b7dad76f61c83/ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a", size = 54167, upload-time = "2026-06-14T22:36:16.952Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c0/d8608c3f4d3f05e6441364b63fde1d279700135c1a6577a773662c07fbcc/ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f", size = 55568, upload-time = "2026-06-14T22:36:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/22/8e/dd12b735aaba0806c3d70c18184d50e1f9712e0757c7c0a4f376450cfe28/ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1", size = 59086, upload-time = "2026-06-14T22:36:19.071Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/ad41e8752d5ec3a590a5e7b426a54e36b7aab911d9b5a4f7384dc62507ab/ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52", size = 58667, upload-time = "2026-06-14T22:36:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/b44a6afb77b94118655c029081b7932d64bb4c5b1c8ba2b7f5808b5d0bc2/ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d", size = 1038553, upload-time = "2026-06-14T22:36:21.245Z" }, + { url = "https://files.pythonhosted.org/packages/7e/93/fab1d786174c8780eb3e386c73f1925a435e97fbf77c957fea4fca83994d/ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469", size = 1197938, upload-time = "2026-06-14T22:36:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/2f073bb708f9d128f5d1cb39063a5f6421b1ce94c61be8661c55a189f407/ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8", size = 1090938, upload-time = "2026-06-14T22:36:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/cdaa50bba29d7dc9eb19212755b09bb96f56596e75957c3717c6b85454de/ujson-5.13.0-cp314-cp314t-win32.whl", hash = "sha256:74f3dd61aeb01b7b2a6754e400224e819279041b3867935a55ccf57fb86a43b2", size = 41802, upload-time = "2026-06-14T22:36:25.418Z" }, + { url = "https://files.pythonhosted.org/packages/bd/66/a6e669e90083febdf6c0600d3807f6017fd4d3962d5bd6ddc605c73a06e5/ujson-5.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5c31317d5e4504dae8f98795358b6082fc0ef96e7394806db0a76a4a8717f446", size = 42790, upload-time = "2026-06-14T22:36:26.614Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5f/fcc6c6a9d711fd8b020ca8ff65148212f0a712c809d173cd949e58de68c6/ujson-5.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aefd3c9c95f9b62348956396ff7b31818476f8f54dc4a4e64cbd4f0491db6fca", size = 40708, upload-time = "2026-06-14T22:36:27.721Z" }, + { url = "https://files.pythonhosted.org/packages/30/70/dbdd277d64bd3a149532567ceb082fe26f4ead58c39e0a97566ccbdf14a3/ujson-5.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3e074a1f7778d58aa3b3056bab7b6251aabb3f381808018ca2b7fb8dbdeef7ab", size = 58393, upload-time = "2026-06-14T22:36:28.702Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/592c70af94a67cafacd9c840ae2980f27d511dde2732a4c0dfac8f176ae8/ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bb53ef95d35875262b8d0aa28506ca612ddd07058bee2a90f609938e69dc801", size = 54447, upload-time = "2026-06-14T22:36:29.802Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9d/2bb91e1e25a8584cb3b63544b9bd26f621173535c77ac6cae13bad8e7904/ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb296a0aa480ab88d895ddaa90372604c08ccc72323f02590612c775426ab413", size = 56066, upload-time = "2026-06-14T22:36:30.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/ec/8e3802fc4a4e31e817b972bbb0e704a484d8c75ec349b3feb45fa9fb54c4/ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2862f81af44b3a7e74c5d80caa118d736be1991ce6f1d5c723716fa403060cc6", size = 54938, upload-time = "2026-06-14T22:36:32.051Z" }, + { url = "https://files.pythonhosted.org/packages/4a/48/d0e3e511039b86fd1ecfe2bf761c800552d273ef8f19e71de93bf38a909e/ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c16e07581172f08585b409246f4535dab13ee85af0e3d3cfa8684b653ca44fa8", size = 56115, upload-time = "2026-06-14T22:36:33.349Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/689613037fe691d18eae075cd141089f3a3156146be14512df92d8a9ae8f/ujson-5.13.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:9bd0f2dd05937c3b089af316884de18c6f6182ddb8ffce597d2e7c7a9ba9f447", size = 41802, upload-time = "2026-06-14T22:36:34.523Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7f/276f830bef4d530d50ff4d8f8c568002e7ebed9f64c06747b1d1c4325f02/ujson-5.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:96e7e019b097b4b25fccddadb369d13f412c13695fcf0680b6bf906376156151", size = 52479, upload-time = "2026-06-14T22:36:35.627Z" }, + { url = "https://files.pythonhosted.org/packages/fc/50/99b05555ef42a2ab2e26aa369c46bde6a64f8aeeb687628102e98cc78bed/ujson-5.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7892ea6dd85ede6d30fbbd22af1239b9d81dabdf9b7a8f10ca6d4464d4d9b8ab", size = 49953, upload-time = "2026-06-14T22:36:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/78/15/399766e8ba002bd8e5e2e45828e0e12a5dbeb4145d6a604ba3787db5eec2/ujson-5.13.0-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e1842e10adc8f0db0d3e8aa3a1f8b05ce0456b39e180c8553d7f36dd0bf24b6b", size = 55716, upload-time = "2026-06-14T22:36:37.833Z" }, + { url = "https://files.pythonhosted.org/packages/61/a6/825d92bce42cd442b57d89b4c20afc1737246321d3433c6194e5c35c6a11/ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8bbb1f5ab810954fb307b6c5e68af58210c31da8569f9e6498a3958c1859e72", size = 48648, upload-time = "2026-06-14T22:36:38.878Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/7ea7a32f35457560806375193dbc4f0d8ffd8c8adae42f86686392a76c41/ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f6d55c68985654a84a9b47ac51f2655adac4fd264e4189f832960c2270dcf70", size = 49947, upload-time = "2026-06-14T22:36:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/af/b4/0bd6449ae35b6026d94f4d1a32dbc9f40c969ed1d6e36a7e26ccf56371be/ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b266f182d4bce74a6a9e1f86988485e8cd422efdcc7c3f537e00cc956f52678", size = 51149, upload-time = "2026-06-14T22:36:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2d/39da479a8461d1d78d533940a8426a84e23708a6a7a426f2178e04443252/ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfaa8302eb9bb7f5e231f256caf83040760585e32751d19155c0f0c0225f8de1", size = 52456, upload-time = "2026-06-14T22:36:42.266Z" }, + { url = "https://files.pythonhosted.org/packages/12/cc/91ff81c50b85a158878f06d6e9153227bbc04209db291a152c286a0ee4a8/ujson-5.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:43dee3081b00fe447c5b9e2fe7ebfd57f7bcc5dd25b8a439c26c8c174dd581be", size = 41155, upload-time = "2026-06-14T22:36:43.455Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "uvicorn" version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -1820,3 +2979,111 @@ sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e66 wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] + +[[package]] +name = "wcmatch" +version = "8.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] + +[[package]] +name = "zizmor" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/a8/1c0c6619e213e6e1fd8323f09661542197448db2984649d2c34097997859/zizmor-1.27.0.tar.gz", hash = "sha256:ffaf8e1bb39447e643592d669761bfbc2aa636875db9079614f6a6c81cec60c8", size = 548896, upload-time = "2026-07-14T08:16:49.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/81/ba3dbd48a5e3d56d8a9693ba3dac9484df1b3f83862a453ade9f70ef21ff/zizmor-1.27.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0de272b6c19910f5bc6965ac7d4566af26db31571e983e3b567298389d0c84c7", size = 9090700, upload-time = "2026-07-14T08:16:31.04Z" }, + { url = "https://files.pythonhosted.org/packages/2a/18/960f032faa67427afb8b3e11877b7e911d1ccff3f3bb8bec0c416d4df42c/zizmor-1.27.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cc94158472428e04298b879ef36cb94360b753ece38fa2303464dd7dae07bc68", size = 8673964, upload-time = "2026-07-14T08:16:33.038Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2a/034e4d11d1ed81e23fc9e526391616848467f0db6faeaa9e1b88fb8560ec/zizmor-1.27.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:b70697ebe555a28fbd3deaa259936f059f64b9f8531020f5b1f3d99087d4f62a", size = 8777649, upload-time = "2026-07-14T08:16:34.831Z" }, + { url = "https://files.pythonhosted.org/packages/2e/27/8dce2cd076ee0c39b0d9ac129703400c9c1c21c71f1709da660e3769d628/zizmor-1.27.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:0bba3eff43f919b04b9b6365b4ef17437649e11eafb73f603407873b65ad01dd", size = 8367119, upload-time = "2026-07-14T08:16:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/21/64/4e21d26a2fc910594aaab19367ac25467a4da6438b7c381f67e90c38945e/zizmor-1.27.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:afb28123882d2b8248f1e480bf6cc6d1af102e0d3fbe22a40f7f795b1aa9d435", size = 9163603, upload-time = "2026-07-14T08:16:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/17ffd2884f8d74ce747c6ffe11481de5effb35079fd00a0725fab336e57d/zizmor-1.27.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e1042dd14fb4597a1e3007c7ef3f5a017305ca50cfacf43d603f3ae870c65eb1", size = 8807297, upload-time = "2026-07-14T08:16:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/af/3a347da3007655c7979b4b99d6a4d309ebc344715daa7c1357f17d00d680/zizmor-1.27.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:138e65d560e3875dbbbec50eae56a9e14707f8f4e006112174e4752155c01db9", size = 8333630, upload-time = "2026-07-14T08:16:42.814Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/921c74ed022fe8e1f599d7e7353cab63d550dda68e410096ed735b7388db/zizmor-1.27.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90004342c3c5122d7e4e8e27ac6d3c462b878da0ea3d408ff45fa7d768139a76", size = 9253035, upload-time = "2026-07-14T08:16:44.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/9cdb03c6c2341cf273c9c34506e5193cea7e73dc50b49ccf5f93a3579bdb/zizmor-1.27.0-py3-none-win32.whl", hash = "sha256:fe8947f635c925b83ef83fcd66de5f89a012b22784bb86fcd7e2a1f4e7648c9d", size = 7538509, upload-time = "2026-07-14T08:16:46.253Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/b924e569218646684d1e211f299282c0b9a0780d6b15f9e10e4a3fdaac11/zizmor-1.27.0-py3-none-win_amd64.whl", hash = "sha256:debc723721172c170d5922171a40eaebf5787a02ff3e69d30597dafdb66a21ba", size = 8643618, upload-time = "2026-07-14T08:16:47.835Z" }, +] From 8711197984ec705f6520c0368e68bc6e8b612406 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 23 Jul 2026 20:50:42 +0300 Subject: [PATCH 52/55] feat(infra): basic opentelemetry setup --- deploy/{ => keycloak}/keycloak.env | 0 deploy/{ => keycloak}/keycloak.json | 0 deploy/{ => observability}/alertmanager.yml | 0 deploy/{ => observability}/alerts.yml | 0 deploy/{ => observability}/blackbox.yml | 0 deploy/observability/collector.yaml | 31 + deploy/{ => observability}/prometheus.yml | 0 deploy/{ => persistence}/init.sql | 0 deploy/{ => persistence}/postgres.env | 0 docker-compose.base.yaml | 34 +- docker-compose.dev.yaml | 7 + example.config.toml | 4 + pyproject.toml | 6 + src/conode/bootstrap/api/run.py | 7 + .../bootstrap/di/providers/connection.py | 5 + src/conode/bootstrap/telemetry/__init__.py | 3 + src/conode/bootstrap/telemetry/otel.py | 41 + src/conode/infrastructure/config.py | 11 + .../infrastructure/persistence/types.py | 2 +- uv.lock | 1012 +++++------------ 20 files changed, 412 insertions(+), 751 deletions(-) rename deploy/{ => keycloak}/keycloak.env (100%) rename deploy/{ => keycloak}/keycloak.json (100%) rename deploy/{ => observability}/alertmanager.yml (100%) rename deploy/{ => observability}/alerts.yml (100%) rename deploy/{ => observability}/blackbox.yml (100%) create mode 100644 deploy/observability/collector.yaml rename deploy/{ => observability}/prometheus.yml (100%) rename deploy/{ => persistence}/init.sql (100%) rename deploy/{ => persistence}/postgres.env (100%) create mode 100644 src/conode/bootstrap/telemetry/__init__.py create mode 100644 src/conode/bootstrap/telemetry/otel.py diff --git a/deploy/keycloak.env b/deploy/keycloak/keycloak.env similarity index 100% rename from deploy/keycloak.env rename to deploy/keycloak/keycloak.env diff --git a/deploy/keycloak.json b/deploy/keycloak/keycloak.json similarity index 100% rename from deploy/keycloak.json rename to deploy/keycloak/keycloak.json diff --git a/deploy/alertmanager.yml b/deploy/observability/alertmanager.yml similarity index 100% rename from deploy/alertmanager.yml rename to deploy/observability/alertmanager.yml diff --git a/deploy/alerts.yml b/deploy/observability/alerts.yml similarity index 100% rename from deploy/alerts.yml rename to deploy/observability/alerts.yml diff --git a/deploy/blackbox.yml b/deploy/observability/blackbox.yml similarity index 100% rename from deploy/blackbox.yml rename to deploy/observability/blackbox.yml diff --git a/deploy/observability/collector.yaml b/deploy/observability/collector.yaml new file mode 100644 index 0000000..e123cf9 --- /dev/null +++ b/deploy/observability/collector.yaml @@ -0,0 +1,31 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: api:4317 + + +processors: + batch: + timeout: 5s + send_batch_size: 1024 + send_batch_max_size: 2048 + +exporters: + otlp: + endpoint: jaeger:4317 + tls: + insecure: true + + +service: + pipelines: + traces: + receivers: + - otlp + + processors: + - batch + + exporters: + - otlp \ No newline at end of file diff --git a/deploy/prometheus.yml b/deploy/observability/prometheus.yml similarity index 100% rename from deploy/prometheus.yml rename to deploy/observability/prometheus.yml diff --git a/deploy/init.sql b/deploy/persistence/init.sql similarity index 100% rename from deploy/init.sql rename to deploy/persistence/init.sql diff --git a/deploy/postgres.env b/deploy/persistence/postgres.env similarity index 100% rename from deploy/postgres.env rename to deploy/persistence/postgres.env diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml index 51c6adc..b192d3f 100644 --- a/docker-compose.base.yaml +++ b/docker-compose.base.yaml @@ -35,7 +35,7 @@ services: environment: OAUTH2_PROXY_CLIENT_ID: conode - OAUTH2_PROXY_CLIENT_SECRET: HxZkeEWaKX0J1rXNPpkshkhojgm6roqrNxWWtTBtUiwSm4kN1QzIH1cCcTulKPlYqCEFVELVuFu94tqFGDyjuo + OAUTH2_PROXY_CLIENT_SECRET: RELW6bj1AsUr3yQUmc4Nn3y5mGfS6qzjD7tCnkIuekGa9mMmU9B6vuCFVyvUL9mBDrRUJVVPSe42hTzbFN6Vqi OAUTH2_PROXY_COOKIE_SECRET: x3CpXUQxfDgXzDDxIdST3mTbTDosKOlx volumes: @@ -83,7 +83,7 @@ services: condition: service_healthy env_file: - - "./deploy/keycloak.env" + - "./deploy/keycloak/keycloak.env" healthcheck: test: @@ -104,7 +104,7 @@ services: volumes: - keycloak-data:/opt/keycloak/ - - ./deploy/keycloak.json:/opt/keycloak/data/import/realm-export.json + - ./deploy/keycloak/keycloak.json:/opt/keycloak/data/import/realm-export.json networks: - conode-network @@ -114,7 +114,7 @@ services: volumes: - postgres-data:/var/lib/postgresql/data - - ./deploy:/docker-entrypoint-initdb.d:ro + - ./deploy/persistence:/docker-entrypoint-initdb.d:ro networks: conode-network: @@ -122,7 +122,7 @@ services: - persistence.literally.io env_file: - - "./deploy/postgres.env" + - "./deploy/persistence/postgres.env" healthcheck: test: ["CMD", "pg_isready", "-U", "conode", "-d", "conode"] @@ -130,6 +130,30 @@ services: timeout: 15s retries: 5 + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + + environment: + - COLLECTOR_OTLP_ENABLED=true + + restart: unless-stopped + networks: + - conode-network + + collector: + image: otel/opentelemetry-collector:0.156.0 + command: --config=/etc/otelcol/config.yaml + + depends_on: + - jaeger + + volumes: + - ./deploy/observability/collector.yaml:/etc/otelcol/config.yaml:ro + + networks: + - conode-network + volumes: postgres-data: keycloak-data: diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 8383f94..0d27053 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -9,6 +9,13 @@ services: ports: - 5432:5432 + jaeger: + labels: + - "traefik.enable=true" + - "traefik.http.routers.jaeger.rule=Host(`jaeger.conode.com`)" + - "traefik.http.routers.jaeger.entrypoints=web" + - "traefik.http.services.jaeger.loadbalancer.server.port=16686" + traefik: command: - "--log.level=DEBUG" diff --git a/example.config.toml b/example.config.toml index 6ed3150..56b03d8 100644 --- a/example.config.toml +++ b/example.config.toml @@ -15,3 +15,7 @@ expires_in = 3600 jwks_url = "http://keycloak:8080/realms/conode/protocol/openid-connect/certs" issuer = "http://keycloak.conode.com/realms/conode" audience = "conode" + +[telemetry] +endpoint = "collector:4317" +enabled = true \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7986439..38df14e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,12 @@ dependencies = [ "cryptography==46.0.5", "dishka==1.7.2", "fastapi==0.128.0", + "opentelemetry-api>=1.44.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.44.0", + "opentelemetry-instrumentation-fastapi>=0.65b0", + "opentelemetry-instrumentation-logging>=0.65b0", + "opentelemetry-instrumentation-sqlalchemy>=0.65b0", + "opentelemetry-sdk>=1.44.0", "pydantic[email]==2.13.2", "pyjwt==2.11.0", "sqlalchemy==2.0.46", diff --git a/src/conode/bootstrap/api/run.py b/src/conode/bootstrap/api/run.py index 10eeead..725e3f7 100644 --- a/src/conode/bootstrap/api/run.py +++ b/src/conode/bootstrap/api/run.py @@ -5,9 +5,11 @@ from dishka.integrations.fastapi import setup_dishka from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from conode.bootstrap.di import get_async_container from conode.bootstrap.logs import configure_structlog +from conode.bootstrap.telemetry import configure_telemetry from conode.infrastructure.config import Config, load_config from conode.presentation.common import ( include_exception_handlers, @@ -52,9 +54,14 @@ def run_http(_argv: list[str]) -> None: config = load_config() app = create_app(config) + + configure_telemetry(config.otel) + container = get_async_container(config) log_configuration = configure_structlog() + FastAPIInstrumentor().instrument_app(app) + setup_dishka(app=app, container=container) uvicorn.run( diff --git a/src/conode/bootstrap/di/providers/connection.py b/src/conode/bootstrap/di/providers/connection.py index 094a790..2d3cd23 100644 --- a/src/conode/bootstrap/di/providers/connection.py +++ b/src/conode/bootstrap/di/providers/connection.py @@ -1,6 +1,7 @@ from collections.abc import AsyncIterator from dishka import Provider, Scope, WithParents, provide, provide_all +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from sqlalchemy import URL from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -32,6 +33,10 @@ async def provide_async_engine( max_overflow=96, pool_timeout=30, ) + SQLAlchemyInstrumentor().instrument( + engine=engine.sync_engine, + enable_commenter=True, + ) yield async_sessionmaker( engine, expire_on_commit=False, diff --git a/src/conode/bootstrap/telemetry/__init__.py b/src/conode/bootstrap/telemetry/__init__.py new file mode 100644 index 0000000..110b1ef --- /dev/null +++ b/src/conode/bootstrap/telemetry/__init__.py @@ -0,0 +1,3 @@ +from .otel import configure_telemetry + +__all__ = ("configure_telemetry",) diff --git a/src/conode/bootstrap/telemetry/otel.py b/src/conode/bootstrap/telemetry/otel.py new file mode 100644 index 0000000..81077a7 --- /dev/null +++ b/src/conode/bootstrap/telemetry/otel.py @@ -0,0 +1,41 @@ +import os +from uuid import uuid4 + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from conode.infrastructure.config import OTELConfig + + +def configure_telemetry(config: OTELConfig) -> None: + if not config.enabled: + return + + resource = Resource.create( + { + SERVICE_NAME: "conode", + SERVICE_INSTANCE_ID: f"conode-{os.getpid()}-{uuid4().hex[:8]}", + }, + ) + + provider = TracerProvider( + resource=resource, + ) + + exporter = OTLPSpanExporter( + endpoint=config.endpoint, + insecure=True, + ) + + processor = BatchSpanProcessor( + exporter, + ) + + provider.add_span_processor(processor) + + trace.set_tracer_provider(provider) diff --git a/src/conode/infrastructure/config.py b/src/conode/infrastructure/config.py index 48b08d2..cafaf40 100644 --- a/src/conode/infrastructure/config.py +++ b/src/conode/infrastructure/config.py @@ -30,11 +30,18 @@ class DatabaseConfig: port: int +@dataclass(slots=True, frozen=True, kw_only=True) +class OTELConfig: + endpoint: str + enabled: bool + + @dataclass(slots=True, frozen=True, kw_only=True) class Config: api: APIConfig database: DatabaseConfig secrets: SecretsConfig + otel: OTELConfig def load_config(path: str = "config.toml") -> Config: @@ -61,4 +68,8 @@ def load_config(path: str = "config.toml") -> Config: audience=config["secrets"]["audience"], issuer=config["secrets"]["issuer"], ), + otel=OTELConfig( + enabled=config["telemetry"]["enabled"], + endpoint=config["telemetry"]["endpoint"], + ), ) diff --git a/src/conode/infrastructure/persistence/types.py b/src/conode/infrastructure/persistence/types.py index 56aa9d0..c0527c7 100644 --- a/src/conode/infrastructure/persistence/types.py +++ b/src/conode/infrastructure/persistence/types.py @@ -25,7 +25,7 @@ def process_bind_param(self, value: T | None, dialect: Dialect) -> Any: @override def process_result_value(self, value: Any | None, dialect: Dialect) -> T | None: - return self.vo_class(value) if value else None + return self.vo_class(value) if value is not None else None class UsernameType(BaseVOTypeDecorator[Username]): diff --git a/uv.lock b/uv.lock index 4e2e906..82b9165 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,8 @@ requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", "python_full_version == '3.14.*'", - "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] @@ -15,7 +16,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } @@ -46,7 +47,7 @@ name = "anyio" version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -112,6 +113,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, ] +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + [[package]] name = "ast-serialize" version = "0.4.0" @@ -222,28 +235,11 @@ wheels = [ name = "attrs" version = "21.4.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] sdist = { url = "https://files.pythonhosted.org/packages/d7/77/ebb15fc26d0f815839ecd897b919ed6d85c050feeb83e100e020df9153d2/attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd", size = 201839, upload-time = "2021-12-29T13:15:09.056Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/be/7abce643bfdf8ca01c48afa2ddf8308c2308b0c3b239a44e57d020afa0ef/attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", size = 60567, upload-time = "2021-12-29T13:15:06.703Z" }, ] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -273,16 +269,13 @@ wheels = [ name = "black" version = "22.12.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "mypy-extensions", marker = "python_full_version < '3.14'" }, - { name = "pathspec", marker = "python_full_version < '3.14'" }, - { name = "platformdirs", marker = "python_full_version < '3.14'" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/59/e873cc6807fb62c11131e5258ca15577a3b7452abad08dc49286cf8245e8/black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", size = 553112, upload-time = "2022-12-09T15:57:04.428Z" } wheels = [ @@ -293,52 +286,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/51/1f7f93c0555eaf4cbb628e26ba026e3256174a45bd9397ff1ea7cf96bad5/black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", size = 167343, upload-time = "2022-12-09T15:57:02.229Z" }, ] -[[package]] -name = "black" -version = "26.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.14'" }, - { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "pathspec", marker = "python_full_version >= '3.14'" }, - { name = "platformdirs", marker = "python_full_version >= '3.14'" }, - { name = "pytokens", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, - { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, -] - [[package]] name = "boltons" version = "21.0.0" @@ -544,7 +491,7 @@ resolution-markers = [ "python_full_version == '3.14.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ @@ -556,11 +503,12 @@ name = "click" version = "8.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ @@ -597,33 +545,37 @@ dependencies = [ { name = "alembic" }, { name = "argon2-cffi" }, { name = "asyncpg" }, - { name = "bandit" }, { name = "cryptography" }, { name = "dishka" }, { name = "fastapi" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-instrumentation-sqlalchemy" }, + { name = "opentelemetry-sdk" }, { name = "pydantic", extra = ["email"] }, { name = "pyjwt" }, - { name = "semgrep", version = "0.112.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "semgrep", version = "1.156.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "sqlalchemy" }, { name = "structlog" }, { name = "uvicorn" }, - { name = "zizmor" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ + { name = "bandit" }, { name = "datamodel-code-generator" }, { name = "dirty-equals" }, { name = "httpx" }, { name = "mypy" }, { name = "polyfactory" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "ruff" }, + { name = "semgrep" }, + { name = "zizmor" }, ] [package.metadata] @@ -631,29 +583,38 @@ requires-dist = [ { name = "alembic", specifier = "==1.18.4" }, { name = "argon2-cffi", specifier = "==25.1.0" }, { name = "asyncpg", specifier = "==0.31.0" }, - { name = "bandit", specifier = ">=1.9.4" }, { name = "cryptography", specifier = "==46.0.5" }, - { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.54.0" }, - { name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.11" }, { name = "dishka", specifier = "==1.7.2" }, { name = "fastapi", specifier = "==0.128.0" }, - { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, - { name = "mypy", marker = "extra == 'dev'" }, - { name = "polyfactory", marker = "extra == 'dev'", specifier = "==3.3.0" }, + { name = "opentelemetry-api", specifier = ">=1.44.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.44.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.65b0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.44.0" }, { name = "pydantic", extras = ["email"], specifier = "==2.13.2" }, { name = "pyjwt", specifier = "==2.11.0" }, - { name = "pytest", marker = "extra == 'dev'" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, - { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, - { name = "ruff", marker = "extra == 'dev'" }, - { name = "semgrep", specifier = ">=0.112.1" }, { name = "sqlalchemy", specifier = "==2.0.46" }, { name = "structlog", specifier = "==25.5.0" }, { name = "uvicorn", specifier = "==0.40.0" }, - { name = "zizmor", specifier = ">=1.27.0" }, ] -provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = "==1.9.4" }, + { name = "datamodel-code-generator", specifier = "==0.54.0" }, + { name = "dirty-equals", specifier = "==0.11" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "mypy" }, + { name = "polyfactory", specifier = "==3.3.0" }, + { name = "pytest" }, + { name = "pytest-asyncio", specifier = "==1.3.0" }, + { name = "pytest-cov", specifier = "==7.1.0" }, + { name = "pytest-xdist", specifier = "==3.8.0" }, + { name = "ruff" }, + { name = "semgrep", specifier = "==0.112.1" }, + { name = "zizmor", specifier = "==1.27.0" }, +] [[package]] name = "coverage" @@ -755,7 +716,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -824,17 +785,15 @@ version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, - { name = "black", version = "22.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "black", version = "26.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "black" }, { name = "genson" }, { name = "inflect" }, { name = "isort" }, { name = "jinja2" }, - { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "packaging" }, { name = "pydantic" }, { name = "pyyaml" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "tomli", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/43/2640cd5293fb5430528166908d6439cf3321bc1c54de5fe58ef100b143a1/datamodel_code_generator-0.54.0.tar.gz", hash = "sha256:2b183598d049e265146a8224c35d1bb96a80a641ea8ecd2a82e6a0e97b56da6b", size = 824327, upload-time = "2026-02-14T16:19:05.347Z" } wheels = [ @@ -864,7 +823,7 @@ name = "dishka" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/d7/1be31f5ef32387059190353f9fa493ff4d07a1c75fa856c7566ca45e0800/dishka-1.7.2.tar.gz", hash = "sha256:47d4cb5162b28c61bf5541860e605ed5eaf5c667122299c7ef657c86fc8d5a49", size = 68132, upload-time = "2025-09-24T21:23:05.135Z" } wheels = [ @@ -893,28 +852,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -982,44 +925,22 @@ wheels = [ name = "glom" version = "22.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ - { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "boltons", marker = "python_full_version < '3.14'" }, - { name = "face", marker = "python_full_version < '3.14'" }, + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/69432deefa6f5283ec75b246d0540097ae26f618b915519ee3824c4c5dd6/glom-22.1.0.tar.gz", hash = "sha256:1510c6587a8f9c64a246641b70033cbc5ebde99f02ad245693678038e821aeb5", size = 189738, upload-time = "2022-01-24T09:34:04.874Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/e8/68e274b2a30e1fdfd25bdc27194382be3f233929c8f727c0440d58ac074f/glom-22.1.0-py2.py3-none-any.whl", hash = "sha256:5339da206bf3532e01a83a35aca202960ea885156986d190574b779598e9e772", size = 100687, upload-time = "2022-01-24T09:34:02.391Z" }, ] -[[package]] -name = "glom" -version = "25.12.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -dependencies = [ - { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "boltons", marker = "python_full_version >= '3.14'" }, - { name = "face", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ @@ -1080,6 +1001,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, ] +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/14/5d05bfd85c101cbe44a12d7c1cea9c40698e0438cddf3a70019f735b5a27/grpcio-1.82.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17", size = 6177087, upload-time = "2026-07-08T12:34:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/c906f8e6d0b54c0137885fff6f7b5883c6bbc381b44a0ba5ea07d7d1579b/grpcio-1.82.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31", size = 11960907, upload-time = "2026-07-08T12:34:10.583Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/ec4aa76cdf25539b9e960cbb9d5739f892ea6cde58078b5293860c1159d3/grpcio-1.82.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560", size = 6754802, upload-time = "2026-07-08T12:34:13.082Z" }, + { url = "https://files.pythonhosted.org/packages/e6/dd/47519c2a8fd9db47ec4493f44bd9f5b0175307e07089b1132e54b7b5b19c/grpcio-1.82.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6e8a08f7038ba7a77f71e250804e4aba84fe91d22cfc54ff43c07b7529c4728", size = 7484535, upload-time = "2026-07-08T12:34:15.164Z" }, + { url = "https://files.pythonhosted.org/packages/63/99/659711e9689c4dd553bcd4eacff9cb9f458f34b60edf7afb3bbc1b0a58a2/grpcio-1.82.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50fd2fe83426b1b1c6cdc4d72d555223b7dddf8ce07c5bac218b13fc6d684c6f", size = 6919066, upload-time = "2026-07-08T12:34:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/f2b772356b4f593ffe439795509fcbf675b0ff98211ae8ce2a180f2e559f/grpcio-1.82.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b758540a24d5394a9c578bf9f6126389f474b106ac3d9df1d53de56cb14c9fd9", size = 7525855, upload-time = "2026-07-08T12:34:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/b28cfffb989a84d8272593498bddd2d68148cce1813ad55189c469b0f1f8/grpcio-1.82.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4ba4aac238f685743575d9d700003ac16537cce26e7c774993134f530652464", size = 8565122, upload-time = "2026-07-08T12:34:21.951Z" }, + { url = "https://files.pythonhosted.org/packages/97/f9/54956cb0c701190cbc9d7e535c3f84acf0285c6b9ed198a902766e17c3cd/grpcio-1.82.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed6fc621d6f366c88a60f0b971d5afd21d441d9aa561ee688de5b7acdb2cf901", size = 7933872, upload-time = "2026-07-08T12:34:24.539Z" }, + { url = "https://files.pythonhosted.org/packages/76/85/5f9cd1f965bbe4329556a212f178ae0c072b18b446cae05ed32fa8847c53/grpcio-1.82.1-cp310-cp310-win32.whl", hash = "sha256:bd2f45e46fff5b91c10997d0743a987517a7dde67c64c592835c2dcaac66f587", size = 4257373, upload-time = "2026-07-08T12:34:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/c4f42f7c69c53d27ed41643421b55908bcbe885b68f5a208135c72917c98/grpcio-1.82.1-cp310-cp310-win_amd64.whl", hash = "sha256:5e171d5f0d6a0af78ea7512783f170a44f80c165259d8773e3a354a7f991f2b5", size = 5006571, upload-time = "2026-07-08T12:34:28.778Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3e/496992d08c0aaa11272eb6228dc8ab947da01fe835de243cd00521bce4c4/grpcio-1.82.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b454a2d97bfab7565683a02345f86bd182ab69fd7c2bdb7414171e7538f266b1", size = 6146068, upload-time = "2026-07-08T12:35:21.365Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8f/f263d6f14fdba6b56cfadd91fd3e158a52682b72c6016d1f8723d435659f/grpcio-1.82.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3dde70abfc80b3be11de53ba0d601c439e7fb2afd3583ad1788d1146bec92fdc", size = 11948600, upload-time = "2026-07-08T12:35:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/8c/14/3a02e6ee49c2d85bc15eaae321e0e11ab3542cad3c5b2de121ecce0c4296/grpcio-1.82.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5523099c98c292ea1ae08e617249db760c56a78f8deae879027fe7d1ffbcbf6", size = 6714591, upload-time = "2026-07-08T12:35:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/58e3738696f48ab7645347b98d8a7f93d10e00e6218388fbfcd6c9310e3d/grpcio-1.82.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5e5c4dc0a59b0f8490a6bdfd6fc8395b9d8ad8a8407c7d67ca7b5bba15c0877f", size = 7454995, upload-time = "2026-07-08T12:35:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6c/2557c1a889363072fbf2285ecd0e8c44860d4dbd60f017a32537c5b863e2/grpcio-1.82.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c40d94ba820329cc191981bc22fa6f6eed0799c6d921f3c6709521d59d4a2fd7", size = 6888621, upload-time = "2026-07-08T12:35:32.38Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/907706ccaff1223f1e10fd5b37fc16faead43392fccb4e786e7e390ac141/grpcio-1.82.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c816180e31e273caaec6f8bd86a8392499d5bbb26f41da44e3dce48bde69095", size = 7505069, upload-time = "2026-07-08T12:35:35.072Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/ff97b0d0f635987ee5ec80dfedafa1aad629303745d48e8637d10eec5b80/grpcio-1.82.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31fd780b261830720cb70b0fd8f0aa51d49e75a66d7464ad2e31d4b765f2580", size = 8535384, upload-time = "2026-07-08T12:35:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/a97fddd970a8d1588cade06eca20443761c1858b0ad6590a5c835aa18062/grpcio-1.82.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d76152d7c31d7210d4a106e5d8b64da5bba5d6abf11be30e2f7b0a0c59bbcbf", size = 7910707, upload-time = "2026-07-08T12:35:40.797Z" }, + { url = "https://files.pythonhosted.org/packages/20/e4/eaba1517888af483a88d449eb7566f0f7f63446d46f339c5891798435875/grpcio-1.82.1-cp313-cp313-win32.whl", hash = "sha256:38e9dcb5258226fb3282630b31b16a968df52c8c6ad514af540646e0a4578f8a", size = 4240363, upload-time = "2026-07-08T12:35:43.298Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/66a98d47732e35290bef722f6149fed3709cd4cf61166f6f53a12f417302/grpcio-1.82.1-cp313-cp313-win_amd64.whl", hash = "sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90", size = 5000980, upload-time = "2026-07-08T12:35:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cb/cf9ae9e164c6e6dc8a494faa9771763df9da150eefe19671009624d1559f/grpcio-1.82.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:35f990f7784c8fd2872644f07f96ebb4d9e48e145a190ab80d0280af91a1bfb2", size = 6146901, upload-time = "2026-07-08T12:35:49.261Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/eccf26dbcfb7f7cab8027c5490a16c8937c5aa7a2ec20a3eab2cf7a43165/grpcio-1.82.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:46536a4a1f4434df3c851b9254ff6fc7df5705b273681a15ca277d5921c178a0", size = 11954756, upload-time = "2026-07-08T12:35:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/75/3b3b4a3cc9f084b026af96e1d3e539b1af29ec7f41ed0dfff3cb99cc8626/grpcio-1.82.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d6650a7c1ebb7921c70e12a385439a8118efb99e669fa9ed31cf25db1843937c", size = 6723087, upload-time = "2026-07-08T12:35:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/b0f0c9b1400a99a4da4c09b114f101b192f8f11192e76f620b8962f5d90b/grpcio-1.82.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8e110c66df5204c0506d6c8787b35d48b8b699ef5aa366d6c4d67325c67fe9a", size = 7454542, upload-time = "2026-07-08T12:35:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bd/428e38868382aa193697a5aa53973f29c58e58ba4268aa0c86a2715ee58b/grpcio-1.82.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f853eae07235a51a27bb5d6a9a175a59ca55dc9b99edc6ce2f76f07332d333ae", size = 6889588, upload-time = "2026-07-08T12:36:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/03e01d5e10259bf5c08ee50570cc94724e79c956f61fd2f09b341af0956c/grpcio-1.82.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:60b0f2c95337694fc094b77d9f60f50566c84b5677393e342eb98daeee242d98", size = 7514166, upload-time = "2026-07-08T12:36:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/ff/59/278b4b600329e2ba3849f3c1ea3c820b3a01b38a7ad184ba09595e8d2733/grpcio-1.82.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b064fc444812bdaa9825d33c26f8d732d63ee6a5d78557c1faf92c98687fed27", size = 8536166, upload-time = "2026-07-08T12:36:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/27/7ccf2ef00f27a8e47a79d641c8ceaf7d3028c7a03d9a97b4c8a9a783c086/grpcio-1.82.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d7ede11d747b4e1bd05e3bc0260e155b65a88735a895a10f6521f19b889511e", size = 7912572, upload-time = "2026-07-08T12:36:08.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/33742482d2753f2d3a1b7641664b6622262d44f2f3b609f13425dd86d36f/grpcio-1.82.1-cp314-cp314-win32.whl", hash = "sha256:3d21f19838dc255ecbb79321b15ae9b98fbddff4c3d4aedb0a81bdd7f4ab572a", size = 4321856, upload-time = "2026-07-08T12:36:10.899Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/03329c847172c78ddeb1eb9be6b444fdbc12775a84c958b27e427e7b926d/grpcio-1.82.1-cp314-cp314-win_amd64.whl", hash = "sha256:e20f1edbb15f99e3128ec86433f9785fd5a451d8f115e74fe0056134f092a9d5", size = 5141114, upload-time = "2026-07-08T12:36:13.595Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1117,15 +1099,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - [[package]] name = "idna" version = "3.15" @@ -1135,18 +1108,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "inflect" version = "7.5.0" @@ -1194,50 +1155,15 @@ wheels = [ name = "jsonschema" version = "4.17.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ - { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pyrsistent", marker = "python_full_version < '3.14'" }, + { name = "attrs" }, + { name = "pyrsistent" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785, upload-time = "2022-11-29T20:37:47.453Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/97/c698bd9350f307daad79dd740806e1a59becd693bd11443a0f531e3229b3/jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6", size = 90379, upload-time = "2022-11-29T20:37:45.842Z" }, ] -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -dependencies = [ - { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "jsonschema-specifications", marker = "python_full_version >= '3.14'" }, - { name = "referencing", marker = "python_full_version >= '3.14'" }, - { name = "rpds-py", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "librt" version = "0.11.0" @@ -1432,31 +1358,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mcp" -version = "1.23.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.14'" }, - { name = "httpx", marker = "python_full_version >= '3.14'" }, - { name = "httpx-sse", marker = "python_full_version >= '3.14'" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "pydantic", marker = "python_full_version >= '3.14'" }, - { name = "pydantic-settings", marker = "python_full_version >= '3.14'" }, - { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.14'" }, - { name = "python-multipart", marker = "python_full_version >= '3.14'" }, - { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, - { name = "sse-starlette", marker = "python_full_version >= '3.14'" }, - { name = "starlette", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.14'" }, - { name = "uvicorn", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1484,7 +1385,7 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } @@ -1545,168 +1446,183 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.37.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.37.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-proto", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.37.0" +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-proto", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-sdk", marker = "python_full_version >= '3.14'" }, - { name = "requests", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.58b0" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, - { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "wrapt", marker = "python_full_version >= '3.14'" }, + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" }, ] [[package]] -name = "opentelemetry-instrumentation-requests" -version = "0.58b0" +name = "opentelemetry-instrumentation-fastapi" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-util-http", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" }, ] [[package]] -name = "opentelemetry-instrumentation-threading" -version = "0.58b0" +name = "opentelemetry-instrumentation-logging" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.14'" }, - { name = "wrapt", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/9d/72af21527ce6f286ac78dc2c75ce44b76cc45343a28f0bcbb13f213421fc/opentelemetry_instrumentation_sqlalchemy-0.65b0.tar.gz", hash = "sha256:8ec2e79f1e00808c5dc639ab5b2cfcdf9dcdef55efd6442c6019707fe2894028", size = 18007, upload-time = "2026-07-16T15:26:19.197Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/94/3eb3195a60b894cc1852a5657a2b64764a998085c50d7fb3452148af8f18/opentelemetry_instrumentation_sqlalchemy-0.65b0-py3-none-any.whl", hash = "sha256:4d8a2e5afc7b505a48d05cb1fb6db5f8b31681814c1d7767bd6d4b5bdd3a3047", size = 14410, upload-time = "2026-07-16T15:25:32.04Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.37.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.37.0" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.58b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.58b0" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" }, ] [[package]] name = "packaging" version = "21.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ - { name = "pyparsing", marker = "python_full_version < '3.14'" }, + { name = "pyparsing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/9e/d1a7217f69310c1db8fdf8ab396229f55a699ce34a203691794c5d1cad0c/packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", size = 84848, upload-time = "2021-11-18T00:39:13.586Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/8e/8de486cbd03baba4deef4142bd643a3e7bbe954a784dc1bb17142572d127/packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522", size = 40750, upload-time = "2021-11-18T00:39:10.932Z" }, ] -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - [[package]] name = "pathspec" version = "1.1.1" @@ -1758,17 +1674,17 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.6" +version = "7.35.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, ] [[package]] @@ -1916,20 +1832,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/f3/0a7087e5f861d66ca64ce927230b397cc264c87b712156e6a93b26a459c8/pydantic_core-2.46.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:154dbfdfb11b8cbd8ff4d00d0b81e3d19f4cb4bedd5aa9f091060ba071474c6a", size = 2192159, upload-time = "2026-04-17T09:11:20.123Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.14'" }, - { name = "python-dotenv", marker = "python_full_version >= '3.14'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1948,11 +1850,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] -[package.optional-dependencies] -crypto = [ - { name = "cryptography", marker = "python_full_version >= '3.14'" }, -] - [[package]] name = "pyparsing" version = "3.3.2" @@ -1993,52 +1890,27 @@ wheels = [ name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, - { name = "exceptiongroup", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "iniconfig", marker = "python_full_version < '3.14'" }, - { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pluggy", marker = "python_full_version < '3.14'" }, - { name = "pygments", marker = "python_full_version < '3.14'" }, - { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, - { name = "iniconfig", marker = "python_full_version >= '3.14'" }, - { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "pluggy", marker = "python_full_version >= '3.14'" }, - { name = "pygments", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - [[package]] name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } @@ -2053,8 +1925,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -2067,105 +1938,25 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - [[package]] name = "python-lsp-jsonrpc" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ujson", marker = "python_full_version < '3.14'" }, + { name = "ujson" }, ] sdist = { url = "https://files.pythonhosted.org/packages/99/45/1c2a272950679af529f7360af6ee567ef266f282e451be926329e8d50d84/python-lsp-jsonrpc-1.0.0.tar.gz", hash = "sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd", size = 10011, upload-time = "2021-04-14T21:19:20.659Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/ee/754bfd5f6bfe7162c10d3ecb0aeef6f882f91d3231596c83f761a75efd0b/python_lsp_jsonrpc-1.0.0-py3-none-any.whl", hash = "sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7", size = 8507, upload-time = "2021-04-14T21:19:18.698Z" }, ] -[[package]] -name = "python-multipart" -version = "0.0.32" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, -] - -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2230,19 +2021,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "rpds-py", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "requests" version = "2.34.2" @@ -2251,8 +2029,7 @@ dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -2272,137 +2049,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] -[[package]] -name = "rpds-py" -version = "2026.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, - { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, - { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, - { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, - { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, - { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, - { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, - { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, - { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, - { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, - { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, - { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, - { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, - { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, - { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, - { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, - { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, - { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, - { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, - { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, - { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, - { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, - { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, - { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, - { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, - { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, - { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, - { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, - { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, - { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, - { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, - { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, - { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, - { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, - { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, - { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, - { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, - { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, - { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, - { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, - { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, -] - [[package]] name = "ruamel-yaml" version = "0.17.40" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, ] @@ -2411,19 +2061,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/79/5e2cffa1c77432f11cd93a5351f30732c997a239d3a3090856a72d6d8ba7/ruamel.yaml-0.17.40-py3-none-any.whl", hash = "sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c", size = 113666, upload-time = "2023-10-20T12:53:52.628Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruamel-yaml-clib" version = "0.2.14" @@ -2503,41 +2140,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] -[[package]] -name = "semantic-version" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, -] - [[package]] name = "semgrep" version = "0.112.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] dependencies = [ - { name = "attrs", version = "21.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "boltons", marker = "python_full_version < '3.14'" }, + { name = "attrs" }, + { name = "boltons" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "click-option-group", marker = "python_full_version < '3.14'" }, - { name = "colorama", marker = "python_full_version < '3.14'" }, - { name = "defusedxml", marker = "python_full_version < '3.14'" }, - { name = "glom", version = "22.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "jsonschema", version = "4.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "packaging", version = "21.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "peewee", marker = "python_full_version < '3.14'" }, - { name = "python-lsp-jsonrpc", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "ruamel-yaml", version = "0.17.40", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "tqdm", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, - { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "wcmatch", marker = "python_full_version < '3.14'" }, + { name = "click-option-group" }, + { name = "colorama" }, + { name = "defusedxml" }, + { name = "glom" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "peewee" }, + { name = "python-lsp-jsonrpc" }, + { name = "requests" }, + { name = "ruamel-yaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wcmatch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/00/6850f808965fc531c7287e7b8ac71daa13ce6c819ac23acb35beff5c2710/semgrep-0.112.1.tar.gz", hash = "sha256:2140a0fd16f3ac0a0e330980613ba65fe58faea7808a965e219c4199bab96102", size = 195076, upload-time = "2022-09-08T02:11:29.173Z" } wheels = [ @@ -2546,54 +2171,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/2d/091319e7146c6dc33bf8123e5e8186803bd330f1f7b3ac7995c2fa95933f/semgrep-0.112.1-cp37.cp38.cp39.py37.py38.py39-none-macosx_11_0_arm64.whl", hash = "sha256:bc2437f12e41ac5e28a303e463098565a3d948c7744f6745bdb0fea341ccdae1", size = 25508160, upload-time = "2022-09-08T02:11:26.813Z" }, ] -[[package]] -name = "semgrep" -version = "1.156.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -dependencies = [ - { name = "attrs", version = "26.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "boltons", marker = "python_full_version >= '3.14'" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "click-option-group", marker = "python_full_version >= '3.14'" }, - { name = "colorama", marker = "python_full_version >= '3.14'" }, - { name = "exceptiongroup", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "glom", version = "25.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "mcp", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-api", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-instrumentation-requests", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-instrumentation-threading", marker = "python_full_version >= '3.14'" }, - { name = "opentelemetry-sdk", marker = "python_full_version >= '3.14'" }, - { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "peewee", marker = "python_full_version >= '3.14'" }, - { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.14'" }, - { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, - { name = "requests", marker = "python_full_version >= '3.14'" }, - { name = "rich", marker = "python_full_version >= '3.14'" }, - { name = "ruamel-yaml", version = "0.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "ruamel-yaml-clib", marker = "python_full_version >= '3.14'" }, - { name = "semantic-version", marker = "python_full_version >= '3.14'" }, - { name = "tomli", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "wcmatch", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/8d3079d353686db515a1188d6110301e1edff75abcffbfaee99a55f21344/semgrep-1.156.0.tar.gz", hash = "sha256:6c58c499ad86385c4786f04fe387d788113f1b9c95bc7b4c38941bf078cf5d9a", size = 52953549, upload-time = "2026-03-17T21:53:47.818Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/d4/9f8efd20f96cb9ff74d5dd013b1938b9246001c4b61e8e98501f236af71a/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:5bd2924958af5d4e199fe82c4ac9c7be4ae4fd20a3a46ce9f71237f8408f5b66", size = 42194313, upload-time = "2026-03-17T21:53:25.492Z" }, - { url = "https://files.pythonhosted.org/packages/66/a9/3d4082f30bca4ba738d391e241174c2297be87bb5ca37a3911b164289694/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:ff57b35def987ec3f21748051fdb89ae57574984bf8108b03d79473da49e93f0", size = 45891219, upload-time = "2026-03-17T21:53:28.903Z" }, - { url = "https://files.pythonhosted.org/packages/60/32/dca58fbb76a0d5ba16ed366e3478c50c737bd3da8788eb0d213d60562075/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7e9ae909fa8c26220c88392af9c22479c0ef51e170eeda8b2c81382b431afb76", size = 60020097, upload-time = "2026-03-17T21:53:31.923Z" }, - { url = "https://files.pythonhosted.org/packages/92/0c/b00156bee40cf9e594c2bd10672232e33d9a7d1bc8b5a6dd697097c8d6be/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:a7f68544bcd33fac9bc519e6a6c4759d612836422ce87bc08dd014481c3e8fd0", size = 57680875, upload-time = "2026-03-17T21:53:34.967Z" }, - { url = "https://files.pythonhosted.org/packages/66/9d/1a82d535e5a54e9617efcf3b8e9ffecff091566471946dd9369cd586c3bd/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_0_aarch64.whl", hash = "sha256:9604dddeebee8a3acc89f6ac25ad646e222a753a04fb944c2c5635f615b3a242", size = 60020098, upload-time = "2026-03-17T21:53:38.203Z" }, - { url = "https://files.pythonhosted.org/packages/0d/3c/3d48471dbfcb66cfb22b175d2221bc538355edea988f62cf1abb886afd8c/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_0_x86_64.whl", hash = "sha256:b68d14aced6c6f389fc7524e314b5638aa11ba50f35d3d3d97488e0930a21beb", size = 57680873, upload-time = "2026-03-17T21:53:41.211Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3d/f6f42ee18039e0f655ef5ddcd68ef2447b4c922178879fd1e9a75b846d41/semgrep-1.156.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:a9a683f94f99629d345ac73a215859fc5ce6c58fe67ad2f78671de3874a260e3", size = 53820998, upload-time = "2026-03-17T21:53:44.318Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.46" @@ -2650,19 +2227,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] -[[package]] -name = "sse-starlette" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.14'" }, - { name = "starlette", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, -] - [[package]] name = "starlette" version = "0.50.0" @@ -2695,7 +2259,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", "python_full_version == '3.14.*'", - "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", ] sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } wheels = [ @@ -2714,27 +2279,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, ] -[[package]] -name = "tomli" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, -] - [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, @@ -2790,7 +2338,7 @@ name = "tqdm" version = "4.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } wheels = [ @@ -2943,28 +2491,11 @@ wheels = [ name = "urllib3" version = "1.26.20" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", -] sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, ] -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - [[package]] name = "uvicorn" version = "0.40.0" @@ -3061,15 +2592,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] -[[package]] -name = "zipp" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, -] - [[package]] name = "zizmor" version = "1.27.0" From 02389d6a64bcfffec4ec93fd4e174f674f8f7979 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 23 Jul 2026 22:25:50 +0300 Subject: [PATCH 53/55] feat(infra): clickhouse + jaeger collector/query --- deploy/observability/collector.yaml | 4 +- deploy/observability/jaeger/collector.yaml | 51 ++++++++++++++++++++ deploy/observability/jaeger/config-ui.json | 3 ++ deploy/observability/jaeger/query.yaml | 45 ++++++++++++++++++ deploy/persistence/clickhouse.env | 3 ++ docker-compose.base.yaml | 55 ++++++++++++++++++---- docker-compose.dev.yaml | 8 ++-- 7 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 deploy/observability/jaeger/collector.yaml create mode 100644 deploy/observability/jaeger/config-ui.json create mode 100644 deploy/observability/jaeger/query.yaml create mode 100644 deploy/persistence/clickhouse.env diff --git a/deploy/observability/collector.yaml b/deploy/observability/collector.yaml index e123cf9..d5c14a1 100644 --- a/deploy/observability/collector.yaml +++ b/deploy/observability/collector.yaml @@ -2,7 +2,7 @@ receivers: otlp: protocols: grpc: - endpoint: api:4317 + endpoint: 0.0.0.0:4317 processors: @@ -13,7 +13,7 @@ processors: exporters: otlp: - endpoint: jaeger:4317 + endpoint: jaeger-collector:4317 tls: insecure: true diff --git a/deploy/observability/jaeger/collector.yaml b/deploy/observability/jaeger/collector.yaml new file mode 100644 index 0000000..474e148 --- /dev/null +++ b/deploy/observability/jaeger/collector.yaml @@ -0,0 +1,51 @@ +service: + extensions: [jaeger_storage, jaeger_query, healthcheckv2] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [jaeger_storage_exporter] + telemetry: + resource: + service.name: jaeger + + logs: + level: debug + +extensions: + healthcheckv2: + use_v2: true + http: + + jaeger_query: + storage: + traces: some-storage + ui: + config_file: /jaeger/config-ui.json + jaeger_storage: + backends: + some-storage: + clickhouse: + addresses: + - clickhouse:9000 + database: jaeger + auth: + basic: + username: jaeger + password: jaegerSecretPassword + create_schema: true + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + jaeger_storage_exporter: + trace_storage: some-storage \ No newline at end of file diff --git a/deploy/observability/jaeger/config-ui.json b/deploy/observability/jaeger/config-ui.json new file mode 100644 index 0000000..39f2437 --- /dev/null +++ b/deploy/observability/jaeger/config-ui.json @@ -0,0 +1,3 @@ +{ + "archiveEnabled": true +} \ No newline at end of file diff --git a/deploy/observability/jaeger/query.yaml b/deploy/observability/jaeger/query.yaml new file mode 100644 index 0000000..558a2b5 --- /dev/null +++ b/deploy/observability/jaeger/query.yaml @@ -0,0 +1,45 @@ + +service: + extensions: [jaeger_storage, jaeger_query, healthcheckv2] + pipelines: + traces: + receivers: [nop] + processors: [batch] + exporters: [nop] + telemetry: + resource: + service.name: jaeger-query + logs: + level: info + +extensions: + healthcheckv2: + use_v2: true + http: + + jaeger_query: + storage: + traces: query_storage + ui: + config_file: /jaeger/config-ui.json + + jaeger_storage: + backends: + query_storage: + clickhouse: + addresses: + - clickhouse:9000 + database: jaeger + auth: + basic: + username: jaeger + password: jaegerSecretPassword + +receivers: + nop: + +processors: + batch: + +exporters: + nop: \ No newline at end of file diff --git a/deploy/persistence/clickhouse.env b/deploy/persistence/clickhouse.env new file mode 100644 index 0000000..4628be1 --- /dev/null +++ b/deploy/persistence/clickhouse.env @@ -0,0 +1,3 @@ +CLICKHOUSE_DB=jaeger +CLICKHOUSE_USER=jaeger +CLICKHOUSE_PASSWORD=jaegerSecretPassword \ No newline at end of file diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml index b192d3f..0373ce3 100644 --- a/docker-compose.base.yaml +++ b/docker-compose.base.yaml @@ -130,24 +130,62 @@ services: timeout: 15s retries: 5 - jaeger: - image: jaegertracing/all-in-one:latest - container_name: jaeger + jaeger-collector: + image: jaegertracing/jaeger:2.20.0 + command: --config /jaeger/config.yaml --feature-gates=storage.clickhouse - environment: - - COLLECTOR_OTLP_ENABLED=true + volumes: + - ./deploy/observability/jaeger/collector.yaml:/jaeger/config.yaml:ro + - ./deploy/observability/jaeger/config-ui.json:/jaeger/config-ui.json:ro + + restart: unless-stopped + networks: + - conode-network + + depends_on: + clickhouse: + condition: service_healthy + + jaeger-query: + image: jaegertracing/jaeger:2.20.0 + command: --config /jaeger/config.yaml --feature-gates=storage.clickhouse + + volumes: + - ./deploy/observability/jaeger/query.yaml:/jaeger/config.yaml:ro + - ./deploy/observability/jaeger/config-ui.json:/jaeger/config-ui.json:ro + + depends_on: + clickhouse: + condition: service_healthy restart: unless-stopped networks: - conode-network + clickhouse: + image: clickhouse/clickhouse-server:25.3 + container_name: clickhouse + restart: unless-stopped + + env_file: + - "./deploy/persistence/clickhouse.env" + + volumes: + - clickhouse-data:/var/lib/clickhouse + + networks: + - conode-network + + healthcheck: + test: ["CMD", "clickhouse-client", "--query", "SELECT 1"] + interval: 10s + timeout: 5s + retries: 5 + collector: image: otel/opentelemetry-collector:0.156.0 command: --config=/etc/otelcol/config.yaml - depends_on: - - jaeger - volumes: - ./deploy/observability/collector.yaml:/etc/otelcol/config.yaml:ro @@ -155,6 +193,7 @@ services: - conode-network volumes: + clickhouse-data: postgres-data: keycloak-data: diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 0d27053..a62dd99 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -9,12 +9,12 @@ services: ports: - 5432:5432 - jaeger: + jaeger-query: labels: - "traefik.enable=true" - - "traefik.http.routers.jaeger.rule=Host(`jaeger.conode.com`)" - - "traefik.http.routers.jaeger.entrypoints=web" - - "traefik.http.services.jaeger.loadbalancer.server.port=16686" + - "traefik.http.routers.observability.rule=Host(`observability.conode.com`)" + - "traefik.http.routers.observability.entrypoints=web" + - "traefik.http.services.observability.loadbalancer.server.port=16686" traefik: command: From 16bb94dc5e83675075b6f10e8f80814ed1089a95 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Sat, 25 Jul 2026 18:00:44 +0300 Subject: [PATCH 54/55] some changes --- deploy/observability/jaeger/query.yaml | 1 - docker-compose.base.yaml | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/deploy/observability/jaeger/query.yaml b/deploy/observability/jaeger/query.yaml index 558a2b5..44bc22d 100644 --- a/deploy/observability/jaeger/query.yaml +++ b/deploy/observability/jaeger/query.yaml @@ -1,4 +1,3 @@ - service: extensions: [jaeger_storage, jaeger_query, healthcheckv2] pipelines: diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml index 0373ce3..b21d743 100644 --- a/docker-compose.base.yaml +++ b/docker-compose.base.yaml @@ -29,13 +29,26 @@ services: networks: - conode-network + grafana: + image: grafana/grafana:12.1.1 + + labels: + - "traefik.enable=true" + + - "traefik.http.routers.grafana.rule=Host(`grafana.conode.com`)" + - "traefik.http.routers.grafana.entrypoints=web" + - "traefik.http.services.grafana.loadbalancer.server.port=3000" + + networks: + - conode-network + oauth: image: quay.io/oauth2-proxy/oauth2-proxy:latest command: ["--config=/etc/oauth2-proxy/oauth2-proxy.toml"] environment: OAUTH2_PROXY_CLIENT_ID: conode - OAUTH2_PROXY_CLIENT_SECRET: RELW6bj1AsUr3yQUmc4Nn3y5mGfS6qzjD7tCnkIuekGa9mMmU9B6vuCFVyvUL9mBDrRUJVVPSe42hTzbFN6Vqi + OAUTH2_PROXY_CLIENT_SECRET: y9QISRU4LktDcAOvrXmw5q8t5s44JshMrnc4PIqPfq1vIEGipEtYgmUxvgiG9nb0PsxVwvlVQQ4FPOlhl9tiZ3 OAUTH2_PROXY_COOKIE_SECRET: x3CpXUQxfDgXzDDxIdST3mTbTDosKOlx volumes: From 5d5e7ba3fac3fb3d201dceb095834bfca531a512 Mon Sep 17 00:00:00 2001 From: Davlat Davydov Date: Thu, 30 Jul 2026 20:58:29 +0300 Subject: [PATCH 55/55] feat(infra): replace jaeger with grafana stack --- deploy/observability/collector.yaml | 31 --------- deploy/observability/grafana/config.alloy | 63 ++++++++++++++++++ deploy/observability/grafana/prometheus.yml | 9 +++ deploy/observability/grafana/tempo.yaml | 24 +++++++ deploy/observability/prometheus.yml | 13 ---- docker-compose.base.yaml | 72 +++++++++------------ docker-compose.dev.yaml | 7 -- src/conode/bootstrap/telemetry/otel.py | 44 ++++++++++--- 8 files changed, 163 insertions(+), 100 deletions(-) delete mode 100644 deploy/observability/collector.yaml create mode 100644 deploy/observability/grafana/config.alloy create mode 100644 deploy/observability/grafana/prometheus.yml create mode 100644 deploy/observability/grafana/tempo.yaml delete mode 100644 deploy/observability/prometheus.yml diff --git a/deploy/observability/collector.yaml b/deploy/observability/collector.yaml deleted file mode 100644 index d5c14a1..0000000 --- a/deploy/observability/collector.yaml +++ /dev/null @@ -1,31 +0,0 @@ -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - - -processors: - batch: - timeout: 5s - send_batch_size: 1024 - send_batch_max_size: 2048 - -exporters: - otlp: - endpoint: jaeger-collector:4317 - tls: - insecure: true - - -service: - pipelines: - traces: - receivers: - - otlp - - processors: - - batch - - exporters: - - otlp \ No newline at end of file diff --git a/deploy/observability/grafana/config.alloy b/deploy/observability/grafana/config.alloy new file mode 100644 index 0000000..956565c --- /dev/null +++ b/deploy/observability/grafana/config.alloy @@ -0,0 +1,63 @@ +otelcol.receiver.otlp "otlp_receiver" { + grpc { + endpoint = "0.0.0.0:4317" + } + + http { + endpoint = "0.0.0.0:4318" + } + + output { + metrics = [otelcol.processor.batch.metrics.input] + traces = [otelcol.processor.batch.traces.input] + } +} + + +otelcol.processor.batch "metrics" { + timeout = "5s" + + send_batch_size = 1024 + send_batch_max_size = 2048 + + output { + metrics = [otelcol.exporter.prometheus.metrics.input] + } +} + + +otelcol.processor.batch "traces" { + timeout = "5s" + + send_batch_size = 1024 + send_batch_max_size = 2048 + + output { + traces = [otelcol.exporter.otlp.tempo.input] + } +} + + +otelcol.exporter.prometheus "metrics" { + forward_to = [ + prometheus.remote_write.default.receiver, + ] +} + + +prometheus.remote_write "default" { + endpoint { + url = "http://prometheus:9090/api/v1/write" + } +} + + +otelcol.exporter.otlp "tempo" { + client { + endpoint = "tempo:4317" + + tls { + insecure = true + } + } +} \ No newline at end of file diff --git a/deploy/observability/grafana/prometheus.yml b/deploy/observability/grafana/prometheus.yml new file mode 100644 index 0000000..184dc94 --- /dev/null +++ b/deploy/observability/grafana/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: "prometheus" + + static_configs: + - targets: + - "prometheus:9090" \ No newline at end of file diff --git a/deploy/observability/grafana/tempo.yaml b/deploy/observability/grafana/tempo.yaml new file mode 100644 index 0000000..87f548d --- /dev/null +++ b/deploy/observability/grafana/tempo.yaml @@ -0,0 +1,24 @@ +stream_over_http_enabled: true + +server: + http_listen_port: 3200 + log_level: info + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks + + +usage_report: + reporting_enabled: false \ No newline at end of file diff --git a/deploy/observability/prometheus.yml b/deploy/observability/prometheus.yml deleted file mode 100644 index 3d702a5..0000000 --- a/deploy/observability/prometheus.yml +++ /dev/null @@ -1,13 +0,0 @@ -global: - scrape_interval: 15s - -scrape_configs: - - job_name: "prometheus" - - static_configs: - - targets: ["prometheus:9090"] - - - job_name: "managment" - - static_configs: - - targets: ["node-exporter:9100"] \ No newline at end of file diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml index b21d743..24872f0 100644 --- a/docker-compose.base.yaml +++ b/docker-compose.base.yaml @@ -143,72 +143,62 @@ services: timeout: 15s retries: 5 - jaeger-collector: - image: jaegertracing/jaeger:2.20.0 - command: --config /jaeger/config.yaml --feature-gates=storage.clickhouse + alloy: + image: grafana/alloy:latest volumes: - - ./deploy/observability/jaeger/collector.yaml:/jaeger/config.yaml:ro - - ./deploy/observability/jaeger/config-ui.json:/jaeger/config-ui.json:ro + - ./deploy/observability/grafana/config.alloy:/etc/alloy/config.alloy + - alloy-data:/var/lib/alloy/data + + command: + - run + - /etc/alloy/config.alloy + - --storage.path=/var/lib/alloy/data + - --server.http.listen-addr=0.0.0.0:12345 - restart: unless-stopped networks: - conode-network - depends_on: - clickhouse: - condition: service_healthy - jaeger-query: - image: jaegertracing/jaeger:2.20.0 - command: --config /jaeger/config.yaml --feature-gates=storage.clickhouse + prometheus: + image: prom/prometheus - volumes: - - ./deploy/observability/jaeger/query.yaml:/jaeger/config.yaml:ro - - ./deploy/observability/jaeger/config-ui.json:/jaeger/config-ui.json:ro + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.enable-remote-write-receiver" - depends_on: - clickhouse: - condition: service_healthy + volumes: + - prometheus-data:/prometheus + - ./deploy/observability/grafana/prometheus.yml:/etc/prometheus/prometheus.yml - restart: unless-stopped networks: - conode-network - clickhouse: - image: clickhouse/clickhouse-server:25.3 - container_name: clickhouse - restart: unless-stopped - env_file: - - "./deploy/persistence/clickhouse.env" + tempo: + image: grafana/tempo:latest - volumes: - - clickhouse-data:/var/lib/clickhouse - - networks: - - conode-network - - healthcheck: - test: ["CMD", "clickhouse-client", "--query", "SELECT 1"] - interval: 10s - timeout: 5s - retries: 5 + command: + - "-target=all" + - "-config.file=/etc/tempo.yaml" - collector: - image: otel/opentelemetry-collector:0.156.0 - command: --config=/etc/otelcol/config.yaml + restart: always volumes: - - ./deploy/observability/collector.yaml:/etc/otelcol/config.yaml:ro + - ./deploy/observability/grafana/tempo.yaml:/etc/tempo.yaml + - tempo-data:/var/tempo networks: - conode-network + volumes: - clickhouse-data: + prometheus-data: postgres-data: keycloak-data: + tempo-data: + alloy-data: networks: conode-network: diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index a62dd99..8383f94 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -9,13 +9,6 @@ services: ports: - 5432:5432 - jaeger-query: - labels: - - "traefik.enable=true" - - "traefik.http.routers.observability.rule=Host(`observability.conode.com`)" - - "traefik.http.routers.observability.entrypoints=web" - - "traefik.http.services.observability.loadbalancer.server.port=16686" - traefik: command: - "--log.level=DEBUG" diff --git a/src/conode/bootstrap/telemetry/otel.py b/src/conode/bootstrap/telemetry/otel.py index 81077a7..a2249a3 100644 --- a/src/conode/bootstrap/telemetry/otel.py +++ b/src/conode/bootstrap/telemetry/otel.py @@ -1,11 +1,22 @@ import os from uuid import uuid4 -from opentelemetry import trace +from opentelemetry import metrics, trace +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) -from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, SERVICE_NAME, Resource +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import ( + SERVICE_INSTANCE_ID, + SERVICE_NAME, + Resource, +) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -23,19 +34,36 @@ def configure_telemetry(config: OTELConfig) -> None: }, ) - provider = TracerProvider( + tracer_provider = TracerProvider( resource=resource, ) - exporter = OTLPSpanExporter( + trace_exporter = OTLPSpanExporter( endpoint=config.endpoint, insecure=True, ) - processor = BatchSpanProcessor( - exporter, + tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + + trace.set_tracer_provider( + tracer_provider, ) - provider.add_span_processor(processor) + metric_exporter = OTLPMetricExporter( + endpoint=config.endpoint, + insecure=True, + ) - trace.set_tracer_provider(provider) + meter_provider = MeterProvider( + resource=resource, + metric_readers=[ + PeriodicExportingMetricReader( + metric_exporter, + export_interval_millis=5000, + ), + ], + ) + + metrics.set_meter_provider( + meter_provider, + )