PK!aiodynamo/__init__.pyPK!tH)EEaiodynamo/client.pyfrom functools import partial from itertools import chain from typing import List, Dict, Any, TypeVar, Union, AsyncIterator import attr from boto3.dynamodb.conditions import ConditionBase, ConditionExpressionBuilder from botocore.exceptions import ClientError from .errors import ItemNotFound, TableNotFound, EmptyItem from .models import ( Throughput, KeyType, KeySpec, KeySchema, LocalSecondaryIndex, GlobalSecondaryIndex, StreamSpecification, ReturnValues, UpdateExpression, TableStatus, TableDescription, Select, get_projection, ProjectionExpr, ) from .types import Item, TableName from .utils import unroll, py2dy, dy2py, clean _Key = TypeVar("_Key") _Val = TypeVar("_Val") @attr.s class Table: client: "Client" = attr.ib() name: TableName = attr.ib() async def exists(self) -> bool: return await self.client.table_exists(self.name) async def create( self, throughput: Throughput, keys: KeySchema, *, lsis: List[LocalSecondaryIndex] = None, gsis: List[GlobalSecondaryIndex] = None, stream: StreamSpecification = None ): return await self.client.create_table( self.name, throughput, keys, lsis=lsis, gsis=gsis, stream=stream ) async def describe(self) -> TableDescription: return await self.client.describe_table(self.name) async def delete(self): return await self.client.delete_table(self.name) async def delete_item( self, key: Dict[str, Any], *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[None, Item]: return await self.client.delete_item( self.name, key, return_values=return_values, condition=condition ) async def get_item( self, key: Dict[str, Any], *, projection: ProjectionExpr = None ) -> Item: return await self.client.get_item(self.name, key, projection=projection) async def put_item( self, item: Dict[str, Any], *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[None, Item]: return await self.client.put_item( self.name, item, return_values=return_values, condition=condition ) def query( self, key_condition: ConditionBase, *, start_key: Dict[str, Any] = None, filter_expression: ConditionBase = None, scan_forward: bool = True, index: str = None, limit: int = None, projection: ProjectionExpr = None, select: Select = Select.all_attributes ) -> AsyncIterator[Item]: return self.client.query( self.name, key_condition, start_key=start_key, filter_expression=filter_expression, scan_forward=scan_forward, index=index, limit=limit, projection=projection, select=select, ) def scan( self, *, index: str = None, limit: int = None, start_key: Dict[str, Any] = None, projection: ProjectionExpr = None, filter_expression: ConditionBase = None ) -> AsyncIterator[Item]: return self.client.scan( self.name, index=index, limit=limit, start_key=start_key, projection=projection, filter_expression=filter_expression, ) async def count( self, key_condition: ConditionBase, *, start_key: Dict[str, Any] = None, filter_expression: ConditionBase = None, index: str = None ) -> int: return await self.client.count( self.name, key_condition, start_key=start_key, filter_expression=filter_expression, index=index, ) async def update_item( self, key: Item, update_expression: UpdateExpression, *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[Item, None]: return await self.client.update_item( self.name, key, update_expression, return_values=return_values, condition=condition, ) @attr.s class Client: # core is an aiobotocore DynamoDB client, use aiobotocore.get_session().create_client("dynamodb") to create one. core = attr.ib() def table(self, name: TableName) -> Table: return Table(self, name) async def table_exists(self, name: TableName) -> bool: try: description = await self.describe_table(name) except TableNotFound: return False return description.status is TableStatus.active async def create_table( self, name: TableName, throughput: Throughput, keys: KeySchema, *, lsis: List[LocalSecondaryIndex] = None, gsis: List[GlobalSecondaryIndex] = None, stream: StreamSpecification = None ): lsis: List[LocalSecondaryIndex] = lsis or [] gsis: List[GlobalSecondaryIndex] = gsis or [] stream = stream or StreamSpecification() attributes = {} attributes.update(keys.to_attributes()) for index in chain(lsis, gsis): attributes.update(index.schema.to_attributes()) attribute_definitions = [ {"AttributeName": key, "AttributeType": value} for key, value in attributes.items() ] key_schema = keys.encode() local_secondary_indexes = [index.encode() for index in lsis] global_secondary_indexes = [index.encode() for index in gsis] provisioned_throughput = throughput.encode() stream_specification = stream.encode() await self.core.create_table( **clean( AttributeDefinitions=attribute_definitions, TableName=name, KeySchema=key_schema, LocalSecondaryIndexes=local_secondary_indexes, GlobalSecondaryIndexes=global_secondary_indexes, ProvisionedThroughput=provisioned_throughput, StreamSpecification=stream_specification, ) ) return None async def describe_table(self, name: TableName): try: response = await self.core.describe_table(TableName=name) except ClientError as exc: try: if exc.response["Error"]["Code"] == "ResourceNotFoundException": raise TableNotFound(name) except KeyError: pass raise exc description = response["Table"] attributes: Dict[str, KeyType] = { attribute["AttributeName"]: KeyType(attribute["AttributeType"]) for attribute in description["AttributeDefinitions"] } return TableDescription( attributes=attributes, created=description["CreationDateTime"], item_count=description["ItemCount"], key_schema=KeySchema( *[ KeySpec( name=key["AttributeName"], type=attributes[key["AttributeName"]] ) for key in description["KeySchema"] ] ), throughput=Throughput( read=description["ProvisionedThroughput"]["ReadCapacityUnits"], write=description["ProvisionedThroughput"]["WriteCapacityUnits"], ), status=TableStatus(description["TableStatus"]), ) async def delete_item( self, table: str, key: Dict[str, Any], *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[None, Item]: key = py2dy(key) if not key: raise EmptyItem() if condition: condition_expression, expression_attribute_names, expression_attribute_values = ConditionExpressionBuilder().build_expression( condition ) else: condition_expression = ( expression_attribute_names ) = expression_attribute_values = None resp = await self.core.delete_item( **clean( TableName=table, Key=key, ReturnValues=return_values.value, ConditionExpression=condition_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttribuetValues=expression_attribute_values, ) ) if "Attributes" in resp: return dy2py(resp["Attributes"]) else: return None async def delete_table(self, table: TableName): await self.core.delete_table(TableName=table) async def get_item( self, table: TableName, key: Dict[str, Any], *, projection: ProjectionExpr = None ) -> Item: projection_expression, expression_attribute_names = get_projection(projection) key = py2dy(key) if not key: raise EmptyItem() resp = await self.core.get_item( **clean( TableName=table, Key=key, ProjectionExpression=projection_expression, ExpressionAttributeNames=expression_attribute_names, ) ) if "Item" in resp: return dy2py(resp["Item"]) else: raise ItemNotFound(key) async def put_item( self, table: TableName, item: Dict[str, Any], *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[None, Item]: if condition: condition_expression, expression_attribute_names, expression_attribute_values = ConditionExpressionBuilder().build_expression( condition ) else: condition_expression = ( expression_attribute_names ) = expression_attribute_values = None item = py2dy(item) if not item: raise EmptyItem() resp = await self.core.put_item( **clean( TableName=table, Item=item, ReturnValues=return_values.value, ConditionExpression=condition_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=py2dy(expression_attribute_values), ) ) if "Attributes" in resp: return dy2py(resp["Attributes"]) else: return None async def query( self, table: TableName, key_condition: ConditionBase, *, start_key: Dict[str, Any] = None, filter_expression: ConditionBase = None, scan_forward: bool = True, index: str = None, limit: int = None, projection: ProjectionExpr = None, select: Select = Select.all_attributes ) -> AsyncIterator[Item]: if projection: select = Select.specific_attributes if select is Select.count: raise TypeError("Cannot use Select.count with query, use count instead") expression_attribute_names = {} expression_attribute_values = {} projection_expression, ean = get_projection(projection) expression_attribute_names.update(ean) builder = ConditionExpressionBuilder() if filter_expression: filter_expression, ean, eav = builder.build_expression(filter_expression) expression_attribute_names.update(ean) expression_attribute_values.update(eav) if key_condition: key_condition_expression, ean, eav = builder.build_expression( key_condition, True ) expression_attribute_names.update(ean) expression_attribute_values.update(eav) else: key_condition_expression = None coro_func = partial( self.core.query, **clean( TableName=table, IndexName=index, ScanIndexForward=scan_forward, ProjectionExpression=projection_expression, FilterExpression=filter_expression, KeyConditionExpression=key_condition_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=py2dy(expression_attribute_values), Select=select.value, ) ) async for raw in unroll( coro_func, "ExclusiveStartKey", "LastEvaluatedKey", "Items", py2dy(start_key) if start_key else None, limit, "Limit", ): yield dy2py(raw) async def scan( self, table: TableName, *, index: str = None, limit: int = None, start_key: Dict[str, Any] = None, projection: ProjectionExpr = None, filter_expression: ConditionBase = None ) -> AsyncIterator[Item]: expression_attribute_names = {} expression_attribute_values = {} projection_expression, ean = get_projection(projection) expression_attribute_names.update(ean) builder = ConditionExpressionBuilder() if filter_expression: filter_expression, ean, eav = builder.build_expression(filter_expression) expression_attribute_names.update(ean) expression_attribute_values.update(eav) coro_func = partial( self.core.scan, **clean( TableName=table, IndexName=index, ProjectionExpression=projection_expression, FilterExpression=filter_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=py2dy(expression_attribute_values), ) ) async for raw in unroll( coro_func, "ExclusiveStartKey", "LastEvaluatedKey", "Items", py2dy(start_key) if start_key else None, limit, "Limit", ): yield dy2py(raw) async def count( self, table: TableName, key_condition: ConditionBase, *, start_key: Dict[str, Any] = None, filter_expression: ConditionBase = None, index: str = None ) -> int: expression_attribute_names = {} expression_attribute_values = {} builder = ConditionExpressionBuilder() if filter_expression: filter_expression, ean, eav = builder.build_expression(filter_expression) expression_attribute_names.update(ean) expression_attribute_values.update(eav) if key_condition: key_condition_expression, ean, eav = builder.build_expression( key_condition, True ) expression_attribute_names.update(ean) expression_attribute_values.update(eav) else: key_condition_expression = None coro_func = partial( self.core.query, **clean( TableName=table, IndexName=index, FilterExpression=filter_expression, KeyConditionExpression=key_condition_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=py2dy(expression_attribute_values), Select=Select.count.value, ) ) count_sum = 0 async for count in unroll( coro_func, "ExclusiveStartKey", "LastEvaluatedKey", "Count", py2dy(start_key) if start_key else None, process=lambda x: [x], ): count_sum += count return count_sum async def update_item( self, table: TableName, key: Item, update_expression: UpdateExpression, *, return_values: ReturnValues = ReturnValues.none, condition: ConditionBase = None ) -> Union[Item, None]: update_expression, expression_attribute_names, expression_attribute_values = ( update_expression.encode() ) if not update_expression: raise EmptyItem() builder = ConditionExpressionBuilder() if condition: condition_expression, ean, eav = builder.build_expression(condition) expression_attribute_names.update(ean) expression_attribute_values.update(eav) else: condition_expression = None resp = await self.core.update_item( **clean( TableName=table, Key=py2dy(key), UpdateExpression=update_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=py2dy(expression_attribute_values), ConditionExpression=condition_expression, ReturnValues=return_values.value, ) ) if "Attributes" in resp: return dy2py(resp["Attributes"]) else: return None PK!zzaiodynamo/errors.pyclass ItemNotFound(Exception): pass class TableNotFound(Exception): pass class EmptyItem(Exception): pass PK![kO%O%aiodynamo/models.pyimport abc import datetime from collections import defaultdict from enum import Enum from typing import Dict, List, Any, Set, Tuple, Union import attr from .types import Path, PathEncoder, EncoderFunc, NOTHING, EMPTY from .utils import clean, ensure_not_empty, check_empty_value, maybe_immutable ProjectionExpr = Union["ProjectionExpression", "F"] @attr.s class Throughput: read: int = attr.ib() write: int = attr.ib() def encode(self): return {"ReadCapacityUnits": self.read, "WriteCapacityUnits": self.write} class KeyType(Enum): string = "S" number = "N" binary = "B" @attr.s class KeySpec: name: str = attr.ib() type: KeyType = attr.ib() @attr.s class KeySchema: hash_key: KeySpec = attr.ib() range_key: KeySpec = attr.ib(default=None) def __iter__(self): yield self.hash_key if self.range_key: yield self.range_key def to_attributes(self) -> Dict[str, str]: return {key.name: key.type.value for key in self} def encode(self) -> List[Dict[str, str]]: return [ {"AttributeName": key.name, "KeyType": key_type} for key, key_type in zip(self, ["HASH", "RANGE"]) ] class ProjectionType(Enum): all = "ALL" keys_only = "KEYS_ONLY" include = "INCLUDE" @attr.s class Projection: type: ProjectionType = attr.ib() attrs: List[str] = attr.ib(default=None) def encode(self): encoded = {"ProjectionType": self.type.value} if self.attrs: encoded["NonKeyAttributes"] = self.attrs return encoded @attr.s class LocalSecondaryIndex: name: str = attr.ib() schema: KeySchema = attr.ib() projection: Projection = attr.ib() def encode(self): return { "IndexName": self.name, "KeySchema": self.schema.encode(), "Projection": self.projection.encode(), } @attr.s class GlobalSecondaryIndex(LocalSecondaryIndex): throughput: Throughput = attr.ib() def encode(self): return {**super().encode(), "ProvisionedThroughput": self.throughput.encode()} class StreamViewType(Enum): keys_only = "KEYS_ONLY" new_image = "NEW_IMAGE" old_image = "OLD_IMAGE" new_and_old_images = "NEW_AND_OLD_IMAGES" @attr.s class StreamSpecification: enabled: bool = attr.ib(default=False) view_type: StreamViewType = attr.ib(default=StreamViewType.new_and_old_images) def encode(self): return clean( StreamEnabled=self.enabled, StreamViewType=self.view_type.value if self.enabled else None, ) class ReturnValues(Enum): none = "NONE" all_old = "ALL_OLD" updated_old = "UPDATED_OLD" all_new = "ALL_NEW" updated_new = "UPDATED_NEW" class ActionTypes(Enum): set = "SET" remove = "REMOVE" add = "ADD" delete = "DELETE" class BaseAction(metaclass=abc.ABCMeta): type = abc.abstractproperty() def encode(self, name_encoder: "Encoder", value_encoder: "Encoder") -> str: return self._encode(name_encoder.encode_path, value_encoder.encode) @abc.abstractmethod def _encode(self, N: PathEncoder, V: EncoderFunc) -> str: ... @attr.s class SetAction(BaseAction): path: Path = attr.ib() value: Any = attr.ib(converter=ensure_not_empty) ine: "F" = attr.ib(default=NOTHING) type = ActionTypes.set @check_empty_value def _encode(self, N: PathEncoder, V: EncoderFunc) -> str: if self.ine is not NOTHING: return f"{N(self.path)} = if_not_exists({N(self.ine.path)}, {V(self.value)}" else: return f"{N(self.path)} = {V(self.value)}" def if_not_exists(self, key: "F") -> "SetAction": return attr.evolve(self, ine=key) @attr.s class ChangeAction(BaseAction): path: Path = attr.ib() value: Any = attr.ib(converter=ensure_not_empty) type = ActionTypes.set @check_empty_value def _encode(self, N: PathEncoder, V: EncoderFunc) -> str: if self.value > 0: op = "+" value = self.value else: value = self.value * -1 op = "-" return f"{N(self.path)} = {N(self.path)} {op} {V(value)}" @attr.s class AppendAction(BaseAction): path: Path = attr.ib() value: Any = attr.ib(converter=ensure_not_empty) type = ActionTypes.set @check_empty_value def _encode(self, N: PathEncoder, V: EncoderFunc) -> str: return f"{N(self.path)} = list_append({N(self.path)}, {V(self.value)})" @attr.s class RemoveAction(BaseAction): path: Path = attr.ib() type = ActionTypes.remove def _encode(self, N, V) -> str: return N(self.path) @attr.s class DeleteAction(BaseAction): path: Path = attr.ib() value: Any = attr.ib(converter=ensure_not_empty) type = ActionTypes.delete @check_empty_value def _encode(self, N: PathEncoder, V: EncoderFunc) -> str: return f"{N(self.path)} {V(self.value)}" @attr.s class AddAction(BaseAction): path: Path = attr.ib() value: Any = attr.ib(converter=ensure_not_empty) type = ActionTypes.add @check_empty_value def _encode(self, N: PathEncoder, V: EncoderFunc): return f"{N(self.path)} {V(self.value)}" class F: def __init__(self, *path): self.path: Path = path def __and__(self, other: "F") -> "ProjectionExpression": pe = ProjectionExpression() return pe & self & other def encode(self, encoder: "Encoder") -> str: return encoder.encode_path(self.path) def set(self, value: Any) -> "UpdateExpression": return UpdateExpression(SetAction(self.path, value)) def change(self, diff: int) -> "UpdateExpression": return UpdateExpression(ChangeAction(self.path, diff)) def append(self, value: List[Any]) -> "UpdateExpression": return UpdateExpression(AppendAction(self.path, list(value))) def remove(self) -> "UpdateExpression": return UpdateExpression(RemoveAction(self.path)) def add(self, value: Set[Any]) -> "UpdateExpression": return UpdateExpression(AddAction(self.path, value)) def delete(self, value: Set[Any]) -> "UpdateExpression": return UpdateExpression(DeleteAction(self.path, value)) class UpdateExpression: def __init__(self, *updates: BaseAction): self.updates = updates def __and__(self, other: "UpdateExpression") -> "UpdateExpression": return UpdateExpression(*self.updates, *other.updates) def __bool__(self): return bool(self.updates) def encode(self) -> Tuple[str, Dict[str, Any], Dict[str, Any]]: name_encoder = Encoder("#N") value_encoder = Encoder(":V") parts = defaultdict(list) for action in self.updates: value = action.encode(name_encoder, value_encoder) if value is not EMPTY: parts[action.type].append(value) part_list = [ f'{action.value} {", ".join(values)}' for action, values in parts.items() ] return " ".join(part_list), name_encoder.finalize(), value_encoder.finalize() @attr.s class ProjectionExpression: fields: List[F] = attr.ib(default=attr.Factory(list)) def __and__(self, field: F) -> "ProjectionExpression": return ProjectionExpression(self.fields + [field]) def encode(self) -> Tuple[str, Dict[str, Any]]: name_encoder = Encoder("#N") return ( ",".join(field.encode(name_encoder) for field in self.fields), name_encoder.finalize(), ) class TableStatus(Enum): creating = "CREATING" updating = "UPDATING" deleting = "DELETING" active = "ACTIVE" @attr.s class TableDescription: attributes: Dict[str, KeyType] = attr.ib() created: datetime.datetime = attr.ib() item_count: int = attr.ib() key_schema: KeySchema = attr.ib() throughput: Throughput = attr.ib() status: TableStatus = attr.ib() @attr.s class Encoder: prefix: str = attr.ib() data: List[Any] = attr.ib(default=attr.Factory(list)) cache: Dict[Any, Any] = attr.ib(default=attr.Factory(dict)) def finalize(self) -> Dict[str, str]: return {f"{self.prefix}{index}": value for index, value in enumerate(self.data)} def encode(self, name: Any) -> str: key = maybe_immutable(name) try: return self.cache[key] except KeyError: can_cache = True except TypeError: can_cache = False encoded = f"{self.prefix}{len(self.data)}" self.data.append(name) if can_cache: self.cache[key] = encoded return encoded def encode_path(self, path: Path) -> str: bits = [self.encode(path[0])] for part in path[1:]: if isinstance(part, int): bits.append(f"[{part}]") else: bits.append(f".{self.encode(part)}") return "".join(bits) class Select(Enum): all_attributes = "ALL_ATTRIBUTES" all_projected_attributes = "ALL_PROJECTED_ATTRIBUTES" count = "COUNT" specific_attributes = "SPECIFIC_ATTRIBUTES" def get_projection( projection: Union[ProjectionExpression, F, None] ) -> Tuple[Union[str, None], Dict[str, Any]]: if projection is None: return None, {} if isinstance(projection, ProjectionExpression): return projection.encode() else: encoder = Encoder("#N") return projection.encode(encoder), encoder.finalize() PK!JZZaiodynamo/types.pyfrom typing import TypeVar, Dict, Any, List, Union, Callable from boto3.dynamodb.types import TypeDeserializer, TypeSerializer Item = TypeVar("Item", bound=Dict[str, Any]) DynamoItem = TypeVar("DynamoItem", bound=Dict[str, Dict[str, Any]]) TableName = TypeVar("TableName", bound=str) Path = List[Union[str, int]] PathEncoder = Callable[[Path], str] EncoderFunc = Callable[[Any], str] NOTHING = object() EMPTY = object() class BinaryTypeDeserializer(TypeDeserializer): def _deserialize_b(self, value): return value Serializer = TypeSerializer() Deserializer = BinaryTypeDeserializer() PK!(|K@ aiodynamo/utils.pyimport collections from functools import wraps from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterable, Tuple, Union from .types import Deserializer, DynamoItem, EMPTY, Item, Serializer async def unroll( coro_func: Callable[[], Awaitable[Dict[str, Any]]], inkey: str, outkey: str, itemkey: str, start: Any = None, limit: int = None, limitkey: str = None, process: Callable[[Any], Iterable[Any]] = lambda x: x, ) -> AsyncIterator[Any]: value = start got = 0 while True: kwargs = {} if value is not None: kwargs[inkey] = value if limit: want = limit - got kwargs[limitkey] = want resp = await coro_func(**kwargs) value = resp.get(outkey, None) items = resp.get(itemkey, []) for item in process(items): yield item got += 1 if limit and got >= limit: return if value is None: break def ensure_not_empty(value): if value is None: return value elif isinstance(value, (bytes, str)): if not value: return EMPTY elif isinstance(value, collections.abc.Mapping): value = dict(remove_empty_strings(value)) elif isinstance(value, collections.abc.Iterable): value = value.__class__( item for item in map(ensure_not_empty, value) if item is not EMPTY ) return value def remove_empty_strings(data: Item) -> Iterable[Tuple[str, Any]]: for key, value in data.items(): value = ensure_not_empty(value) if value is not EMPTY: yield key, value def py2dy(data: Union[Item, None]) -> Union[DynamoItem, None]: if data is None: return data return { key: Serializer.serialize(value) for key, value in remove_empty_strings(data) } def dy2py(data: DynamoItem) -> Item: return {key: Deserializer.deserialize(value) for key, value in data.items()} def check_empty_value(meth): @wraps(meth) def wrapper(self, *args, **kwargs): if self.value is EMPTY: return self.value return meth(self, *args, **kwargs) return wrapper def clean(**kwargs): return { key: value for key, value in kwargs.items() if value or isinstance(value, bool) } def maybe_immutable(thing: Any): if isinstance(thing, list): return tuple(thing) elif isinstance(thing, set): return frozenset(thing) else: return thing PK!@(( aiodynamo-19.3.dist-info/LICENSECopyright 2019 HENNGE K.K. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK!Hu)GTUaiodynamo-19.3.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H?t!aiodynamo-19.3.dist-info/METADATA_O0) ( Uil;ɾDy[v~, `LkrЛqHm:)O") [`q]v8O]~M(uL#7Q DSeTYXRe #K{ t%ŖnO/3J ǔxIc§[Rmmav0jQJi5%2˪dDfVN.啺эiG2MXR4+nYԟjW{i e(88-\!4&>Ɍ MjKvb# qi`ؽ)S.ĪRu;+nXzoy)xB֞x{ԣ_PK!aiodynamo/__init__.pyPK!tH)EE3aiodynamo/client.pyPK!zzEaiodynamo/errors.pyPK![kO%O%,Faiodynamo/models.pyPK!JZZkaiodynamo/types.pyPK!(|K@ 6naiodynamo/utils.pyPK!@(( Wxaiodynamo-19.3.dist-info/LICENSEPK!Hu)GTUzaiodynamo-19.3.dist-info/WHEELPK!H?t!M{aiodynamo-19.3.dist-info/METADATAPK!HxS}aiodynamo-19.3.dist-info/RECORDPK