From a6efc63a5fc5615759cd3a058671b471e076590b Mon Sep 17 00:00:00 2001 From: artem-chupryna Date: Wed, 25 Sep 2024 13:39:11 +0200 Subject: [PATCH] feat(python-sdk): nested collections and documents (#32) Documents are indexed by a key in a collection, and contain JSON data: * Create documents in a collection * Manage tags for a document * Iterate over documents in a collection (with filtering by tags possible) * Read document data Additionally nested collections are introduced. --- python/queries.gql | 122 +++++ python/src/numerous/_client.py | 258 +++++++++- python/src/numerous/collection/__init__.py | 4 +- .../collection/numerous_collection.py | 93 +++- .../numerous/collection/numerous_document.py | 189 ++++++++ .../numerous/generated/graphql/__init__.py | 84 +++- .../generated/graphql/all_elements.py | 10 + .../src/numerous/generated/graphql/client.py | 319 ++++++++++++- .../graphql/collection_collections.py | 55 +++ .../generated/graphql/collection_create.py | 5 +- .../generated/graphql/collection_document.py | 33 ++ .../graphql/collection_document_delete.py | 31 ++ .../graphql/collection_document_set.py | 29 ++ .../graphql/collection_document_tag_add.py | 31 ++ .../graphql/collection_document_tag_delete.py | 31 ++ .../generated/graphql/collection_documents.py | 55 +++ .../src/numerous/generated/graphql/enums.py | 3 + .../numerous/generated/graphql/fragments.py | 19 +- .../numerous/generated/graphql/input_types.py | 10 + .../generated/graphql/update_element.py | 3 + .../src/numerous/generated/graphql/updates.py | 11 + python/src/numerous/jsonbase64.py | 23 + python/src/numerous/session.py | 34 +- python/src/numerous/threaded_event_loop.py | 44 ++ python/tests/test_collections.py | 449 +++++++++++++++++- shared/schema.gql | 43 +- 26 files changed, 1927 insertions(+), 61 deletions(-) create mode 100644 python/src/numerous/collection/numerous_document.py create mode 100644 python/src/numerous/generated/graphql/collection_collections.py create mode 100644 python/src/numerous/generated/graphql/collection_document.py create mode 100644 python/src/numerous/generated/graphql/collection_document_delete.py create mode 100644 python/src/numerous/generated/graphql/collection_document_set.py create mode 100644 python/src/numerous/generated/graphql/collection_document_tag_add.py create mode 100644 python/src/numerous/generated/graphql/collection_document_tag_delete.py create mode 100644 python/src/numerous/generated/graphql/collection_documents.py create mode 100644 python/src/numerous/jsonbase64.py create mode 100644 python/src/numerous/threaded_event_loop.py diff --git a/python/queries.gql b/python/queries.gql index bc708a40..9e01d8ea 100644 --- a/python/queries.gql +++ b/python/queries.gql @@ -97,6 +97,16 @@ fragment CollectionReference on Collection { key } +fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } +} + fragment CollectionNotFound on CollectionNotFound { id } @@ -116,3 +126,115 @@ mutation CollectionCreate($organizationID: ID!, $key: ID!, $parentID: ID) { } } } + +mutation CollectionCollections( + $organizationID: ID! + $key: ID! + $after: ID + $first: Int +) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + id + key + collections(after: $after, first: $first) { + edges { + node { + ... on Collection { + ...CollectionReference + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} + +mutation CollectionDocument($organizationID: ID!, $key: ID!, $docKey: ID!) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + document(key: $docKey) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + } +} + +mutation collectionDocumentSet( + $collectionID: ID! + $key: ID! + $data: Base64JSON! +) { + collectionDocumentSet(collectionID: $collectionID, key: $key, data: $data) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } +} + +mutation collectionDocumentDelete($id: ID!) { + collectionDocumentDelete(id: $id) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } +} + +mutation collectionDocumentTagAdd($id: ID!, $tag: TagInput!) { + collectionDocumentTagAdd(id: $id, tag: $tag) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } +} + +mutation collectionDocumentTagDelete($id: ID!, $tag_key: String!) { + collectionDocumentTagDelete(id: $id, key: $tag_key) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } +} + +mutation collectionDocuments( + $organizationID: ID! + $key: ID! + $tag: TagInput + $after: ID + $first: Int +) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + id + key + documents(after: $after, first: $first, tag: $tag) { + edges { + node { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} diff --git a/python/src/numerous/_client.py b/python/src/numerous/_client.py index 4acd80b9..76f066de 100644 --- a/python/src/numerous/_client.py +++ b/python/src/numerous/_client.py @@ -1,21 +1,58 @@ """GraphQL client wrapper for numerous.""" -import asyncio import os from typing import Optional, Union from numerous.generated.graphql.client import Client as GQLClient -from numerous.generated.graphql.fragments import CollectionNotFound, CollectionReference +from numerous.generated.graphql.collection_collections import ( + CollectionCollectionsCollectionCreateCollection, + CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode, +) +from numerous.generated.graphql.collection_document import ( + CollectionDocumentCollectionCreateCollectionDocument, + CollectionDocumentCollectionCreateCollectionNotFound, +) +from numerous.generated.graphql.collection_document_delete import ( + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound, +) +from numerous.generated.graphql.collection_document_set import ( + CollectionDocumentSetCollectionDocumentSetCollectionDocument, + CollectionDocumentSetCollectionDocumentSetCollectionNotFound, +) +from numerous.generated.graphql.collection_document_tag_add import ( + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound, +) +from numerous.generated.graphql.collection_document_tag_delete import ( + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument, + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound, +) +from numerous.generated.graphql.collection_documents import ( + CollectionDocumentsCollectionCreateCollection, + CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode, +) +from numerous.generated.graphql.fragments import ( + CollectionDocumentReference, + CollectionNotFound, + CollectionReference, +) +from numerous.generated.graphql.input_types import TagInput +from numerous.threaded_event_loop import ThreadedEventLoop API_URL_NOT_SET = "NUMEROUS_API_URL environment variable is not set" MESSAGE_NOT_SET = "NUMEROUS_API_ACCESS_TOKEN environment variable is not set" +COLLECTED_OBJECTS_NUMBER = 100 class Client: def __init__(self, client: GQLClient) -> None: self.client = client - self.organization_id = "" + self._threaded_event_loop = ThreadedEventLoop() + self._threaded_event_loop.start() + self.organization_id = os.getenv("ORGANIZATION_ID", "default_organization") + auth_token = os.getenv("NUMEROUS_API_ACCESS_TOKEN") if not auth_token: raise ValueError(MESSAGE_NOT_SET) @@ -23,9 +60,20 @@ def __init__(self, client: GQLClient) -> None: self.kwargs = {"headers": {"Authorization": f"Bearer {auth_token}"}} def _create_collection_ref( - self, collection_response: Union[CollectionReference, CollectionNotFound] + self, + collection_response: Union[ + CollectionReference, + CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode, + CollectionNotFound, + ], ) -> Optional[CollectionReference]: - if isinstance(collection_response, CollectionReference): + if isinstance( + collection_response, + ( + CollectionReference, + CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode, + ), + ): return CollectionReference( id=collection_response.id, key=collection_response.key ) @@ -38,7 +86,7 @@ async def _create_collection( self.organization_id, collection_key, parent_collection_key, - kwargs=self.kwargs, + **self.kwargs, ) return self._create_collection_ref(response.collection_create) @@ -51,10 +99,206 @@ def get_collection_reference( This method retrieves a collection based on its key and parent key, or creates it if it doesn't exist. """ - return asyncio.run( + return self._threaded_event_loop.await_coro( self._create_collection(collection_key, parent_collection_id) ) + def _create_collection_document_ref( + self, + collection_response: Optional[ + Union[ + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument, + CollectionDocumentSetCollectionDocumentSetCollectionDocument, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound, + CollectionDocumentSetCollectionDocumentSetCollectionNotFound, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound, + CollectionDocumentCollectionCreateCollectionDocument, + CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode, + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound, + ] + ], + ) -> Optional[CollectionDocumentReference]: + if isinstance( + collection_response, + ( + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument, + CollectionDocumentSetCollectionDocumentSetCollectionDocument, + CollectionDocumentCollectionCreateCollectionDocument, + CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument, + ), + ): + return CollectionDocumentReference( + id=collection_response.id, + key=collection_response.key, + data=collection_response.data, + tags=collection_response.tags, + ) + return None + + async def _get_collection_document( + self, collection_key: str, document_key: str + ) -> Optional[CollectionDocumentReference]: + response = await self.client.collection_document( + self.organization_id, + collection_key, + document_key, + **self.kwargs, + ) + if isinstance( + response.collection_create, + CollectionDocumentCollectionCreateCollectionNotFound, + ): + return None + return self._create_collection_document_ref(response.collection_create.document) + + def get_collection_document( + self, collection_key: str, document_key: str + ) -> Optional[CollectionDocumentReference]: + return self._threaded_event_loop.await_coro( + self._get_collection_document(collection_key, document_key) + ) + + async def _set_collection_document( + self, collection_id: str, document_key: str, document_data: str + ) -> Optional[CollectionDocumentReference]: + response = await self.client.collection_document_set( + collection_id, + document_key, + document_data, + **self.kwargs, + ) + return self._create_collection_document_ref(response.collection_document_set) + + def set_collection_document( + self, collection_id: str, document_key: str, document_data: str + ) -> Optional[CollectionDocumentReference]: + return self._threaded_event_loop.await_coro( + self._set_collection_document(collection_id, document_key, document_data) + ) + + async def _delete_collection_document( + self, document_id: str + ) -> Optional[CollectionDocumentReference]: + response = await self.client.collection_document_delete( + document_id, **self.kwargs + ) + return self._create_collection_document_ref(response.collection_document_delete) + + def delete_collection_document( + self, document_id: str + ) -> Optional[CollectionDocumentReference]: + return self._threaded_event_loop.await_coro( + self._delete_collection_document(document_id) + ) + + async def _add_collection_document_tag( + self, document_id: str, tag: TagInput + ) -> Optional[CollectionDocumentReference]: + response = await self.client.collection_document_tag_add( + document_id, tag, **self.kwargs + ) + return self._create_collection_document_ref( + response.collection_document_tag_add + ) + + def add_collection_document_tag( + self, document_id: str, tag: TagInput + ) -> Optional[CollectionDocumentReference]: + return self._threaded_event_loop.await_coro( + self._add_collection_document_tag(document_id, tag) + ) + + async def _delete_collection_document_tag( + self, document_id: str, tag_key: str + ) -> Optional[CollectionDocumentReference]: + response = await self.client.collection_document_tag_delete( + document_id, tag_key, **self.kwargs + ) + return self._create_collection_document_ref( + response.collection_document_tag_delete + ) + + def delete_collection_document_tag( + self, document_id: str, tag_key: str + ) -> Optional[CollectionDocumentReference]: + return self._threaded_event_loop.await_coro( + self._delete_collection_document_tag(document_id, tag_key) + ) + + async def _get_collection_documents( + self, + collection_key: str, + end_cursor: str, + tag_input: Optional[TagInput], + ) -> tuple[Optional[list[Optional[CollectionDocumentReference]]], bool, str]: + response = await self.client.collection_documents( + self.organization_id, + collection_key, + tag_input, + after=end_cursor, + first=COLLECTED_OBJECTS_NUMBER, + **self.kwargs, + ) + + collection = response.collection_create + if not isinstance(collection, CollectionDocumentsCollectionCreateCollection): + return [], False, "" + + documents = collection.documents + edges = documents.edges + page_info = documents.page_info + + result = [self._create_collection_document_ref(edge.node) for edge in edges] + + end_cursor = page_info.end_cursor or "" + has_next_page = page_info.has_next_page + + return result, has_next_page, end_cursor + + def get_collection_documents( + self, collection_key: str, end_cursor: str, tag_input: Optional[TagInput] + ) -> tuple[Optional[list[Optional[CollectionDocumentReference]]], bool, str]: + return self._threaded_event_loop.await_coro( + self._get_collection_documents(collection_key, end_cursor, tag_input) + ) + + async def _get_collection_collections( + self, collection_key: str, end_cursor: str + ) -> tuple[Optional[list[Optional[CollectionReference]]], bool, str]: + response = await self.client.collection_collections( + self.organization_id, + collection_key, + after=end_cursor, + first=COLLECTED_OBJECTS_NUMBER, + **self.kwargs, + ) + + collection = response.collection_create + if not isinstance(collection, CollectionCollectionsCollectionCreateCollection): + return [], False, "" + + collections = collection.collections + edges = collections.edges + page_info = collections.page_info + + result = [self._create_collection_ref(edge.node) for edge in edges] + + end_cursor = page_info.end_cursor or "" + has_next_page = page_info.has_next_page + + return result, has_next_page, end_cursor + + def get_collection_collections( + self, collection_key: str, end_cursor: str + ) -> tuple[Optional[list[Optional[CollectionReference]]], bool, str]: + return self._threaded_event_loop.await_coro( + self._get_collection_collections(collection_key, end_cursor) + ) + def _open_client() -> Client: url = os.getenv("NUMEROUS_API_URL") diff --git a/python/src/numerous/collection/__init__.py b/python/src/numerous/collection/__init__.py index dca0e13e..5fe5445b 100644 --- a/python/src/numerous/collection/__init__.py +++ b/python/src/numerous/collection/__init__.py @@ -1,5 +1,7 @@ """The Python SDK for numerous collections.""" -__all__ = ["collection"] +__all__ = ["collection", "NumerousCollection", "NumerousDocument"] from .collection import collection +from .numerous_collection import NumerousCollection +from .numerous_document import NumerousDocument diff --git a/python/src/numerous/collection/numerous_collection.py b/python/src/numerous/collection/numerous_collection.py index f5fc12ca..0e6cc20a 100644 --- a/python/src/numerous/collection/numerous_collection.py +++ b/python/src/numerous/collection/numerous_collection.py @@ -1,9 +1,11 @@ """Class for working with numerous collections.""" -from typing import Optional +from typing import Iterator, Optional from numerous._client import Client +from numerous.collection.numerous_document import NumerousDocument from numerous.generated.graphql.fragments import CollectionReference +from numerous.generated.graphql.input_types import TagInput class NumerousCollection: @@ -23,3 +25,92 @@ def collection(self, collection_name: str) -> Optional["NumerousCollection"]: if collection_ref is not None: return NumerousCollection(collection_ref, self._client) return None + + def document(self, key: str) -> NumerousDocument: + """ + Get or create a document by key. + + Attributes + ---------- + key (str): The key of the document. + + """ + numerous_doc_ref = self._client.get_collection_document(self.key, key) + if numerous_doc_ref is not None: + numerous_document = NumerousDocument( + self._client, + numerous_doc_ref.key, + (self.id, self.key), + numerous_doc_ref, + ) + else: + numerous_document = NumerousDocument(self._client, key, (self.id, self.key)) + + return numerous_document + + def documents( + self, tag_key: Optional[str] = None, tag_value: Optional[str] = None + ) -> Iterator[NumerousDocument]: + """ + Retrieve documents from the collection, filtered by a tag key and value. + + Parameters + ---------- + tag_key : Optional[str] + The key of the tag used to filter documents (optional). + tag_value : Optional[str] + The value of the tag used to filter documents (optional). + + Yields + ------ + NumerousDocument + Yields NumerousDocument objects from the collection. + + """ + end_cursor = "" + tag_input = None + if tag_key is not None and tag_value is not None: + tag_input = TagInput(key=tag_key, value=tag_value) + has_next_page = True + while has_next_page: + result = self._client.get_collection_documents( + self.key, end_cursor, tag_input + ) + if result is None: + break + numerous_doc_refs, has_next_page, end_cursor = result + if numerous_doc_refs is None: + break + for numerous_doc_ref in numerous_doc_refs: + if numerous_doc_ref is None: + continue + yield NumerousDocument( + self._client, + numerous_doc_ref.key, + (self.id, self.key), + numerous_doc_ref, + ) + + def collections(self) -> Iterator["NumerousCollection"]: + """ + Retrieve nested collections from the collection. + + Yields + ------ + NumerousCollection + Yields NumerousCollection objects. + + """ + end_cursor = "" + has_next_page = True + while has_next_page: + result = self._client.get_collection_collections(self.key, end_cursor) + if result is None: + break + collection_ref_keys, has_next_page, end_cursor = result + if collection_ref_keys is None: + break + for collection_ref_key in collection_ref_keys: + if collection_ref_key is None: + return + yield NumerousCollection(collection_ref_key, self._client) diff --git a/python/src/numerous/collection/numerous_document.py b/python/src/numerous/collection/numerous_document.py new file mode 100644 index 00000000..edf68e92 --- /dev/null +++ b/python/src/numerous/collection/numerous_document.py @@ -0,0 +1,189 @@ +"""Class for working with numerous documents.""" + +from typing import Any, Optional + +from numerous._client import Client +from numerous.generated.graphql.fragments import CollectionDocumentReference +from numerous.generated.graphql.input_types import TagInput +from numerous.jsonbase64 import base64_to_dict, dict_to_base64 + + +class NumerousDocument: + """ + Represents a document in a Numerous collection. + + Attributes + ---------- + key (str): The key of the document. + collection_info tuple[str, str]: The id + and key of collection document belongs to. + data (Optional[dict[str, Any]]): The data of the document. + id (Optional[str]): The unique identifier of the document. + client (Client): The client to connect. + tags (dict[str, str]): The tags associated with the document. + + """ + + def __init__( + self, + client: Client, + key: str, + collection_info: tuple[str, str], + numerous_doc_ref: Optional[CollectionDocumentReference] = None, + ) -> None: + self.key: str = key + self.collection_id: str = collection_info[0] + self.collection_key: str = collection_info[1] + self._client: Client = client + self.document_id: Optional[str] = None + self.data: Optional[dict[str, Any]] = None + + if numerous_doc_ref is not None: + dict_of_tags = {tag.key: tag.value for tag in numerous_doc_ref.tags} + self.data = base64_to_dict(numerous_doc_ref.data) + self.document_id = numerous_doc_ref.id + self._tags: dict[str, str] = ( + dict_of_tags if dict_of_tags is not None else {} + ) + + @property + def exists(self) -> bool: + """Check if the document exists.""" + return self.document_id is not None + + @property + def tags(self) -> dict[str, str]: + """Get the tags for the document.""" + if self.document_id is not None: + return self._tags + + msg = "Cannot get tags from a non-existent document." + raise ValueError(msg) + + def set(self, data: dict[str, Any]) -> None: + """ + Set the data for the document. + + Args: + ---- + data (dict[str, Any]): The data to set for the document. + + Raises: + ------ + ValueError: If the document data setting fails. + + """ + base64_data = dict_to_base64(data) + document = self._client.set_collection_document( + self.collection_id, self.key, base64_data + ) + if document is not None: + self.document_id = document.id + else: + msg = "Failed to delete the document." + raise ValueError(msg) + self.data = data + + def get(self) -> Optional[dict[str, Any]]: + """ + Get the data of the document. + + Returns + ------- + dict[str, Any]: The data of the document. + + Raises + ------ + ValueError: If the document does not exist. + + """ + if not self.exists: + msg = "Document does not exist." + raise ValueError(msg) + self._fetch_data(self.key) + return self.data + + def delete(self) -> None: + """ + Delete the document. + + Raises + ------ + ValueError: If the document does not exist or deletion failed. + + """ + if self.document_id is not None: + deleted_document = self._client.delete_collection_document(self.document_id) + + if deleted_document is not None and deleted_document.id == self.document_id: + self.document_id = None + self.data = None + self._tags = {} + else: + msg = "Failed to delete the document." + raise ValueError(msg) + else: + msg = "Cannot delete a non-existent document." + raise ValueError(msg) + + def tag(self, key: str, value: str) -> None: + """ + Add a tag to the document. + + Args: + ---- + key (str): The tag key. + value (str): The tag value. + + Raises: + ------ + ValueError: If the document does not exist. + + """ + if self.document_id is not None: + tagged_document = self._client.add_collection_document_tag( + self.document_id, TagInput(key=key, value=value) + ) + else: + msg = "Cannot tag a non-existent document." + raise ValueError(msg) + + if tagged_document is not None: + self.tags.update({tag.key: tag.value for tag in tagged_document.tags}) + + def tag_delete(self, tag_key: str) -> None: + """ + Delete a tag from the document. + + Args: + ---- + tag_key (str): The key of the tag to delete. + + Raises: + ------ + ValueError: If the document does not exist. + + """ + if self.document_id is not None: + tagged_document = self._client.delete_collection_document_tag( + self.document_id, tag_key + ) + else: + msg = "Cannot delete tag from a non-existent document." + raise ValueError(msg) + + if tagged_document is not None: + self._tags = {tag.key: tag.value for tag in tagged_document.tags} + + def _fetch_data(self, document_key: str) -> None: + """Fetch the data from the server.""" + if self.document_id is not None: + document = self._client.get_collection_document( + self.collection_key, document_key + ) + else: + msg = "Cannot fetch data from a non-existent document." + raise ValueError(msg) + + if document is not None: + self.data = base64_to_dict(document.data) diff --git a/python/src/numerous/generated/graphql/__init__.py b/python/src/numerous/generated/graphql/__init__.py index b787433f..1299366e 100644 --- a/python/src/numerous/generated/graphql/__init__.py +++ b/python/src/numerous/generated/graphql/__init__.py @@ -19,11 +19,55 @@ from .async_base_client import AsyncBaseClient from .base_model import BaseModel, Upload from .client import Client +from .collection_collections import ( + CollectionCollections, + CollectionCollectionsCollectionCreateCollection, + CollectionCollectionsCollectionCreateCollectionCollections, + CollectionCollectionsCollectionCreateCollectionCollectionsEdges, + CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode, + CollectionCollectionsCollectionCreateCollectionCollectionsPageInfo, + CollectionCollectionsCollectionCreateCollectionNotFound, +) from .collection_create import ( CollectionCreate, CollectionCreateCollectionCreateCollection, CollectionCreateCollectionCreateCollectionNotFound, ) +from .collection_document import ( + CollectionDocument, + CollectionDocumentCollectionCreateCollection, + CollectionDocumentCollectionCreateCollectionDocument, + CollectionDocumentCollectionCreateCollectionNotFound, +) +from .collection_document_delete import ( + CollectionDocumentDelete, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument, + CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound, +) +from .collection_document_set import ( + CollectionDocumentSet, + CollectionDocumentSetCollectionDocumentSetCollectionDocument, + CollectionDocumentSetCollectionDocumentSetCollectionNotFound, +) +from .collection_document_tag_add import ( + CollectionDocumentTagAdd, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument, + CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound, +) +from .collection_document_tag_delete import ( + CollectionDocumentTagDelete, + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument, + CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound, +) +from .collection_documents import ( + CollectionDocuments, + CollectionDocumentsCollectionCreateCollection, + CollectionDocumentsCollectionCreateCollectionDocuments, + CollectionDocumentsCollectionCreateCollectionDocumentsEdges, + CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode, + CollectionDocumentsCollectionCreateCollectionDocumentsPageInfo, + CollectionDocumentsCollectionCreateCollectionNotFound, +) from .enums import ( AppDeploymentStatus, AppSubscriptionStatus, @@ -42,8 +86,10 @@ ) from .fragments import ( ButtonValue, - CollectionReference, + CollectionDocumentReference, + CollectionDocumentReferenceTags, CollectionNotFound, + CollectionReference, GraphContext, GraphContextAffectedBy, GraphContextAffects, @@ -59,6 +105,7 @@ AppDeployInput, AppDeployLogsInput, AppSecret, + AppVersionCreateGitHubInput, AppVersionInput, Auth0WhiteLabelInvitationInput, BuildPushInput, @@ -118,6 +165,7 @@ "AppDeploymentStatus", "AppSecret", "AppSubscriptionStatus", + "AppVersionCreateGitHubInput", "AppVersionInput", "AsyncBaseClient", "Auth0WhiteLabelInvitationInput", @@ -126,11 +174,43 @@ "BuildPushInput", "ButtonValue", "Client", + "CollectionCollections", + "CollectionCollectionsCollectionCreateCollection", + "CollectionCollectionsCollectionCreateCollectionCollections", + "CollectionCollectionsCollectionCreateCollectionCollectionsEdges", + "CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode", + "CollectionCollectionsCollectionCreateCollectionCollectionsPageInfo", + "CollectionCollectionsCollectionCreateCollectionNotFound", "CollectionCreate", "CollectionCreateCollectionCreateCollection", "CollectionCreateCollectionCreateCollectionNotFound", - "CollectionReference", + "CollectionDocument", + "CollectionDocumentCollectionCreateCollection", + "CollectionDocumentCollectionCreateCollectionDocument", + "CollectionDocumentCollectionCreateCollectionNotFound", + "CollectionDocumentDelete", + "CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument", + "CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound", + "CollectionDocumentReference", + "CollectionDocumentReferenceTags", + "CollectionDocumentSet", + "CollectionDocumentSetCollectionDocumentSetCollectionDocument", + "CollectionDocumentSetCollectionDocumentSetCollectionNotFound", + "CollectionDocumentTagAdd", + "CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument", + "CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound", + "CollectionDocumentTagDelete", + "CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument", + "CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound", + "CollectionDocuments", + "CollectionDocumentsCollectionCreateCollection", + "CollectionDocumentsCollectionCreateCollectionDocuments", + "CollectionDocumentsCollectionCreateCollectionDocumentsEdges", + "CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode", + "CollectionDocumentsCollectionCreateCollectionDocumentsPageInfo", + "CollectionDocumentsCollectionCreateCollectionNotFound", "CollectionNotFound", + "CollectionReference", "ElementInput", "ElementSelectInput", "GraphContext", diff --git a/python/src/numerous/generated/graphql/all_elements.py b/python/src/numerous/generated/graphql/all_elements.py index 46de227e..6b7bbc00 100644 --- a/python/src/numerous/generated/graphql/all_elements.py +++ b/python/src/numerous/generated/graphql/all_elements.py @@ -116,3 +116,13 @@ class AllElementsSessionAllTextField(TextFieldValue): class AllElementsSessionAllTextFieldGraphContext(GraphContext): pass + + +AllElements.model_rebuild() +AllElementsSession.model_rebuild() +AllElementsSessionAllElement.model_rebuild() +AllElementsSessionAllButton.model_rebuild() +AllElementsSessionAllHTMLElement.model_rebuild() +AllElementsSessionAllNumberField.model_rebuild() +AllElementsSessionAllSliderElement.model_rebuild() +AllElementsSessionAllTextField.model_rebuild() diff --git a/python/src/numerous/generated/graphql/client.py b/python/src/numerous/generated/graphql/client.py index ad62f034..494a88bc 100644 --- a/python/src/numerous/generated/graphql/client.py +++ b/python/src/numerous/generated/graphql/client.py @@ -6,8 +6,15 @@ from .all_elements import AllElements from .async_base_client import AsyncBaseClient from .base_model import UNSET, UnsetType +from .collection_collections import CollectionCollections from .collection_create import CollectionCreate -from .input_types import ElementInput +from .collection_document import CollectionDocument +from .collection_document_delete import CollectionDocumentDelete +from .collection_document_set import CollectionDocumentSet +from .collection_document_tag_add import CollectionDocumentTagAdd +from .collection_document_tag_delete import CollectionDocumentTagDelete +from .collection_documents import CollectionDocuments +from .input_types import ElementInput, TagInput from .update_element import UpdateElement from .updates import Updates @@ -205,13 +212,13 @@ async def collection_create( } } - fragment CollectionReference on Collection { + fragment CollectionNotFound on CollectionNotFound { id - key } - fragment CollectionNotFound on CollectionNotFound { + fragment CollectionReference on Collection { id + key } """ ) @@ -228,3 +235,307 @@ async def collection_create( ) data = self.get_data(response) return CollectionCreate.model_validate(data) + + async def collection_collections( + self, + organization_id: str, + key: str, + after: Union[Optional[str], UnsetType] = UNSET, + first: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any + ) -> CollectionCollections: + query = gql( + """ + mutation CollectionCollections($organizationID: ID!, $key: ID!, $after: ID, $first: Int) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + id + key + collections(after: $after, first: $first) { + edges { + node { + ... on Collection { + ...CollectionReference + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + + fragment CollectionReference on Collection { + id + key + } + """ + ) + variables: Dict[str, object] = { + "organizationID": organization_id, + "key": key, + "after": after, + "first": first, + } + response = await self.execute( + query=query, + operation_name="CollectionCollections", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionCollections.model_validate(data) + + async def collection_document( + self, organization_id: str, key: str, doc_key: str, **kwargs: Any + ) -> CollectionDocument: + query = gql( + """ + mutation CollectionDocument($organizationID: ID!, $key: ID!, $docKey: ID!) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + document(key: $docKey) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = { + "organizationID": organization_id, + "key": key, + "docKey": doc_key, + } + response = await self.execute( + query=query, + operation_name="CollectionDocument", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionDocument.model_validate(data) + + async def collection_document_set( + self, collection_id: str, key: str, data: Any, **kwargs: Any + ) -> CollectionDocumentSet: + query = gql( + """ + mutation collectionDocumentSet($collectionID: ID!, $key: ID!, $data: Base64JSON!) { + collectionDocumentSet(collectionID: $collectionID, key: $key, data: $data) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = { + "collectionID": collection_id, + "key": key, + "data": data, + } + response = await self.execute( + query=query, + operation_name="collectionDocumentSet", + variables=variables, + **kwargs + ) + _data = self.get_data(response) + return CollectionDocumentSet.model_validate(_data) + + async def collection_document_delete( + self, id: str, **kwargs: Any + ) -> CollectionDocumentDelete: + query = gql( + """ + mutation collectionDocumentDelete($id: ID!) { + collectionDocumentDelete(id: $id) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, + operation_name="collectionDocumentDelete", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionDocumentDelete.model_validate(data) + + async def collection_document_tag_add( + self, id: str, tag: TagInput, **kwargs: Any + ) -> CollectionDocumentTagAdd: + query = gql( + """ + mutation collectionDocumentTagAdd($id: ID!, $tag: TagInput!) { + collectionDocumentTagAdd(id: $id, tag: $tag) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = {"id": id, "tag": tag} + response = await self.execute( + query=query, + operation_name="collectionDocumentTagAdd", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionDocumentTagAdd.model_validate(data) + + async def collection_document_tag_delete( + self, id: str, tag_key: str, **kwargs: Any + ) -> CollectionDocumentTagDelete: + query = gql( + """ + mutation collectionDocumentTagDelete($id: ID!, $tag_key: String!) { + collectionDocumentTagDelete(id: $id, key: $tag_key) { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = {"id": id, "tag_key": tag_key} + response = await self.execute( + query=query, + operation_name="collectionDocumentTagDelete", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionDocumentTagDelete.model_validate(data) + + async def collection_documents( + self, + organization_id: str, + key: str, + tag: Union[Optional[TagInput], UnsetType] = UNSET, + after: Union[Optional[str], UnsetType] = UNSET, + first: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any + ) -> CollectionDocuments: + query = gql( + """ + mutation collectionDocuments($organizationID: ID!, $key: ID!, $tag: TagInput, $after: ID, $first: Int) { + collectionCreate(organizationID: $organizationID, key: $key) { + __typename + ... on Collection { + id + key + documents(after: $after, first: $first, tag: $tag) { + edges { + node { + __typename + ... on CollectionDocument { + ...CollectionDocumentReference + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + + fragment CollectionDocumentReference on CollectionDocument { + id + key + data + tags { + key + value + } + } + """ + ) + variables: Dict[str, object] = { + "organizationID": organization_id, + "key": key, + "tag": tag, + "after": after, + "first": first, + } + response = await self.execute( + query=query, + operation_name="collectionDocuments", + variables=variables, + **kwargs + ) + data = self.get_data(response) + return CollectionDocuments.model_validate(data) diff --git a/python/src/numerous/generated/graphql/collection_collections.py b/python/src/numerous/generated/graphql/collection_collections.py new file mode 100644 index 00000000..e50de521 --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_collections.py @@ -0,0 +1,55 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import List, Literal, Optional, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionReference + + +class CollectionCollections(BaseModel): + collection_create: Union[ + "CollectionCollectionsCollectionCreateCollection", + "CollectionCollectionsCollectionCreateCollectionNotFound", + ] = Field(alias="collectionCreate", discriminator="typename__") + + +class CollectionCollectionsCollectionCreateCollection(BaseModel): + typename__: Literal["Collection"] = Field(alias="__typename") + id: str + key: str + collections: "CollectionCollectionsCollectionCreateCollectionCollections" + + +class CollectionCollectionsCollectionCreateCollectionCollections(BaseModel): + edges: List["CollectionCollectionsCollectionCreateCollectionCollectionsEdges"] + page_info: "CollectionCollectionsCollectionCreateCollectionCollectionsPageInfo" = ( + Field(alias="pageInfo") + ) + + +class CollectionCollectionsCollectionCreateCollectionCollectionsEdges(BaseModel): + node: "CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode" + + +class CollectionCollectionsCollectionCreateCollectionCollectionsEdgesNode( + CollectionReference +): + pass + + +class CollectionCollectionsCollectionCreateCollectionCollectionsPageInfo(BaseModel): + has_next_page: bool = Field(alias="hasNextPage") + end_cursor: Optional[str] = Field(alias="endCursor") + + +class CollectionCollectionsCollectionCreateCollectionNotFound(BaseModel): + typename__: Literal["CollectionNotFound"] = Field(alias="__typename") + + +CollectionCollections.model_rebuild() +CollectionCollectionsCollectionCreateCollection.model_rebuild() +CollectionCollectionsCollectionCreateCollectionCollections.model_rebuild() +CollectionCollectionsCollectionCreateCollectionCollectionsEdges.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_create.py b/python/src/numerous/generated/graphql/collection_create.py index d7946b88..ef8c6956 100644 --- a/python/src/numerous/generated/graphql/collection_create.py +++ b/python/src/numerous/generated/graphql/collection_create.py @@ -6,7 +6,7 @@ from pydantic import Field from .base_model import BaseModel -from .fragments import CollectionReference, CollectionNotFound +from .fragments import CollectionNotFound, CollectionReference class CollectionCreate(BaseModel): @@ -22,3 +22,6 @@ class CollectionCreateCollectionCreateCollection(CollectionReference): class CollectionCreateCollectionCreateCollectionNotFound(CollectionNotFound): typename__: Literal["CollectionNotFound"] = Field(alias="__typename") + + +CollectionCreate.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_document.py b/python/src/numerous/generated/graphql/collection_document.py new file mode 100644 index 00000000..e500df53 --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_document.py @@ -0,0 +1,33 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import Literal, Optional, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocument(BaseModel): + collection_create: Union[ + "CollectionDocumentCollectionCreateCollection", + "CollectionDocumentCollectionCreateCollectionNotFound", + ] = Field(alias="collectionCreate", discriminator="typename__") + + +class CollectionDocumentCollectionCreateCollection(BaseModel): + typename__: Literal["Collection"] = Field(alias="__typename") + document: Optional["CollectionDocumentCollectionCreateCollectionDocument"] + + +class CollectionDocumentCollectionCreateCollectionDocument(CollectionDocumentReference): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentCollectionCreateCollectionNotFound(BaseModel): + typename__: Literal["CollectionNotFound"] = Field(alias="__typename") + + +CollectionDocument.model_rebuild() +CollectionDocumentCollectionCreateCollection.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_document_delete.py b/python/src/numerous/generated/graphql/collection_document_delete.py new file mode 100644 index 00000000..a407909b --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_document_delete.py @@ -0,0 +1,31 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import Literal, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocumentDelete(BaseModel): + collection_document_delete: Union[ + "CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument", + "CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound", + ] = Field(alias="collectionDocumentDelete", discriminator="typename__") + + +class CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocument( + CollectionDocumentReference +): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentDeleteCollectionDocumentDeleteCollectionDocumentNotFound( + BaseModel +): + typename__: Literal["CollectionDocumentNotFound"] = Field(alias="__typename") + + +CollectionDocumentDelete.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_document_set.py b/python/src/numerous/generated/graphql/collection_document_set.py new file mode 100644 index 00000000..e90380d8 --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_document_set.py @@ -0,0 +1,29 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import Literal, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocumentSet(BaseModel): + collection_document_set: Union[ + "CollectionDocumentSetCollectionDocumentSetCollectionDocument", + "CollectionDocumentSetCollectionDocumentSetCollectionNotFound", + ] = Field(alias="collectionDocumentSet", discriminator="typename__") + + +class CollectionDocumentSetCollectionDocumentSetCollectionDocument( + CollectionDocumentReference +): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentSetCollectionDocumentSetCollectionNotFound(BaseModel): + typename__: Literal["CollectionNotFound"] = Field(alias="__typename") + + +CollectionDocumentSet.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_document_tag_add.py b/python/src/numerous/generated/graphql/collection_document_tag_add.py new file mode 100644 index 00000000..00196572 --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_document_tag_add.py @@ -0,0 +1,31 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import Literal, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocumentTagAdd(BaseModel): + collection_document_tag_add: Union[ + "CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument", + "CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound", + ] = Field(alias="collectionDocumentTagAdd", discriminator="typename__") + + +class CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocument( + CollectionDocumentReference +): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentTagAddCollectionDocumentTagAddCollectionDocumentNotFound( + BaseModel +): + typename__: Literal["CollectionDocumentNotFound"] = Field(alias="__typename") + + +CollectionDocumentTagAdd.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_document_tag_delete.py b/python/src/numerous/generated/graphql/collection_document_tag_delete.py new file mode 100644 index 00000000..c996b5fc --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_document_tag_delete.py @@ -0,0 +1,31 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import Literal, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocumentTagDelete(BaseModel): + collection_document_tag_delete: Union[ + "CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument", + "CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound", + ] = Field(alias="collectionDocumentTagDelete", discriminator="typename__") + + +class CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocument( + CollectionDocumentReference +): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentTagDeleteCollectionDocumentTagDeleteCollectionDocumentNotFound( + BaseModel +): + typename__: Literal["CollectionDocumentNotFound"] = Field(alias="__typename") + + +CollectionDocumentTagDelete.model_rebuild() diff --git a/python/src/numerous/generated/graphql/collection_documents.py b/python/src/numerous/generated/graphql/collection_documents.py new file mode 100644 index 00000000..ca558608 --- /dev/null +++ b/python/src/numerous/generated/graphql/collection_documents.py @@ -0,0 +1,55 @@ +# Generated by ariadne-codegen +# Source: queries.gql + +from typing import List, Literal, Optional, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import CollectionDocumentReference + + +class CollectionDocuments(BaseModel): + collection_create: Union[ + "CollectionDocumentsCollectionCreateCollection", + "CollectionDocumentsCollectionCreateCollectionNotFound", + ] = Field(alias="collectionCreate", discriminator="typename__") + + +class CollectionDocumentsCollectionCreateCollection(BaseModel): + typename__: Literal["Collection"] = Field(alias="__typename") + id: str + key: str + documents: "CollectionDocumentsCollectionCreateCollectionDocuments" + + +class CollectionDocumentsCollectionCreateCollectionDocuments(BaseModel): + edges: List["CollectionDocumentsCollectionCreateCollectionDocumentsEdges"] + page_info: "CollectionDocumentsCollectionCreateCollectionDocumentsPageInfo" = Field( + alias="pageInfo" + ) + + +class CollectionDocumentsCollectionCreateCollectionDocumentsEdges(BaseModel): + node: "CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode" + + +class CollectionDocumentsCollectionCreateCollectionDocumentsEdgesNode( + CollectionDocumentReference +): + typename__: Literal["CollectionDocument"] = Field(alias="__typename") + + +class CollectionDocumentsCollectionCreateCollectionDocumentsPageInfo(BaseModel): + has_next_page: bool = Field(alias="hasNextPage") + end_cursor: Optional[str] = Field(alias="endCursor") + + +class CollectionDocumentsCollectionCreateCollectionNotFound(BaseModel): + typename__: Literal["CollectionNotFound"] = Field(alias="__typename") + + +CollectionDocuments.model_rebuild() +CollectionDocumentsCollectionCreateCollection.model_rebuild() +CollectionDocumentsCollectionCreateCollectionDocuments.model_rebuild() +CollectionDocumentsCollectionCreateCollectionDocumentsEdges.model_rebuild() diff --git a/python/src/numerous/generated/graphql/enums.py b/python/src/numerous/generated/graphql/enums.py index dde88a8a..6ecf52a1 100644 --- a/python/src/numerous/generated/graphql/enums.py +++ b/python/src/numerous/generated/graphql/enums.py @@ -33,6 +33,8 @@ class SubscriptionOfferStatus(str, Enum): class AppSubscriptionStatus(str, Enum): ACTIVE = "ACTIVE" WITHDRAWN = "WITHDRAWN" + EXPIRED = "EXPIRED" + CANCELED = "CANCELED" class ToolHashType(str, Enum): @@ -44,3 +46,4 @@ class ToolHashType(str, Enum): class PaymentAccountStatus(str, Enum): RESTRICTED = "RESTRICTED" VERIFIED = "VERIFIED" + UNKNOWN = "UNKNOWN" diff --git a/python/src/numerous/generated/graphql/fragments.py b/python/src/numerous/generated/graphql/fragments.py index 491044c7..1d91f1d4 100644 --- a/python/src/numerous/generated/graphql/fragments.py +++ b/python/src/numerous/generated/graphql/fragments.py @@ -1,7 +1,7 @@ # Generated by ariadne-codegen # Source: queries.gql -from typing import List, Literal, Optional +from typing import Any, List, Literal, Optional from pydantic import Field @@ -12,15 +12,27 @@ class ButtonValue(BaseModel): button_value: str = Field(alias="buttonValue") -class CollectionReference(BaseModel): +class CollectionDocumentReference(BaseModel): id: str key: str + data: Any + tags: List["CollectionDocumentReferenceTags"] + + +class CollectionDocumentReferenceTags(BaseModel): + key: str + value: str class CollectionNotFound(BaseModel): id: str +class CollectionReference(BaseModel): + id: str + key: str + + class GraphContext(BaseModel): parent: Optional["GraphContextParent"] affected_by: List[Optional["GraphContextAffectedBy"]] = Field(alias="affectedBy") @@ -83,8 +95,9 @@ class TextFieldValue(BaseModel): ButtonValue.model_rebuild() -CollectionReference.model_rebuild() +CollectionDocumentReference.model_rebuild() CollectionNotFound.model_rebuild() +CollectionReference.model_rebuild() GraphContext.model_rebuild() HTMLValue.model_rebuild() NumberFieldValue.model_rebuild() diff --git a/python/src/numerous/generated/graphql/input_types.py b/python/src/numerous/generated/graphql/input_types.py index dadfb8a3..dfabe2bd 100644 --- a/python/src/numerous/generated/graphql/input_types.py +++ b/python/src/numerous/generated/graphql/input_types.py @@ -66,6 +66,11 @@ class AppDeleteInput(BaseModel): organization_slug: str = Field(alias="organizationSlug") +class AppVersionCreateGitHubInput(BaseModel): + owner: str + repo: str + + class PaymentConfigurationInput(BaseModel): monthly_price_usd: Any = Field(alias="monthlyPriceUSD") trial_days: Optional[int] = Field(alias="trialDays", default=None) @@ -107,3 +112,8 @@ class BuildPushInput(BaseModel): class TagInput(BaseModel): key: str value: str + + +AppDeployInput.model_rebuild() +SubscriptionOfferInput.model_rebuild() +BuildPushInput.model_rebuild() diff --git a/python/src/numerous/generated/graphql/update_element.py b/python/src/numerous/generated/graphql/update_element.py index d53fc20b..0955321c 100644 --- a/python/src/numerous/generated/graphql/update_element.py +++ b/python/src/numerous/generated/graphql/update_element.py @@ -24,3 +24,6 @@ class UpdateElementElementUpdate(BaseModel): "SliderElement", "TextField", ] = Field(alias="__typename") + + +UpdateElement.model_rebuild() diff --git a/python/src/numerous/generated/graphql/updates.py b/python/src/numerous/generated/graphql/updates.py index 666aad7d..cbd0e11b 100644 --- a/python/src/numerous/generated/graphql/updates.py +++ b/python/src/numerous/generated/graphql/updates.py @@ -147,3 +147,14 @@ class UpdatesToolSessionEventToolSessionActionTriggered(BaseModel): class UpdatesToolSessionEventToolSessionActionTriggeredElement(BaseModel): id: str name: str + + +Updates.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdated.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementElement.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementButton.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementHTMLElement.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementNumberField.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementSliderElement.model_rebuild() +UpdatesToolSessionEventToolSessionElementUpdatedElementTextField.model_rebuild() +UpdatesToolSessionEventToolSessionActionTriggered.model_rebuild() diff --git a/python/src/numerous/jsonbase64.py b/python/src/numerous/jsonbase64.py new file mode 100644 index 00000000..edff48e0 --- /dev/null +++ b/python/src/numerous/jsonbase64.py @@ -0,0 +1,23 @@ +"""dict to base64 conversion.""" + +import base64 +import json +from typing import cast + + +DECODED_JSON_NOT_DICT = "Decoded JSON is not a dictionary" + + +def dict_to_base64(input_dict: dict[str, str]) -> str: + json_str = json.dumps(input_dict) + json_bytes = json_str.encode("utf-8") + base64_bytes = base64.b64encode(json_bytes) + return base64_bytes.decode("utf-8") + + +def base64_to_dict(base64_str: str) -> dict[str, str]: + json_str = base64.b64decode(base64_str).decode("utf-8") + result = json.loads(json_str) + if not isinstance(result, dict): + raise TypeError(DECODED_JSON_NOT_DICT) + return cast(dict[str, str], result) diff --git a/python/src/numerous/session.py b/python/src/numerous/session.py index cd1f7c51..5e5bdef3 100644 --- a/python/src/numerous/session.py +++ b/python/src/numerous/session.py @@ -1,12 +1,9 @@ """App sessions manages app instances.""" -import asyncio import logging import random import string import threading -import typing -from concurrent.futures import Future from typing import Any, Callable, Generic, Optional, Type, Union from plotly import graph_objects as go @@ -27,6 +24,7 @@ ) from numerous.generated.graphql.fragments import GraphContextParent from numerous.generated.graphql.input_types import ElementInput +from numerous.threaded_event_loop import ThreadedEventLoop from numerous.updates import UpdateHandler from numerous.utils import AppT @@ -84,36 +82,6 @@ def __init__(self, elem: ElementDataModel) -> None: super().__init__(f"Tool session missing required element '{elem.name}'") -class ThreadedEventLoop: - """Wrapper for an asyncio event loop running in a thread.""" - - def __init__(self) -> None: - self._loop = asyncio.new_event_loop() - self._thread = threading.Thread( - target=self._run_loop_forever, - name="Event Loop Thread", - daemon=True, - ) - - def start(self) -> None: - """Start the thread and run the event loop.""" - if not self._thread.is_alive(): - self._thread.start() - - def stop(self) -> None: - """Stop the event loop, and terminate the thread.""" - if self._thread.is_alive(): - self._loop.stop() - - def schedule(self, coroutine: typing.Awaitable[typing.Any]) -> Future[Any]: - """Schedule a coroutine in the event loop.""" - return asyncio.run_coroutine_threadsafe(coroutine, self._loop) - - def _run_loop_forever(self) -> None: - asyncio.set_event_loop(self._loop) - self._loop.run_forever() - - class Session(Generic[AppT]): def __init__( self, diff --git a/python/src/numerous/threaded_event_loop.py b/python/src/numerous/threaded_event_loop.py new file mode 100644 index 00000000..88906a0c --- /dev/null +++ b/python/src/numerous/threaded_event_loop.py @@ -0,0 +1,44 @@ +"""Separate Thread Manager for Asyncio Operations.""" + +import asyncio +import threading +import typing +from concurrent.futures import Future + + +T = typing.TypeVar("T") + + +class ThreadedEventLoop: + """Wrapper for an asyncio event loop running in a thread.""" + + def __init__(self) -> None: + self._loop = asyncio.new_event_loop() + self._thread = threading.Thread( + target=self._run_loop_forever, + name="Event Loop Thread", + daemon=True, + ) + + def start(self) -> None: + """Start the thread and run the event loop.""" + if not self._thread.is_alive(): + self._thread.start() + + def stop(self) -> None: + """Stop the event loop, and terminate the thread.""" + if self._thread.is_alive(): + self._loop.stop() + + def await_coro(self, coroutine: typing.Awaitable[T]) -> T: + """Awaiting for coroutine to finish.""" + f = self.schedule(coroutine) + return f.result() + + def schedule(self, coroutine: typing.Awaitable[T]) -> Future[T]: + """Schedule a coroutine in the event loop.""" + return asyncio.run_coroutine_threadsafe(coroutine, self._loop) + + def _run_loop_forever(self) -> None: + asyncio.set_event_loop(self._loop) + self._loop.run_forever() diff --git a/python/tests/test_collections.py b/python/tests/test_collections.py index 3e7e25a0..f776d018 100644 --- a/python/tests/test_collections.py +++ b/python/tests/test_collections.py @@ -1,11 +1,27 @@ -from unittest.mock import Mock +from unittest.mock import Mock, call import pytest from numerous import collection -from numerous._client import Client +from numerous._client import COLLECTED_OBJECTS_NUMBER, Client +from numerous.collection.numerous_document import NumerousDocument from numerous.generated.graphql.client import Client as GQLClient +from numerous.generated.graphql.collection_collections import CollectionCollections from numerous.generated.graphql.collection_create import CollectionCreate +from numerous.generated.graphql.collection_document import CollectionDocument +from numerous.generated.graphql.collection_document_delete import ( + CollectionDocumentDelete, +) +from numerous.generated.graphql.collection_document_set import CollectionDocumentSet +from numerous.generated.graphql.collection_document_tag_add import ( + CollectionDocumentTagAdd, +) +from numerous.generated.graphql.collection_document_tag_delete import ( + CollectionDocumentTagDelete, +) +from numerous.generated.graphql.collection_documents import CollectionDocuments +from numerous.generated.graphql.input_types import TagInput +from numerous.jsonbase64 import dict_to_base64 ORGANIZATION_ID = "test_org" @@ -15,6 +31,11 @@ COLLECTION_REFERENCE_ID = "test_id" NESTED_COLLECTION_REFERENCE_KEY = "nested_test_key" NESTED_COLLECTION_REFERENCE_ID = "nested_test_id" +COLLECTION_DOCUMENT_KEY = "test_document" +DOCUMENT_DATA = {"test": "test"} +BASE64_DOCUMENT_DATA = dict_to_base64(DOCUMENT_DATA) +DOCUMENT_ID = "915b75c5-9e95-4fa7-aaa2-2214c8d251ce" +HEADERS_WITH_AUTHORIZATION = {"headers": {"Authorization": "Bearer token"}} def _collection_create_collection_reference(key: str, ref_id: str) -> CollectionCreate: @@ -23,6 +44,158 @@ def _collection_create_collection_reference(key: str, ref_id: str) -> Collection ) +def _collection_document_set_reference(key: str) -> CollectionDocumentSet: + return CollectionDocumentSet.model_validate( + { + "collectionDocumentSet": { + "__typename": "CollectionDocument", + "id": DOCUMENT_ID, + "key": key, + "data": BASE64_DOCUMENT_DATA, + "tags": [], + } + } + ) + + +def _collection_document_tag_delete_found(_id: str) -> CollectionDocumentTagDelete: + return CollectionDocumentTagDelete.model_validate( + { + "collectionDocumentTagDelete": { + "__typename": "CollectionDocument", + "id": _id, + "key": "t21", + "data": BASE64_DOCUMENT_DATA, + "tags": [], + } + } + ) + + +def _collection_document_tag_add_found(_id: str) -> CollectionDocumentTagAdd: + return CollectionDocumentTagAdd.model_validate( + { + "collectionDocumentTagAdd": { + "__typename": "CollectionDocument", + "id": _id, + "key": "t21", + "data": BASE64_DOCUMENT_DATA, + "tags": [{"key": "key", "value": "test"}], + } + } + ) + + +def _collection_document_delete_found(_id: str) -> CollectionDocumentDelete: + return CollectionDocumentDelete.model_validate( + { + "collectionDocumentDelete": { + "__typename": "CollectionDocument", + "id": _id, + "key": "t21", + "data": BASE64_DOCUMENT_DATA, + "tags": [], + } + } + ) + + +def _collection_collections(_id: str) -> CollectionCollections: + return CollectionCollections.model_validate( + { + "collectionCreate": { + "__typename": "Collection", + "id": "1a9299d1-5c81-44bb-b94f-ba40afc05f3a", + "key": "root_collection", + "collections": { + "edges": [ + { + "node": { + "__typename": "Collection", + "id": "496da1f7-5378-4962-8373-5c30663848cf", + "key": "collection0", + } + }, + { + "node": { + "__typename": "Collection", + "id": "6ae8ee18-8ebb-4206-aba1-8d2b44c22682", + "key": "collection1", + } + }, + { + "node": { + "__typename": "Collection", + "id": "deb5ee57-e4ba-470c-a913-a6a619e9661d", + "key": "collection2", + } + }, + ], + "pageInfo": { + "hasNextPage": "false", + "endCursor": "deb5ee57-e4ba-470c-a913-a6a619e9661d", + }, + }, + } + } + ) + + +def _collection_documents_reference(key: str) -> CollectionDocuments: + return CollectionDocuments.model_validate( + { + "collectionCreate": { + "__typename": "Collection", + "id": "0d2f82fa-1546-49a4-a034-3392eefc3e4e", + "key": "t1", + "documents": { + "edges": [ + { + "node": { + "__typename": "CollectionDocument", + "id": "10634601-67b5-4015-840c-155d9faf9591", + "key": key, + "data": "ewogICJoZWxsbyI6ICJ3b3JsZCIKfQ==", + "tags": [{"key": "key", "value": "test"}], + } + }, + { + "node": { + "__typename": "CollectionDocument", + "id": "915b75c5-9e95-4fa7-aaa2-2214c8d251ce", + "key": key + "1", + "data": "ewogICJoZWxsbyI6ICJ3b3JsZCIKfQ==", + "tags": [], + } + }, + ], + "pageInfo": { + "hasNextPage": "false", + "endCursor": "915b75c5-9e95-4fa7-aaa2-2214c8d251ce", + }, + }, + } + } + ) + + +def _collection_document_reference(key: str) -> CollectionDocument: + return CollectionDocument.model_validate( + { + "collectionCreate": { + "__typename": "Collection", + "document": { + "__typename": "CollectionDocument", + "id": DOCUMENT_ID, + "key": key, + "data": BASE64_DOCUMENT_DATA, + "tags": [], + }, + } + } + ) + + def _collection_create_collection_not_found(ref_id: str) -> CollectionCreate: return CollectionCreate.model_validate( {"collectionCreate": {"typename__": "CollectionNotFound", "id": ref_id}} @@ -32,6 +205,7 @@ def _collection_create_collection_not_found(ref_id: str) -> CollectionCreate: @pytest.fixture(autouse=True) def _set_env_vars(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("NUMEROUS_API_URL", "url_value") + monkeypatch.setenv("ORGANIZATION_ID", ORGANIZATION_ID) monkeypatch.setenv("NUMEROUS_API_ACCESS_TOKEN", "token") @@ -41,15 +215,14 @@ def test_collection_returns_new_collection() -> None: gql.collection_create.return_value = _collection_create_collection_reference( COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID ) - organization_id = "" + parent_key = None - kwargs = {"headers": {"Authorization": "Bearer token"}} result = collection(COLLECTION_NAME, _client) gql.collection_create.assert_called_once() gql.collection_create.assert_called_once_with( - organization_id, COLLECTION_NAME, parent_key, kwargs=kwargs + ORGANIZATION_ID, COLLECTION_NAME, parent_key, **HEADERS_WITH_AUTHORIZATION ) assert result.key == COLLECTION_REFERENCE_KEY assert result.id == COLLECTION_REFERENCE_ID @@ -59,7 +232,8 @@ def test_collection_returns_new_nested_collection() -> None: gql = Mock(GQLClient) _client = Client(gql) gql.collection_create.return_value = _collection_create_collection_reference( - NESTED_COLLECTION_REFERENCE_KEY, NESTED_COLLECTION_REFERENCE_ID + NESTED_COLLECTION_REFERENCE_KEY, + NESTED_COLLECTION_REFERENCE_ID, ) result = collection(COLLECTION_NAME, _client) @@ -85,3 +259,266 @@ def test_nested_collection_not_found_returns_none() -> None: nested_result = result.collection(NESTED_COLLECTION_ID) assert nested_result is None + + +def test_collection_document_returns_new_document() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + test_collection = collection(COLLECTION_NAME, _client) + + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + + gql.collection_document.assert_called_once_with( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + COLLECTION_DOCUMENT_KEY, + **HEADERS_WITH_AUTHORIZATION, + ) + assert isinstance(document, NumerousDocument) + assert document.exists is False + + +def test_collection_document_returns_existing_document() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document.return_value = _collection_document_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + + gql.collection_document.assert_called_once_with( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + COLLECTION_DOCUMENT_KEY, + **HEADERS_WITH_AUTHORIZATION, + ) + assert isinstance(document, NumerousDocument) + assert document.exists + + +def test_collection_document_set_data_uploads_document() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document_set.return_value = _collection_document_set_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + assert isinstance(document, NumerousDocument) + assert document.exists is False + + document.set({"test": "test"}) + + gql.collection_document_set.assert_called_once_with( + COLLECTION_REFERENCE_ID, + COLLECTION_DOCUMENT_KEY, + BASE64_DOCUMENT_DATA, + **HEADERS_WITH_AUTHORIZATION, + ) + assert document.exists + + +def test_collection_document_get_returns_dict() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document.return_value = _collection_document_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + + data = document.get() + + assert isinstance(document, NumerousDocument) + gql.collection_document.assert_has_calls( + [ + call( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + COLLECTION_DOCUMENT_KEY, + **HEADERS_WITH_AUTHORIZATION, + ), + call( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + COLLECTION_DOCUMENT_KEY, + **HEADERS_WITH_AUTHORIZATION, + ), + ] + ) + assert document.exists + assert data == DOCUMENT_DATA + + +def test_collection_document_delete_marks_document_exists_false() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document.return_value = _collection_document_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + assert document.document_id is not None + gql.collection_document_delete.return_value = _collection_document_delete_found( + document.document_id + ) + assert document.exists + + document.delete() + + gql.collection_document_delete.assert_called_once_with( + DOCUMENT_ID, **HEADERS_WITH_AUTHORIZATION + ) + assert document.exists is False + + +def test_collection_document_tag_add() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document.return_value = _collection_document_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + assert document.document_id is not None + gql.collection_document_tag_add.return_value = _collection_document_tag_add_found( + document.document_id + ) + assert document.exists + + document.tag("key", "test") + + gql.collection_document_tag_add.assert_called_once_with( + DOCUMENT_ID, TagInput(key="key", value="test"), **HEADERS_WITH_AUTHORIZATION + ) + assert document.tags == {"key": "test"} + + +def test_collection_document_tag_delete() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_document.return_value = _collection_document_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + document = test_collection.document(COLLECTION_DOCUMENT_KEY) + assert document.document_id is not None + gql.collection_document_tag_add.return_value = _collection_document_tag_add_found( + document.document_id + ) + gql.collection_document_tag_delete.return_value = ( + _collection_document_tag_delete_found(document.document_id) + ) + assert document.exists + document.tag("key", "test") + assert document.tags == {"key": "test"} + + document.tag_delete("key") + + assert document.tags == {} + gql.collection_document_tag_delete.assert_called_once_with( + DOCUMENT_ID, "key", **HEADERS_WITH_AUTHORIZATION + ) + + +def test_collection_documents_return_more_than_one() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_documents.return_value = _collection_documents_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + + result = [] + expected_number_of_documents = 2 + for document in test_collection.documents(): + assert document.exists + result.append(document) + + assert len(result) == expected_number_of_documents + gql.collection_documents.assert_called_once_with( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + None, + after="", + first=COLLECTED_OBJECTS_NUMBER, + **HEADERS_WITH_AUTHORIZATION, + ) + + +def test_collection_documents_query_tag_specific_document() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_documents.return_value = _collection_documents_reference( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + + tag_key = "key" + tag_value = "value" + for document in test_collection.documents(tag_key=tag_key, tag_value=tag_value): + assert document.exists + + gql.collection_documents.assert_called_once_with( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + TagInput(key=tag_key, value=tag_value), + after="", + first=COLLECTED_OBJECTS_NUMBER, + **HEADERS_WITH_AUTHORIZATION, + ) + + +def test_collection_collections_return_more_than_one() -> None: + gql = Mock(GQLClient) + _client = Client(gql) + gql.collection_create.return_value = _collection_create_collection_reference( + COLLECTION_REFERENCE_KEY, COLLECTION_REFERENCE_ID + ) + gql.collection_collections.return_value = _collection_collections( + COLLECTION_DOCUMENT_KEY + ) + test_collection = collection(COLLECTION_NAME, _client) + result = [] + expected_number_of_collections = 3 + for collection_element in test_collection.collections(): + assert collection_element.key + result.append(collection_element) + + assert len(result) == expected_number_of_collections + gql.collection_collections.assert_called_once_with( + ORGANIZATION_ID, + COLLECTION_REFERENCE_KEY, + after="", + first=COLLECTED_OBJECTS_NUMBER, + **HEADERS_WITH_AUTHORIZATION, + ) diff --git a/shared/schema.gql b/shared/schema.gql index 8ace3a4b..359abd32 100644 --- a/shared/schema.gql +++ b/shared/schema.gql @@ -455,6 +455,18 @@ type AppDeploymentHeartbeat { proxyURL: String } +input AppVersionCreateGitHubInput { + owner: String! + repo: String! +} + +type GitHubRepositoryNotFound { + owner: String! + repo: String! +} + +union AppVersionCreateGitHubResult = AppVersion | GitHubRepositoryNotFound + extend type Query { app(organizationSlug: String!, appSlug: String!): App @hasRole(role: USER) } @@ -479,6 +491,12 @@ extend type Mutation { appDeployHeartbeat(deployID: ID!): AppDeploymentHeartbeat! @canUseApp @trackAppUseOperation + appVersionCreateGitHub( + appID: ID! + input: AppVersionCreateGitHubInput! + ): AppVersionCreateGitHubResult + @canManageApp + @trackAppOperation(eventName: "App Version Create from GitHub") } extend type Subscription { @@ -498,7 +516,8 @@ scalar Decimal directive @canAccessSubscriptionOffer on FIELD_DEFINITION directive @canAccessSubscription on FIELD_DEFINITION -directive @canManageSubscription on FIELD_DEFINITION +directive @canManageInboundSubscription on FIELD_DEFINITION +directive @canManageOutboundSubscription on FIELD_DEFINITION enum SubscriptionOfferStatus { ACCEPTED @@ -510,6 +529,8 @@ enum SubscriptionOfferStatus { enum AppSubscriptionStatus { ACTIVE WITHDRAWN + EXPIRED + CANCELED } type AppSubscription { @@ -518,6 +539,9 @@ type AppSubscription { inboundOrganization: Organization! outboundOrganization: Organization! status: AppSubscriptionStatus! + createdAt: Time! + expiresAt: Time + monthlyPriceUSD: Decimal } type PaymentConfiguration { @@ -555,6 +579,14 @@ type SubscriptionOfferInvalidStatus { id: ID! } +type SubscriptionOfferCheckoutSession { + portalURL: String! +} + +type OrganizationMissingPaymentMethod { + organizationID: ID! +} + type AppSubscriptionNotFound { id: ID! } @@ -576,6 +608,8 @@ union SubscriptionOfferResult = SubscriptionOffer | SubscriptionOfferNotFound union SubscriptionOfferAcceptResult = AppSubscription + | OrganizationMissingPaymentMethod + | SubscriptionOfferCheckoutSession | SubscriptionOfferNotFound | SubscriptionOfferInvalidStatus @@ -618,8 +652,10 @@ extend type Mutation { subscriptionOfferWithdraw( subscriptionOfferId: ID! ): SubscriptionOfferWithdraw! @canAccessSubscriptionOffer - subscriptionCancel(subscriptionId: ID!): AppSubscriptionCancelResult! - @canManageSubscription + subscriptionCancelInbound(subscriptionId: ID!): AppSubscriptionCancelResult! + @canManageInboundSubscription + subscriptionCancelOutbound(subscriptionId: ID!): AppSubscriptionCancelResult! + @canManageOutboundSubscription } ############################################################################### @@ -1117,6 +1153,7 @@ type Tag { enum PaymentAccountStatus { RESTRICTED VERIFIED + UNKNOWN } type PaymentAccount {