talisman-api 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ __all__ = [
2
+ 'AbstractTalismanAPI',
3
+ 'APISchema', 'CompositeTalismanAPIClient',
4
+ 'version',
5
+ 'TalismanAPIClient'
6
+ ]
7
+
8
+ from .abstract import AbstractTalismanAPI
9
+ from .api_client import APISchema, CompositeTalismanAPIClient
10
+ from .decorator import version
11
+ from .wrapper import TalismanAPIClient
@@ -0,0 +1,91 @@
1
+ import inspect
2
+ import logging
3
+ from abc import ABCMeta, abstractmethod
4
+ from importlib import import_module, resources
5
+ from typing import Iterable
6
+
7
+ from gql import gql
8
+ from graphql import DocumentNode, GraphQLError
9
+ from typing_extensions import Self
10
+
11
+ from .api_client import CompositeTalismanAPIClient
12
+ from .api_client.composite import APISchema
13
+ from .wrapper import TalismanAPIClient
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+
18
+ class AbstractTalismanAPI(metaclass=ABCMeta):
19
+ _VERSION: str
20
+
21
+ def __init__(self, client: CompositeTalismanAPIClient, documents: dict[APISchema, DocumentNode]):
22
+ if set(client) != set(documents):
23
+ raise ValueError
24
+ self._composite_client: CompositeTalismanAPIClient = client
25
+ self._documents: dict[APISchema, DocumentNode] = documents
26
+
27
+ def _client(self, schema: APISchema) -> TalismanAPIClient:
28
+ return TalismanAPIClient(self._composite_client[schema], self._documents[schema])
29
+
30
+ @classmethod
31
+ @abstractmethod
32
+ def _required_apis(cls) -> Iterable[APISchema]:
33
+ pass
34
+
35
+ @classmethod
36
+ async def create(cls, client: CompositeTalismanAPIClient) -> Self:
37
+ documents: dict[APISchema, DocumentNode] = {}
38
+ for api_schema in cls._required_apis():
39
+ if api_schema not in client:
40
+ raise ValueError(f'Client is not configured to use {api_schema} API')
41
+
42
+ package, _ = cls.__module__.rsplit('.', maxsplit=1)
43
+ resource_name = f'graphql/{cls._VERSION}/{api_schema.value}.graphql'
44
+ try:
45
+ with (resources.files(package) / resource_name).open('r', encoding='utf-8') as f:
46
+ document: str = f.read()
47
+ except Exception as e:
48
+ raise ValueError(f'Package {package} contains no required {resource_name} file') from e
49
+ try:
50
+ document: DocumentNode = gql(document)
51
+ except GraphQLError as e:
52
+ raise ValueError(f'GraphQL syntax error if {resource_name} (package {package})') from e
53
+
54
+ api_client = client[api_schema]
55
+ async with api_client:
56
+ if not api_client.validate(document):
57
+ raise ValueError(f'{resource_name} (package {package}) do not match runtime server {api_schema} API')
58
+ documents[api_schema] = document
59
+ return cls(client, documents)
60
+
61
+ @classmethod
62
+ async def get_compatible_api(
63
+ cls,
64
+ client: CompositeTalismanAPIClient,
65
+ *,
66
+ logger: logging.Logger = _LOGGER,
67
+ impl_package: str = '_impl'
68
+ ) -> Self:
69
+ package, _ = cls.__module__.rsplit('.', 1)
70
+ package_name = f'{package}.{impl_package}'
71
+ package = import_module(package_name)
72
+ if not hasattr(package, '__all__'):
73
+ raise ValueError(
74
+ f'{package_name}.__init__ should define __all__ attribute with ordered API implementation classes'
75
+ )
76
+ for impl_name in package.__all__:
77
+ impl_cls = getattr(package, impl_name, None)
78
+ if not inspect.isclass(impl_cls):
79
+ logger.error(f'{package_name}.__all__ should contains only API implementation classes')
80
+ continue
81
+ if not issubclass(impl_cls, cls):
82
+ logger.error(f'{package_name}.{impl_name} should inherit {cls}')
83
+ continue
84
+ try:
85
+ return await impl_cls.create(client)
86
+ except ValueError as e:
87
+ logger.info(
88
+ f'{package_name}.{impl_name} (version: "{impl_cls._VERSION}") is not compatible with actual API: {e}',
89
+ exc_info=e
90
+ )
91
+ raise NotImplementedError('API version is not supported')
@@ -0,0 +1,9 @@
1
+ __all__ = [
2
+ 'TalismanAPIClient',
3
+ 'APISchema', 'CompositeTalismanAPIClient',
4
+ 'GQLClientConfig'
5
+ ]
6
+
7
+ from .client import TalismanAPIClient
8
+ from .composite import APISchema, CompositeTalismanAPIClient
9
+ from .gql_clients import GQLClientConfig
@@ -0,0 +1,112 @@
1
+ import asyncio
2
+ import logging
3
+ from contextlib import AbstractAsyncContextManager
4
+ from typing import AsyncIterator, Callable
5
+
6
+ from graphql import DocumentNode
7
+ from requests import Timeout
8
+
9
+ from .gql_clients import AsyncAbstractGQLClient, GQLClientConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TalismanAPIClient(AbstractAsyncContextManager):
15
+
16
+ def __init__(self, config: GQLClientConfig):
17
+ self._config = config
18
+ self._gql_client: AsyncAbstractGQLClient | None = None
19
+
20
+ self._lock = asyncio.Lock()
21
+ self._opened: int = 0
22
+ self._close_task = None
23
+
24
+ async def __aenter__(self):
25
+ async with self._lock:
26
+ self._opened += 1
27
+ if self._gql_client is None:
28
+ self._gql_client = self._config.configure()
29
+ await self._gql_client.__aenter__()
30
+ if self._close_task is not None:
31
+ self._close_task.cancel()
32
+ self._close_task = None
33
+ return self
34
+
35
+ async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
36
+ async with self._lock:
37
+ self._opened -= 1
38
+ if self._opened == 0 and self._close_task is None:
39
+ self._close_task = asyncio.create_task(self._delayed_close())
40
+
41
+ async def _delayed_close(self, delay: int = 10):
42
+ try:
43
+ await asyncio.sleep(delay)
44
+ async with self._lock:
45
+ if self._opened == 0 and self._gql_client is not None:
46
+ await self._gql_client.__aexit__(None, None, None)
47
+ self._gql_client = None
48
+ except asyncio.CancelledError:
49
+ pass
50
+
51
+ def validate(self, document: DocumentNode) -> bool:
52
+ return self._gql_client.validate(document)
53
+
54
+ async def execute(self, document: DocumentNode, operation_name: str, variables: dict | None = None, raise_on_timeout: bool = True):
55
+ try:
56
+ async with self:
57
+ return await self._gql_client.execute(document, operation_name=operation_name, variables=variables)
58
+
59
+ except Timeout as e:
60
+ logger.error('Timeout while query processing', exc_info=e,
61
+ extra={'variables': str(variables), 'operation_name': operation_name})
62
+ if raise_on_timeout:
63
+ raise e
64
+ except Exception as e:
65
+ logger.error('Some exception was occured during query processing.', exc_info=e,
66
+ extra={'variables': str(variables), 'operation_name': operation_name})
67
+
68
+ raise
69
+
70
+ async def paginate_items(
71
+ self,
72
+ document: DocumentNode,
73
+ operation_name: str,
74
+ variables: dict = None,
75
+ *,
76
+ page_size: int = 50,
77
+ extract_page: Callable[[dict], dict] = lambda d: d
78
+ ) -> AsyncIterator[dict]:
79
+ if variables is None:
80
+ variables = {}
81
+ total = 1 # some value greater than 0
82
+ offset = 0
83
+ while offset < total:
84
+ query_variables = {**variables, "offset": offset, "limit": page_size}
85
+ page = extract_page(await self.execute(document, operation_name, query_variables))
86
+ total = page['pagination']['total']
87
+ items = page['pagination'].get('list', tuple())
88
+ for item in items:
89
+ yield item
90
+ offset += len(items)
91
+
92
+ async def get_all_items(
93
+ self,
94
+ document: DocumentNode,
95
+ operation_name: str,
96
+ variables: dict = None,
97
+ *,
98
+ extract_page: Callable[[dict], dict] = lambda d: d
99
+ ):
100
+ if variables is None:
101
+ variables = {}
102
+ # get total only
103
+ query_variables = {**variables, "offset": 0, "limit": 0}
104
+ page = extract_page(await self.execute(document, operation_name, query_variables))
105
+ total = page['pagination']['total']
106
+
107
+ # get all items
108
+ query_variables = {**variables, "offset": 0, "limit": total}
109
+ page = extract_page(await self.execute(document, operation_name, query_variables))
110
+ items = page['pagination'].get('list', tuple())
111
+ for item in items:
112
+ yield item
@@ -0,0 +1,29 @@
1
+ from enum import Enum
2
+ from typing import Iterator, Mapping
3
+
4
+ from .client import TalismanAPIClient
5
+ from .gql_clients import GQLClientConfig
6
+
7
+
8
+ class APISchema(str, Enum):
9
+ PUBLIC = "public"
10
+ KB_UTILS = "kbutils"
11
+
12
+
13
+ class CompositeTalismanAPIClient(Mapping[APISchema, TalismanAPIClient]):
14
+ def __init__(self, config: dict[str | APISchema, GQLClientConfig]):
15
+ self._api_clients: dict[APISchema, TalismanAPIClient] = {
16
+ APISchema(key): TalismanAPIClient(value) for key, value in config.items()
17
+ }
18
+
19
+ def __contains__(self, item: object) -> bool:
20
+ return item in self._api_clients
21
+
22
+ def __getitem__(self, item: APISchema) -> TalismanAPIClient:
23
+ return self._api_clients.get(item)
24
+
25
+ def __iter__(self) -> Iterator[APISchema]:
26
+ return iter(self._api_clients)
27
+
28
+ def __len__(self) -> int:
29
+ return len(self._api_clients)
@@ -0,0 +1,11 @@
1
+ __all__ = [
2
+ 'AsyncAbstractGQLClient',
3
+ 'GQLClientConfig',
4
+ 'AsyncKeycloakAwareGQLClient',
5
+ 'AsyncNoAuthGQLClient'
6
+ ]
7
+
8
+ from ._abstract import AsyncAbstractGQLClient
9
+ from ._config import GQLClientConfig
10
+ from ._keycloak import AsyncKeycloakAwareGQLClient
11
+ from ._noauth import AsyncNoAuthGQLClient
@@ -0,0 +1,90 @@
1
+ import logging
2
+ from abc import ABCMeta
3
+ from asyncio import Semaphore
4
+ from contextlib import AbstractAsyncContextManager
5
+ from typing import Callable, Optional
6
+
7
+ import backoff
8
+ from aiorwlock import RWLock
9
+ from gql import Client
10
+ from gql.client import AsyncClientSession
11
+ from gql.transport.aiohttp import AIOHTTPTransport, log as aiohttp_logger
12
+ from gql.transport.exceptions import TransportQueryError
13
+ from gql.transport.requests import log as requests_logger
14
+ from graphql import DocumentNode
15
+
16
+ from tp_interfaces.logging.time import AsyncTimeMeasurer
17
+
18
+ requests_logger.setLevel(logging.WARNING)
19
+ aiohttp_logger.setLevel(logging.ERROR)
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class AsyncAbstractGQLClient(AbstractAsyncContextManager, metaclass=ABCMeta):
25
+
26
+ def __init__(self, gql_uri: str, timeout: int = 60, concurrency_limit: int = 10, retry_execute: bool | Callable = True):
27
+ self._gql_uri = gql_uri
28
+ self._timeout = timeout
29
+ self._retry_execute = retry_execute
30
+
31
+ self._client: Optional[Client] = None
32
+ self._session: Optional[AsyncClientSession] = None
33
+
34
+ self._sema = Semaphore(concurrency_limit)
35
+ self._rw_lock = RWLock()
36
+
37
+ def validate(self, document: DocumentNode) -> bool:
38
+ try:
39
+ self._client.validate(document)
40
+ return True
41
+ except Exception:
42
+ return False
43
+
44
+ async def execute(self, document: DocumentNode, variables=None, operation_name=None, extra_headers=None, timeout=None):
45
+ async with self._sema, self._rw_lock.reader_lock:
46
+ async with AsyncTimeMeasurer(
47
+ f"query {operation_name}", inline_time=True, logger=logger, warning_threshold=5000,
48
+ extra={"operation_name": operation_name, "variables": variables}
49
+ ):
50
+ return await self._session.execute(document, variables, operation_name)
51
+
52
+ async def _configure_session(self, headers: dict = None):
53
+ async with self._rw_lock.writer_lock:
54
+ await self._close_session()
55
+
56
+ transport = AIOHTTPTransport(url=self._gql_uri, headers=headers)
57
+ self._client = Client(transport=transport, fetch_schema_from_transport=True, execute_timeout=self._timeout)
58
+
59
+ # here we could change default behaviour of query retrying: just change retry_execute to backoff decorator
60
+ self._session = await self._client.connect_async(reconnecting=True, retry_execute=self._retry_execute)
61
+
62
+ async def _close_session(self):
63
+ if self._session is not None:
64
+ await self._client.close_async()
65
+ self._session = None
66
+ self._client = None
67
+
68
+
69
+ def build_retry_execute(config: bool | dict = True) -> bool | Callable:
70
+ if isinstance(config, bool):
71
+ return config
72
+ if isinstance(config, dict):
73
+ if not config:
74
+ config = {
75
+ 'wait_gen': 'expo',
76
+ 'wait_gen_args': {
77
+ 'base': 2.71828,
78
+ 'factor': 2
79
+ }
80
+ }
81
+ wait_gen = getattr(backoff, config.get('wait_gen', 'expo'))
82
+ max_tries = config.get('max_tries', 5)
83
+ return backoff.on_exception(
84
+ wait_gen,
85
+ Exception,
86
+ max_tries=max_tries,
87
+ giveup=lambda e: isinstance(e, TransportQueryError),
88
+ **config.get('wait_gen_args', {})
89
+ )
90
+ raise ValueError
@@ -0,0 +1,19 @@
1
+ from typing import NamedTuple
2
+
3
+ from ._abstract import AsyncAbstractGQLClient, build_retry_execute
4
+ from ._keycloak import AsyncKeycloakAwareGQLClient
5
+ from ._noauth import AsyncNoAuthGQLClient
6
+
7
+
8
+ class GQLClientConfig(NamedTuple):
9
+ uri: str
10
+ auth: bool = False
11
+ timeout: int = 60
12
+ concurrency_limit: int = 30
13
+ retry_execute: bool | dict = True
14
+
15
+ def configure(self) -> AsyncAbstractGQLClient:
16
+ retry_execute = build_retry_execute(self.retry_execute)
17
+ if self.auth:
18
+ return AsyncKeycloakAwareGQLClient(self.uri, self.timeout, self.concurrency_limit, retry_execute)
19
+ return AsyncNoAuthGQLClient(self.uri, self.timeout, self.concurrency_limit, retry_execute)
@@ -0,0 +1,90 @@
1
+ import enum
2
+ import logging
3
+ import os
4
+ import time
5
+ from asyncio import Lock
6
+ from typing import Callable, Optional
7
+
8
+ from keycloak import KeycloakOpenID
9
+ from urllib3.util.retry import log
10
+
11
+ from tp_interfaces.logging.time import TimeMeasurer
12
+ from ._abstract import AsyncAbstractGQLClient
13
+
14
+ logger = logging.getLogger(__name__)
15
+ log.setLevel(logging.INFO)
16
+
17
+
18
+ class AuthEnvs(enum.Enum):
19
+ AUTH_URL = "KEYCLOAK_AUTH_URL"
20
+ REALM = "KEYCLOAK_REALM"
21
+ CLIENT_ID = "KEYCLOAK_CLIENT_ID"
22
+ CLIENT_KEY = "KEYCLOAK_CLIENT_KEY"
23
+ USER = "KEYCLOAK_USER"
24
+ PWD = "KEYCLOAK_PWD"
25
+
26
+ @property
27
+ def env(self) -> str:
28
+ return os.getenv(self.value)
29
+
30
+
31
+ class AsyncKeycloakAwareGQLClient(AsyncAbstractGQLClient):
32
+ _TIME_OFFSET = 10 # in seconds
33
+
34
+ def __init__(self, gql_uri: str, timeout: int = 60, concurrency_limit: int = 10, retry_execute: bool | Callable = True):
35
+ super().__init__(gql_uri, timeout, concurrency_limit, retry_execute)
36
+
37
+ self._auth: dict[AuthEnvs, str] = {env: env.env for env in AuthEnvs}
38
+
39
+ not_set_envs = {env for env, val in self._auth.items() if val is None}
40
+ if not_set_envs:
41
+ raise ValueError(f"Authorization environment values are not set: {not_set_envs}")
42
+
43
+ self._keycloak_openid: Optional[KeycloakOpenID] = None
44
+ self._access_token: Optional[str] = None
45
+ self._refresh_token: Optional[str] = None
46
+ self._access_expiration_timestamp: float = 0
47
+ self._refresh_expiration_timestamp: float = 0
48
+
49
+ self._lock = Lock()
50
+
51
+ async def _ensure_session_liveness(self):
52
+ offsetted_time = time.time() + self._TIME_OFFSET
53
+ if offsetted_time < self._access_expiration_timestamp:
54
+ return
55
+
56
+ time_before_req = time.time()
57
+ if offsetted_time < self._refresh_expiration_timestamp and self._refresh_token is not None:
58
+ with TimeMeasurer("refreshing access token with refresh token", logger=logger):
59
+ token_info = self._keycloak_openid.refresh_token(self._refresh_token)
60
+ else:
61
+ with TimeMeasurer("refreshing access token with credentials", logger=logger):
62
+ token_info = self._keycloak_openid.token(self._auth[AuthEnvs.USER], self._auth[AuthEnvs.PWD])
63
+
64
+ self._access_token = token_info['access_token']
65
+ self._access_expiration_timestamp = time_before_req + token_info['expires_in']
66
+ self._refresh_token = token_info['refresh_token']
67
+ self._refresh_expiration_timestamp = time_before_req + token_info['refresh_expires_in']
68
+
69
+ headers = {"X-Auth-Token": self._access_token, "Authorization": f"Bearer {self._access_token}"}
70
+ await self._configure_session(headers)
71
+
72
+ async def __aenter__(self):
73
+ self._keycloak_openid = KeycloakOpenID(
74
+ self._auth[AuthEnvs.AUTH_URL], self._auth[AuthEnvs.REALM], self._auth[AuthEnvs.CLIENT_ID], self._auth[AuthEnvs.CLIENT_KEY]
75
+ )
76
+ return self
77
+
78
+ async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
79
+ async with self._lock:
80
+ async with self._rw_lock.writer_lock:
81
+ await self._close_session()
82
+
83
+ self._access_token, self._refresh_token = None, None
84
+ self._access_expiration_timestamp, self._refresh_expiration_timestamp = 0, 0
85
+ self._keycloak_openid = None
86
+
87
+ async def execute(self, document, variables=None, operation_name=None, extra_headers=None, timeout=None):
88
+ async with self._lock: # lock session configuration to avoid concurrent keycloack requests
89
+ await self._ensure_session_liveness()
90
+ return await super().execute(document, variables, operation_name, extra_headers, timeout)
@@ -0,0 +1,12 @@
1
+ from ._abstract import AsyncAbstractGQLClient
2
+
3
+
4
+ class AsyncNoAuthGQLClient(AsyncAbstractGQLClient):
5
+
6
+ async def __aenter__(self):
7
+ await self._configure_session()
8
+ return self
9
+
10
+ async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
11
+ async with self._rw_lock.writer_lock:
12
+ await self._close_session()
@@ -0,0 +1,9 @@
1
+ from .abstract import AbstractTalismanAPI
2
+
3
+
4
+ def version(v: str):
5
+ def decorate(cls: type[AbstractTalismanAPI]):
6
+ cls._VERSION = v
7
+ return cls
8
+
9
+ return decorate
@@ -0,0 +1,54 @@
1
+ from contextlib import AbstractAsyncContextManager
2
+ from typing import AsyncIterator, Callable
3
+
4
+ from graphql import DocumentNode
5
+
6
+ from .api_client import TalismanAPIClient as BaseClient
7
+
8
+
9
+ class TalismanAPIClient(AbstractAsyncContextManager):
10
+ def __init__(self, client: BaseClient, document: DocumentNode):
11
+ self._client = client
12
+ self._document = document
13
+
14
+ async def __aenter__(self):
15
+ await self._client.__aenter__()
16
+ return self
17
+
18
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
19
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
20
+
21
+ async def execute(self, operation_name: str, variables: dict | None = None, raise_on_timeout: bool = True):
22
+ return await self._client.execute(self._document, operation_name, variables, raise_on_timeout)
23
+
24
+ async def paginate_items(
25
+ self,
26
+ operation_name: str,
27
+ variables: dict = None,
28
+ *,
29
+ page_size: int = 20,
30
+ extract_page: Callable[[dict], dict] = lambda d: d
31
+ ) -> AsyncIterator[dict]:
32
+ async for i in self._client.paginate_items(
33
+ self._document,
34
+ operation_name,
35
+ variables,
36
+ page_size=page_size,
37
+ extract_page=extract_page
38
+ ):
39
+ yield i
40
+
41
+ async def get_all_items(
42
+ self,
43
+ operation_name: str,
44
+ variables: dict = None,
45
+ *,
46
+ extract_page: Callable[[dict], dict] = lambda d: d
47
+ ):
48
+ async for i in self._client.get_all_items(
49
+ self._document,
50
+ operation_name,
51
+ variables,
52
+ extract_page=extract_page
53
+ ):
54
+ yield i
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: talisman-api
3
+ Version: 1.0.0
4
+ Summary: Python Talisman API client for Talisman-based app
5
+ Author: ISPRAS Talisman NLP team
6
+ Author-email: modis@ispras.ru
7
+ Maintainer: Vladimir Mayorov
8
+ Maintainer-email: vmayorov@ispras.ru
9
+ License: Apache Software License
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: talisman-interfaces (~=0.7)
19
+ Requires-Dist: aiorwlock (~=1.3)
20
+ Requires-Dist: gql[aiohttp] (~=3.4)
21
+ Requires-Dist: requests (~=2.31)
22
+ Requires-Dist: python-keycloak (~=2.16)
23
+ Requires-Dist: typing-extensions (>=4.0.0)
24
+
@@ -0,0 +1,17 @@
1
+ talisman_api/__init__.py,sha256=Xyz4wn-nSCuWn1ekRY61ym8-PhXglvqX4o9eveUOefc,302
2
+ talisman_api/abstract.py,sha256=IUUbYOIGgRYafHJqct3JPwhknsBTynrC9LbKCgACKjo,3753
3
+ talisman_api/decorator.py,sha256=otwEnX1AhPUvTblyr47Up1Xf1aHA_jQmGpzhsMXZygc,180
4
+ talisman_api/wrapper.py,sha256=q9PMYJ6R9xOZOXGB2aOs4O0IxHCJH7SzYmI9yX1LAHg,1734
5
+ talisman_api/api_client/__init__.py,sha256=3kLisxkvPOKBVhw9qwTZQmVjeJDmlO9IqgoaNrwRyxc,249
6
+ talisman_api/api_client/client.py,sha256=4F83CU63T0lbN4td_yiaXok2UgDihGMxdf5EzapsC6g,4172
7
+ talisman_api/api_client/composite.py,sha256=iKCGZP7gg2zP1UcNJYtm7FHblYloq9uqyFJzoTks3Y8,882
8
+ talisman_api/api_client/gql_clients/__init__.py,sha256=3wGvo38C862_iOHQwu80Bl6c-OTIXDOBmhrVx9DAP08,306
9
+ talisman_api/api_client/gql_clients/_abstract.py,sha256=OWctaZ3S6wcDIpc_gCeMOCFmVykFBCYAVGl43aUhZPw,3389
10
+ talisman_api/api_client/gql_clients/_config.py,sha256=qnrB_yTwdPZqGCg2Bzfe5SkbOnQW72h8ddItjxmPvNs,703
11
+ talisman_api/api_client/gql_clients/_keycloak.py,sha256=SruxIm_ripB8GVc_VWpPIprNyW7efxZMjvhwA-mksfA,3690
12
+ talisman_api/api_client/gql_clients/_noauth.py,sha256=V9GDkMJg3NH5dZG3InWlpxv8sa1Eha220RYBHad_qao,353
13
+ talisman_api-1.0.0.data/data/VERSION,sha256=klIfw8vZZL3J9YSpkbif3apXVO0cyW1tQkRTOGacEwU,5
14
+ talisman_api-1.0.0.dist-info/METADATA,sha256=t1D--9EojEPZeqTxh2fabjCS1hBud2EeJjI7i1OEuUo,867
15
+ talisman_api-1.0.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
16
+ talisman_api-1.0.0.dist-info/top_level.txt,sha256=9i4PbX8BUXG5HjLUv5hrPQg1vgyYBxhD4Uuf8jTVdw8,13
17
+ talisman_api-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.40.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ talisman_api