diff --git a/LICENCE.txt b/LICENCE.txt index 8aa2645..a60fd36 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2023 vaphes Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4599b45..2f4afdc 100644 --- a/README.md +++ b/README.md @@ -21,26 +21,32 @@ $ pip install pocketbase The rule of thumb here is just to use it as you would the javascript lib, but in a pythonic way of course! ```python -from pocketbase import PocketBase # Client also works the same +from pocketbase import PocketBase # Client also works the same +from pocketbase.client import FileUpload client = PocketBase('http://127.0.0.1:8090') -... - -# list and filter "example" collection records -result = client.records.get_list( - "example", 1, 20, {"filter": 'status = true && created > "2022-08-01 10:00:00"'} -) - # authenticate as regular user -user_data = client.users.auth_via_email("test@example.com", "123456") +user_data = client.collection("users").auth_with_password( + "user@example.com", "0123456789") # or as admin -admin_data = client.admins.auth_with_password("test@example.com", "123456") +admin_data = client.admins.auth_with_password("test@example.com", "0123456789") + +# list and filter "example" collection records +result = client.collection("example").get_list( + 1, 20, {"filter": 'status = true && created > "2022-08-01 10:00:00"'}) + +# create record and upload file to image field +result = client.collection("example").create( + { + "status": "true", + "image": FileUpload(("image.png", open("image.png", "rb"))), + }) # and much more... ``` > More detailed API docs and copy-paste examples could be found in the [API documentation for each service](https://pocketbase.io/docs/api-authentication). Just remember to 'pythonize it' 🙃. -
The PocketBase Python SDK is MIT licensed code.
\ No newline at end of file +The PocketBase Python SDK is MIT licensed code.
diff --git a/pocketbase/client.py b/pocketbase/client.py index b27e656..2604252 100644 --- a/pocketbase/client.py +++ b/pocketbase/client.py @@ -1,46 +1,34 @@ from __future__ import annotations -from pocketbase.services.admins import Admins -from pocketbase.stores.base_auth_store import BaseAuthStore -from pocketbase.services.settings import Settings -from pocketbase.services.users import Users -from pocketbase.services.records import Records -from pocketbase.services.realtime import Realtime -from pocketbase.services.logs import Logs -from pocketbase.services.collections import Collections -from pocketbase.models import FileUpload -from typing import Any +from typing import Any, Dict +from urllib.parse import quote, urlencode import httpx - -class ClientResponseError(Exception): - url: str = "" - status: int = 0 - data: dict = {} - is_abort: bool = False - original_error: Any | None = None - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args) - self.url = kwargs.get("url", "") - self.status = kwargs.get("status", 0) - self.data = kwargs.get("data", {}) - self.is_abort = kwargs.get("is_abort", False) - self.original_error = kwargs.get("original_error", None) +from pocketbase.models import FileUpload +from pocketbase.models.record import Record +from pocketbase.services.admin_service import AdminService +from pocketbase.services.collection_service import CollectionService +from pocketbase.services.log_service import LogService +from pocketbase.services.realtime_service import RealtimeService +from pocketbase.services.record_service import RecordService +from pocketbase.services.settings_service import SettingsService +from pocketbase.stores.base_auth_store import BaseAuthStore +from pocketbase.utils import ClientResponseError class Client: base_url: str lang: str auth_store: BaseAuthStore - settings: Settings - admins: Admins - users: Users - collections: Collections - records: Records - logs: Logs - realtime: Realtime + settings: SettingsService + admins: AdminService + records: Record + collections: CollectionService + records: RecordService + logs: LogService + realtime: RealtimeService + record_service: Dict[str, RecordService] def __init__( self, @@ -52,13 +40,18 @@ class Client: self.lang = lang self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore() # services - self.admins = Admins(self) - self.users = Users(self) - self.records = Records(self) - self.collections = Collections(self) - self.logs = Logs(self) - self.settings = Settings(self) - self.realtime = Realtime(self) + self.admins = AdminService(self) + self.collections = CollectionService(self) + self.logs = LogService(self) + self.settings = SettingsService(self) + self.realtime = RealtimeService(self) + self.record_service = {} + + def collection(self, id_or_name: str) -> RecordService: + """Returns the RecordService associated to the specified collection.""" + if id_or_name not in self.record_service: + self.record_service[id_or_name] = RecordService(self, id_or_name) + return self.record_service[id_or_name] def send(self, path: str, req_config: dict[str:Any]) -> Any: """Sends an api http request.""" @@ -69,9 +62,7 @@ class Client: "headers" not in config or "Authorization" not in config["headers"] ): config["headers"] = config.get("headers", {}) - config["headers"].update( - {"Authorization": self.auth_store.token} - ) + config["headers"].update({"Authorization": self.auth_store.token}) # build url + path url = self.build_url(path) # send the request @@ -123,6 +114,21 @@ class Client: ) return data + def get_file_url(self, record: Record, filename: str, query_params: dict): + parts = [ + "api", + "files", + quote(record.collection_id or record.collection_name), + quote(record.id), + quote(filename), + ] + result = self.build_url("/".join(parts)) + if len(query_params) != 0: + params: str = urlencode(query_params) + result += "&" if "?" in result else "?" + result += params + return result + def build_url(self, path: str) -> str: url = self.base_url if not self.base_url.endswith("/"): diff --git a/pocketbase/models/__init__.py b/pocketbase/models/__init__.py index e647289..4e31d92 100644 --- a/pocketbase/models/__init__.py +++ b/pocketbase/models/__init__.py @@ -3,5 +3,4 @@ from .collection import Collection from .external_auth import ExternalAuth from .log_request import LogRequest from .record import Record -from .user import User from .file_upload import FileUpload diff --git a/pocketbase/models/admin.py b/pocketbase/models/admin.py index f6b988f..d52f2a0 100644 --- a/pocketbase/models/admin.py +++ b/pocketbase/models/admin.py @@ -9,10 +9,8 @@ from pocketbase.models.utils.base_model import BaseModel class Admin(BaseModel): avatar: int email: str - last_reset_sent_at: str | datetime.datetime def load(self, data: dict) -> None: super().load(data) self.avatar = data.get("avatar", 0) self.email = data.get("email", "") - self.last_reset_sent_at = to_datetime(data.get("lastResetSentAt", "")) diff --git a/pocketbase/models/collection.py b/pocketbase/models/collection.py index d9b5584..566e7b7 100644 --- a/pocketbase/models/collection.py +++ b/pocketbase/models/collection.py @@ -6,6 +6,7 @@ from pocketbase.models.utils.schema_field import SchemaField class Collection(BaseModel): name: str + type: str schema: list[SchemaField] system: bool list_rule: str | None @@ -13,17 +14,33 @@ class Collection(BaseModel): create_rule: str | None update_rule: str | None delete_rule: str | None + options: dict def load(self, data: dict) -> None: super().load(data) self.name = data.get("name", "") self.system = data.get("system", False) + self.type = data.get("type", "base") + self.options = data.get("options", {}) + + # rules self.list_rule = data.get("listRule", None) self.view_rule = data.get("viewRule", None) self.create_rule = data.get("createRule", None) self.update_rule = data.get("updateRule", None) self.delete_rule = data.get("deleteRule", "") + + # schema schema = data.get("schema", []) self.schema = [] for field in schema: self.schema.append(SchemaField(**field)) + + def is_base(self): + return self.type == "base" + + def is_auth(self): + return self.type == "auth" + + def is_single(self): + return self.type == "single" diff --git a/pocketbase/models/external_auth.py b/pocketbase/models/external_auth.py index bef3d84..1ccf431 100644 --- a/pocketbase/models/external_auth.py +++ b/pocketbase/models/external_auth.py @@ -4,12 +4,14 @@ from pocketbase.models.utils.base_model import BaseModel class ExternalAuth(BaseModel): - user_id: str + record_id: str + collection_id: str provider: str provider_id: str def load(self, data: dict) -> None: super().load(data) - self.user_id = data.get("userId", "") + self.record_id = data.get("recordId", "") + self.collection_id = data.get("collectionId", "") self.provider = data.get("provider", "") self.provider_id = data.get("providerId", "") diff --git a/pocketbase/models/file_upload.py b/pocketbase/models/file_upload.py index 3e5eda7..285c082 100644 --- a/pocketbase/models/file_upload.py +++ b/pocketbase/models/file_upload.py @@ -1,5 +1,6 @@ from httpx._types import FileTypes from typing import Sequence, Union + FileUploadTypes = Union[FileTypes, Sequence[FileTypes]] diff --git a/pocketbase/models/record.py b/pocketbase/models/record.py index 156140f..78ba51b 100644 --- a/pocketbase/models/record.py +++ b/pocketbase/models/record.py @@ -11,15 +11,11 @@ class Record(BaseModel): def load(self, data: dict) -> None: super().load(data) + self.expand = {} for key, value in data.items(): key = camel_to_snake(key).replace("@", "") setattr(self, key, value) - self.collection_id = data.get("@collectionId", "") - self.collection_name = data.get("@collectionName", "") - expand = data.get("@expand", {}) - if expand: - self.expand = expand - self.load_expanded() + self.load_expanded() @classmethod def parse_expanded(cls, data: dict): diff --git a/pocketbase/models/user.py b/pocketbase/models/user.py deleted file mode 100644 index 0a231dc..0000000 --- a/pocketbase/models/user.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from typing import Optional, Union -import datetime - -from pocketbase.utils import to_datetime -from pocketbase.models.record import Record -from pocketbase.models.utils.base_model import BaseModel - - -class User(BaseModel): - email: str - verified: bool - last_reset_sent_at: str | datetime.datetime - last_verification_sent_at: str | datetime.datetime - profile: Record | None - - def load(self, data: dict) -> None: - super().load(data) - self.email = data.get("email", "") - self.verified = data.get("verified", "") - self.last_reset_sent_at = to_datetime(data.get("lastResetSentAt", "")) - self.last_verification_sent_at = to_datetime( - data.get("lastVerificationSentAt", "") - ) - profile = data.get("profile", None) - self.profile = None - if profile: - self.profile = Record(profile) diff --git a/pocketbase/models/utils/base_model.py b/pocketbase/models/utils/base_model.py index e21d66e..ba8505e 100644 --- a/pocketbase/models/utils/base_model.py +++ b/pocketbase/models/utils/base_model.py @@ -30,4 +30,4 @@ class BaseModel(ABC): @property def is_new(self) -> bool: """Returns whether the current loaded data represent a stored db record.""" - return not self.id or self.id == "00000000-0000-0000-0000-000000000000" + return not self.id diff --git a/pocketbase/services/__init__.py b/pocketbase/services/__init__.py index 95100f3..750ae1d 100644 --- a/pocketbase/services/__init__.py +++ b/pocketbase/services/__init__.py @@ -1,7 +1,6 @@ -from .admins import Admins, AdminAuthResponse -from .collections import Collections -from .logs import Logs, HourlyStats -from .realtime import Realtime -from .records import Records -from .settings import Settings -from .users import Users, UserAuthResponse, AuthMethodsList, AuthProviderInfo +from .admin_service import AdminService, AdminAuthResponse +from .collection_service import CollectionService +from .log_service import LogService, HourlyStats +from .realtime_service import RealtimeService +from .record_service import RecordService +from .settings_service import SettingsService diff --git a/pocketbase/services/admins.py b/pocketbase/services/admin_service.py similarity index 70% rename from pocketbase/services/admins.py rename to pocketbase/services/admin_service.py index 7d68980..5949cf0 100644 --- a/pocketbase/services/admins.py +++ b/pocketbase/services/admin_service.py @@ -16,13 +16,45 @@ class AdminAuthResponse: setattr(self, key, value) -class Admins(CrudService): +class AdminService(CrudService): def decode(self, data: dict) -> BaseModel: return Admin(data) def base_crud_path(self) -> str: return "/api/admins" + def update(self, id: str, body_params: dict, query_params: dict) -> BaseModel: + """ + If the current `client.auth_store.model` matches with the updated id, + then on success the `client.auth_store.model` will be updated with the result. + """ + item = super(AdminService).update(id, body_params) + try: + if ( + self.client.auth_store.model.collection_id is not None + and item.id == self.client.auth_store.model.id + ): + self.client.auth_store.save(self.client.auth_store.token, item) + except: + pass + return item + + def delete(self, id: str, body_params: dict, query_params: dict) -> BaseModel: + """ + If the current `client.auth_store.model` matches with the deleted id, + then on success the `client.auth_store` will be cleared. + """ + item = super(AdminService).delete(id, body_params) + try: + if ( + self.client.auth_store.model.collection_id is not None + and item.id == self.client.auth_store.model.id + ): + self.client.auth_store.save(self.client.auth_store.token, item) + except: + pass + return item + def auth_response(self, response_data: dict) -> AdminAuthResponse: """Prepare successful authorize response.""" admin = self.decode(response_data.pop("admin", {})) @@ -35,7 +67,7 @@ class Admins(CrudService): self, email: str, password: str, body_params: dict = {}, query_params: dict = {} ) -> AdminAuthResponse: """ - Authenticate an admin account by its email and password + Authenticate an admin account with its email and password and returns a new admin token and data. On success this method automatically updates the client's AuthStore data. @@ -52,7 +84,7 @@ class Admins(CrudService): ) return self.auth_response(response_data) - def refresh( + def authRefresh( self, body_params: dict = {}, query_params: dict = {} ) -> AdminAuthResponse: """ @@ -63,7 +95,7 @@ class Admins(CrudService): """ return self.auth_response( self.client.send( - self.base_crud_path() + "/refresh", + self.base_crud_path() + "/auth-refresh", {"method": "POST", "params": query_params, "body": body_params}, ) ) diff --git a/pocketbase/services/collections.py b/pocketbase/services/collection_service.py similarity index 96% rename from pocketbase/services/collections.py rename to pocketbase/services/collection_service.py index 76cb27e..9188a4e 100644 --- a/pocketbase/services/collections.py +++ b/pocketbase/services/collection_service.py @@ -5,7 +5,7 @@ from pocketbase.models.utils.base_model import BaseModel from pocketbase.models.collection import Collection -class Collections(CrudService): +class CollectionService(CrudService): def decode(self, data: dict) -> BaseModel: return Collection(data) diff --git a/pocketbase/services/logs.py b/pocketbase/services/log_service.py similarity index 98% rename from pocketbase/services/logs.py rename to pocketbase/services/log_service.py index f352e6f..5f6f10b 100644 --- a/pocketbase/services/logs.py +++ b/pocketbase/services/log_service.py @@ -17,7 +17,7 @@ class HourlyStats: date: Union[str, datetime.datetime] -class Logs(BaseService): +class LogService(BaseService): def get_request_list( self, page: int = 1, per_page: int = 30, query_params: dict = {} ) -> ListResult: diff --git a/pocketbase/services/realtime.py b/pocketbase/services/realtime_service.py similarity index 74% rename from pocketbase/services/realtime.py rename to pocketbase/services/realtime_service.py index a24c6ca..269f908 100644 --- a/pocketbase/services/realtime.py +++ b/pocketbase/services/realtime_service.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, List import dataclasses import json @@ -15,7 +15,7 @@ class MessageData: record: Record -class Realtime(BaseService): +class RealtimeService(BaseService): subscriptions: dict client_id: str = "" event_source: SSEClient | None = None @@ -40,28 +40,51 @@ class Realtime(BaseService): elif self.client_id: self._submit_subscriptions() - def unsubscribe(self, subscription: str | None = None) -> None: + def unsubscribe_by_prefix(self, subscription_prefix: str): + """ + Unsubscribe from all subscriptions starting with the provided prefix. + + This method is no-op if there are no active subscriptions with the provided prefix. + + The related sse connection will be autoclosed if after the + unsubscribe operation there are no active subscriptions left. + """ + to_unsubscribe = [] + for sub in self.subscriptions: + if sub.startswith(subscription_prefix): + to_unsubscribe.append(sub) + if len(to_unsubscribe) == 0: + return + return self.unsubscribe(*to_unsubscribe) + + def unsubscribe(self, subscriptions: List[str] | None = None) -> None: """ Unsubscribe from a subscription. - If the `subscription` argument is not set, + If the `subscriptions` argument is not set, then the client will unsubscribe from all registered subscriptions. The related sse connection will be autoclosed if after the unsubscribe operations there are no active subscriptions left. """ - if not subscription: + if not subscriptions or len(subscriptions) == 0: + # remove all subscriptions self._remove_subscription_listeners() self.subscriptions = {} - elif subscription in self.subscriptions: - self.event_source.remove_event_listener( - subscription, self.subscriptions[subscription] - ) - self.subscriptions.pop(subscription) else: - return + # remove each passed subscription + found = False + for sub in self.subscriptions: + found = True + self.event_source.remove_event_listener(sub, self.subscriptions[sub]) + self.subscriptions.pop(sub) + if not found: + return + if self.client_id: self._submit_subscriptions() + + # no more subscriptions -> close the sse connection if not self.subscriptions: self._disconnect() diff --git a/pocketbase/services/record_service.py b/pocketbase/services/record_service.py new file mode 100644 index 0000000..79241df --- /dev/null +++ b/pocketbase/services/record_service.py @@ -0,0 +1,275 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List + +from urllib.parse import quote, urlencode +from pocketbase.services.realtime_service import Callable, MessageData + +from pocketbase.models.utils.base_model import BaseModel +from pocketbase.models.record import Record +from pocketbase.services.utils.crud_service import CrudService +from pocketbase.utils import camel_to_snake + + +class RecordAuthResponse: + token: str + record: Record + + def __init__(self, token: str, record: Record, **kwargs) -> None: + self.token = token + self.record = record + for key, value in kwargs.items(): + setattr(self, key, value) + + +@dataclass +class AuthProviderInfo: + name: str + state: str + code_verifier: str + code_challenge: str + code_challenge_method: str + auth_url: str + + +@dataclass +class AuthMethodsList: + username_password: bool + email_password: bool + auth_providers: list[AuthProviderInfo] + + +class RecordService(CrudService): + collection_id_or_name: str + + def __init__(self, client, collection_id_or_name) -> None: + super().__init__(client) + self.collection_id_or_name = collection_id_or_name + + def decode(self, data: dict) -> BaseModel: + return Record(data) + + def base_crud_path(self) -> str: + return self.base_collection_path() + "/records" + + def base_collection_path(self) -> str: + """Returns the current collection service base path.""" + return "/api/collections/" + quote(self.collection_id_or_name) + + def get_file_url( + self, record: Record, filename: str, query_params: dict = {} + ) -> str: + """Builds and returns an absolute record file url.""" + base_url = self.client.base_url + if base_url.endswith("/"): + base_url = base_url[:-1] + result = f"{base_url}/api/files/{record.collection_id}/{record.id}/{filename}" + if query_params: + result += "?" + urlencode(query_params) + return result + + def subscribe(self, callback: Callable[[MessageData], None]): + """Subscribe to realtime changes of any record from the collection.""" + return self.client.realtime.subscribe(self.collection_id_or_name, callback) + + def subscribeOne(self, record_id: str, callback: Callable[[MessageData], None]): + """Subscribe to the realtime changes of a single record in the collection.""" + return self.client.realtime.subscribe( + self.collection_id_or_name + "/" + record_id, callback + ) + + def unsubscribe(self, *record_ids: List[str]): + """Subscribe to the realtime changes of a single record in the collection.""" + if record_ids and len(record_ids) == 0: + subs = [] + for id in record_ids: + subs.append(self.collection_id_or_name + "/" + id) + return self.client.realtime.unsubscribe(*subs) + return self.client.realtime.subscribe_by_prefix(self.collection_id_or_name) + + def update(self, id: str, body_params: dict = {}, query_params: dict = {}): + """ + If the current `client.auth_store.model` matches with the updated id, then + on success the `client.auth_store.model` will be updated with the result. + """ + item = super().update(id, body_params) # super(Record).update + try: + if ( + self.client.auth_store.model.collection_id is not None + and item.id == self.client.auth_store.model.id + ): + self.client.auth_store.save(self.client.auth_store.token, item) + except: + pass + return item + + def delete(self, id: str, body_params: dict = {}, query_params: dict = {}): + """ + If the current `client.auth_store.model` matches with the deleted id, + then on success the `client.auth_store` will be cleared. + """ + success = super().delete(id, body_params) # super(Record).delete + try: + if ( + success + and self.client.auth_store.model.collection_id is not None + and id == self.client.auth_store.model.id + ): + self.client.auth_store.clear() + except: + pass + return success + + def auth_response(self, response_data: dict) -> RecordAuthResponse: + """Prepare successful collection authorization response.""" + record = self.decode(response_data.pop("record", {})) + token = response_data.pop("token", "") + if token and record: + self.client.auth_store.save(token, record) + return RecordAuthResponse(token=token, record=record, **response_data) + + def list_auth_methods(self, query_params: str = {}): + """Returns all available collection auth methods.""" + response_data = self.client.send( + self.base_collection_path() + "/auth-methods", + {"method": "GET", "params": query_params}, + ) + username_password = response_data.pop("usernamePassword", False) + email_password = response_data.pop("emailPassword", False) + + def apply_pythonic_keys(ap): + pythonic_keys_ap = { + camel_to_snake(key).replace("@", ""): value for key, value in ap.items() + } + return pythonic_keys_ap + + auth_providers = [ + AuthProviderInfo(**auth_provider) + for auth_provider in map( + apply_pythonic_keys, response_data.get("authProviders", []) + ) + ] + return AuthMethodsList(username_password, email_password, auth_providers) + + def auth_with_password( + self, + username_or_email: str, + password: str, + body_params: dict = {}, + query_params: dict = {}, + ) -> RecordAuthResponse: + """ + Authenticate a single auth collection record via its username/email and password. + + On success, this method also automatically updates + the client's AuthStore data and returns: + - the authentication token + - the authenticated record model + """ + body_params.update({"identity": username_or_email, "password": password}) + response_data = self.client.send( + self.base_collection_path() + "/auth-with-password", + { + "method": "POST", + "params": query_params, + "body": body_params, + "headers": {"Authorization": ""}, + }, + ) + return self.auth_response(response_data) + + def auth_with_oauth2( + self, + provider: str, + code: str, + code_verifier: str, + redirct_url: str, + create_data={}, + body_params={}, + query_params={}, + ): + """ + Authenticate a single auth collection record with OAuth2. + + On success, this method also automatically updates + the client's AuthStore data and returns: + - the authentication token + - the authenticated record model + - the OAuth2 account data (eg. name, email, avatar, etc.) + """ + body_params.update( + { + "provider": provider, + "code": code, + "codeVerifier": code_verifier, + "redirectUrl": redirct_url, + "createData": create_data, + } + ) + response_data = self.client.send( + self.base_collection_path() + "/auth-with-password", + { + "method": "POST", + "params": query_params, + "body": body_params, + }, + ) + return self.auth_response(response_data) + + def authRefresh( + self, body_params: dict = {}, query_params: dict = {} + ) -> RecordAuthResponse: + """ + Refreshes the current authenticated record instance and + returns a new token and record data. + + On success this method also automatically updates the client's AuthStore. + """ + return self.auth_response( + self.client.send( + self.base_collection_path() + "/auth-refresh", + {"method": "POST", "params": query_params, "body": body_params}, + ) + ) + + def requestPasswordReset( + self, email: str, body_params: dict = {}, query_params: dict = {} + ) -> bool: + """Sends auth record password reset request.""" + body_params.update({"email": email}) + self.client.send( + self.base_collection_path() + "/request-password-reset", + { + "method": "POST", + "params": query_params, + "body": body_params, + }, + ) + return True + + def confirmPasswordReset( + self, + password_reset_token: str, + password: str, + password_confirm: str, + body_params: dict = {}, + query_params: dict = {}, + ) -> RecordAuthResponse: + """Confirms auth record password reset reque""" + body_params.update( + { + "token": password_reset_token, + "password": password, + "passwordConfirm": password_confirm, + } + ) + return self.auth_response( + self.client.send( + self.base_collection_path() + "/confirm-password-reset", + { + "method": "POST", + "params": query_params, + "body": body_params, + }, + ) + ) diff --git a/pocketbase/services/records.py b/pocketbase/services/records.py deleted file mode 100644 index 783bd76..0000000 --- a/pocketbase/services/records.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from urllib.parse import quote, urlencode - -from pocketbase.services.utils.sub_crud_service import SubCrudService -from pocketbase.models.utils.base_model import BaseModel -from pocketbase.models.record import Record - - -class Records(SubCrudService): - def decode(self, data: dict) -> BaseModel: - return Record(data) - - def base_crud_path(self, collection_id_or_name: str) -> str: - return "/api/collections/" + quote(collection_id_or_name) + "/records" - - def get_file_url( - self, record: Record, filename: str, query_params: dict = {} - ) -> str: - """Builds and returns an absolute record file url.""" - base_url = self.client.base_url - if base_url.endswith("/"): - base_url = base_url[:-1] - result = f"{base_url}/api/files/{record.collection_id}/{record.id}/{filename}" - if query_params: - result += "?" + urlencode(query_params) - return result diff --git a/pocketbase/services/settings.py b/pocketbase/services/settings_service.py similarity index 97% rename from pocketbase/services/settings.py rename to pocketbase/services/settings_service.py index 4f8eae5..9098d05 100644 --- a/pocketbase/services/settings.py +++ b/pocketbase/services/settings_service.py @@ -3,7 +3,7 @@ from __future__ import annotations from pocketbase.services.utils.base_service import BaseService -class Settings(BaseService): +class SettingsService(BaseService): def get_all(self, query_params: dict = {}) -> dict: """Fetch all available app settings.""" return self.client.send( diff --git a/pocketbase/services/users.py b/pocketbase/services/users.py deleted file mode 100644 index ab40c3d..0000000 --- a/pocketbase/services/users.py +++ /dev/null @@ -1,283 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any -from urllib.parse import quote - -from pocketbase.services.utils.crud_service import CrudService -from pocketbase.models.utils.base_model import BaseModel -from pocketbase.models.user import User -from pocketbase.models.external_auth import ExternalAuth - - -class UserAuthResponse: - token: str - user: User - - def __init__(self, token: str, user: User, **kwargs) -> None: - self.token = token - self.user = user - for key, value in kwargs.items(): - setattr(self, key, value) - - -@dataclass -class AuthProviderInfo: - name: str - state: str - code_verifier: str - code_challenge: str - code_challenge_method: str - auth_url: str - - -@dataclass -class AuthMethodsList: - email_password: bool - auth_providers: list[AuthProviderInfo] - - -class Users(CrudService): - def decode(self, data: dict) -> BaseModel: - return User(data) - - def base_crud_path(self) -> str: - return "/api/users" - - def auth_response(self, response_data: Any) -> UserAuthResponse: - """Prepare successful authorization response.""" - user = self.decode(response_data.pop("user", {})) - token = response_data.pop("token", "") - if token and user: - self.client.auth_store.save(token, user) - return UserAuthResponse(token=token, user=user, **response_data) - - def list_auth_methods(self, query_params: dict = {}) -> AuthMethodsList: - """Returns all available application auth methods.""" - response_data = self.client.send( - self.base_crud_path() + "/auth-methods", - {"method": "GET", "params": query_params}, - ) - email_password = response_data.get("emailPassword", False) - auth_providers = [ - AuthProviderInfo(auth_provider) - for auth_provider in response_data.get("authProviders", []) - ] - return AuthMethodsList(email_password, auth_providers) - - def auth_via_email( - self, email: str, password: str, body_params: dict = {}, query_params: dict = {} - ) -> UserAuthResponse: - """ - Authenticate a user via its email and password. - - On success, this method also automatically updates - the client's AuthStore data and returns: - - new user authentication token - - the authenticated user model record - """ - body_params.update({"email": email, "password": password}) - response_data = self.client.send( - self.base_crud_path() + "/auth-via-email", - { - "method": "POST", - "params": query_params, - "body": body_params, - "headers": {"Authorization": ""}, - }, - ) - return self.auth_response(response_data) - - def auth_via_oauth2( - self, - provider: str, - code: str, - code_verifier: str, - redirect_url: str, - body_params: dict = {}, - query_params: dict = {}, - ) -> UserAuthResponse: - """ - Authenticate a user via OAuth2 client provider. - - On success, this method also automatically updates - the client's AuthStore data and returns: - - new user authentication token - - the authenticated user model record - - the OAuth2 user profile data (eg. name, email, avatar, etc.) - """ - body_params.update( - { - "provider": provider, - "code": code, - "codeVerifier": code_verifier, - "redirectUrl": redirect_url, - } - ) - response_data = self.client.send( - self.base_crud_path() + "/auth-via-oauth2", - { - "method": "POST", - "params": query_params, - "body": body_params, - "headers": {"Authorization": ""}, - }, - ) - return self.auth_response(response_data) - - def refresh( - self, body_params: dict = {}, query_params: dict = {} - ) -> UserAuthResponse: - """ - Refreshes the current user authenticated instance and - returns a new token and user data. - - On success this method also automatically updates the client's AuthStore data. - """ - return self.auth_response( - self.client.send( - self.base_crud_path() + "/refresh", - {"method": "POST", "params": query_params, "body": body_params}, - ) - ) - - def request_password_reset( - self, email: str, body_params: dict = {}, query_params: dict = {} - ) -> bool: - """Sends user password reset request.""" - body_params.update({"email": email}) - self.client.send( - self.base_crud_path() + "/request-password-reset", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - return True - - def confirm_password_reset( - self, - password_reset_token: str, - password: str, - password_confirm: str, - body_params: dict = {}, - query_params: dict = {}, - ) -> UserAuthResponse: - """Confirms user password reset request.""" - body_params.update( - { - "token": password_reset_token, - "password": password, - "passwordConfirm": password_confirm, - } - ) - return self.auth_response( - self.client.send( - self.base_crud_path() + "/confirm-password-reset", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - ) - - def request_verification( - self, email: str, body_params: dict = {}, query_params: dict = {} - ) -> bool: - """Sends user verification email request.""" - body_params.update({"email": email}) - self.client.send( - self.base_crud_path() + "/request-verification", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - return True - - def confirm_verification( - self, verification_token: str, body_params: dict = {}, query_params: dict = {} - ) -> UserAuthResponse: - """Confirms user email verification request.""" - body_params.update( - { - "token": verification_token, - } - ) - return self.auth_response( - self.client.send( - self.base_crud_path() + "/confirm-verification", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - ) - - def request_email_change( - self, new_email: str, body_params: dict = {}, query_params: dict = {} - ) -> bool: - """Sends an email change request to the authenticated user.""" - body_params.update({"newEmail": new_email}) - self.client.send( - self.base_crud_path() + "/request-email-change", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - return True - - def confirm_email_change( - self, - email_change_token: str, - password: str, - body_params: dict = {}, - query_params: dict = {}, - ) -> UserAuthResponse: - """Confirms user new email address.""" - body_params.update( - { - "token": email_change_token, - "password": password, - } - ) - return self.auth_response( - self.client.send( - self.base_crud_path() + "/confirm-email-change", - { - "method": "POST", - "params": query_params, - "body": body_params, - }, - ) - ) - - def list_external_auths( - self, user_id: str, query_params: dict = {} - ) -> list[ExternalAuth]: - """Lists all linked external auth providers for the specified user.""" - response_data = self.client.send( - self.base_crud_path() + "/" + quote(user_id) + "/external-auths", - {"method": "GET", "params": query_params}, - ) - return [ExternalAuth(item) for item in response_data] - - def unlink_external_auth( - self, user_id: str, provider: str, query_params: dict = {} - ) -> bool: - """Unlink a single external auth provider from the specified user.""" - self.client.send( - self.base_crud_path() - + "/" - + quote(user_id) - + "/external-auths/" - + quote(provider), - {"method": "DELETE", "params": query_params}, - ) - return True diff --git a/pocketbase/services/utils/__init__.py b/pocketbase/services/utils/__init__.py index a57fdcb..b34ee1d 100644 --- a/pocketbase/services/utils/__init__.py +++ b/pocketbase/services/utils/__init__.py @@ -1,4 +1,3 @@ from .base_crud_service import BaseCrudService from .base_service import BaseService from .crud_service import CrudService -from .sub_crud_service import SubCrudService diff --git a/pocketbase/services/utils/base_crud_service.py b/pocketbase/services/utils/base_crud_service.py index 38fb859..b4e8e9f 100644 --- a/pocketbase/services/utils/base_crud_service.py +++ b/pocketbase/services/utils/base_crud_service.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABC from urllib.parse import quote +from pocketbase.utils import ClientResponseError from pocketbase.models.utils.base_model import BaseModel from pocketbase.models.utils.list_result import ListResult @@ -56,6 +57,22 @@ class BaseCrudService(BaseService, ABC): ) ) + def _get_first_list_item(self, base_path: str, filter: str, query_params={}): + query_params.update( + { + "filter": filter, + "$cancelKey": "one_by_filter_" + base_path + "_" + filter, + } + ) + result = self._get_list(base_path, 1, 1, query_params) + try: + if len(result.items) == 0: + raise + except: + raise ClientResponseError( + "The requested resource wasn't found.", status=404 + ) + def _create( self, base_path: str, body_params: dict = {}, query_params: dict = {} ) -> BaseModel: diff --git a/pocketbase/services/utils/crud_service.py b/pocketbase/services/utils/crud_service.py index 6340bbe..2411e99 100644 --- a/pocketbase/services/utils/crud_service.py +++ b/pocketbase/services/utils/crud_service.py @@ -12,16 +12,31 @@ class CrudService(BaseCrudService, ABC): """Base path for the crud actions (without trailing slash, eg. '/admins').""" def get_full_list( - self, batch_size: int = 100, query_params: dict = {} + self, batch: int = 200, query_params: dict = {} ) -> list[BaseModel]: - return self._get_full_list(self.base_crud_path(), batch_size, query_params) + return self._get_full_list(self.base_crud_path(), batch, query_params) def get_list( self, page: int = 1, per_page: int = 30, query_params: dict = {} ) -> ListResult: return self._get_list(self.base_crud_path(), page, per_page, query_params) + def _get_first_list_item(self, base_path: str, filter: str, query_params): + """ + Returns the first found item by the specified filter. + + Internally it calls `getList(1, 1, { filter })` and returns the + first found item. + + For consistency with `getOne`, this method will throw a 404 + ClientResponseError if no item was found. + """ + return self._get_first_list_item(base_path, filter, query_params) + def get_one(self, id: str, query_params: dict = {}) -> BaseModel: + """ + Returns single item by its id. + """ return self._get_one(self.base_crud_path(), id, query_params) def create(self, body_params: dict = {}, query_params: dict = {}) -> BaseModel: diff --git a/pocketbase/services/utils/sub_crud_service.py b/pocketbase/services/utils/sub_crud_service.py deleted file mode 100644 index eaa5a30..0000000 --- a/pocketbase/services/utils/sub_crud_service.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -from abc import ABC - -from pocketbase.models.utils.base_model import BaseModel -from pocketbase.models.utils.list_result import ListResult -from pocketbase.services.utils.base_crud_service import BaseCrudService - - -class SubCrudService(BaseCrudService, ABC): - def base_crud_path(self) -> str: - """Base path for the crud actions (without trailing slash, eg. '/admins').""" - - def get_full_list( - self, sub: str, batch_size: int = 100, query_params: dict = {} - ) -> list[BaseModel]: - return self._get_full_list(self.base_crud_path(sub), batch_size, query_params) - - def get_list( - self, sub: str, page: int = 1, per_page: int = 30, query_params: dict = {} - ) -> ListResult: - return self._get_list(self.base_crud_path(sub), page, per_page, query_params) - - def get_one(self, sub: str, id: str, query_params: dict = {}) -> BaseModel: - return self._get_one(self.base_crud_path(sub), id, query_params) - - def create( - self, sub: str, body_params: dict = {}, query_params: dict = {} - ) -> BaseModel: - return self._create(self.base_crud_path(sub), body_params, query_params) - - def update( - self, sub: str, id: str, body_params: dict = {}, query_params: dict = {} - ) -> BaseModel: - return self._update(self.base_crud_path(sub), id, body_params, query_params) - - def delete(self, sub: str, id: str, query_params: dict = {}) -> bool: - return self._delete(self.base_crud_path(sub), id, query_params) diff --git a/pocketbase/stores/base_auth_store.py b/pocketbase/stores/base_auth_store.py index fc6a9d9..920e5c2 100644 --- a/pocketbase/stores/base_auth_store.py +++ b/pocketbase/stores/base_auth_store.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC from pocketbase.models.admin import Admin -from pocketbase.models.user import User +from pocketbase.models.record import Record class BaseAuthStore(ABC): @@ -13,10 +13,10 @@ class BaseAuthStore(ABC): """ base_token: str - base_model: User | Admin | None + base_model: Record | Admin | None def __init__( - self, base_token: str = "", base_model: User | Admin | None = None + self, base_token: str = "", base_model: Record | Admin | None = None ) -> None: super().__init__() self.base_token = base_token @@ -28,14 +28,15 @@ class BaseAuthStore(ABC): return self.base_token @property - def model(self) -> User | Admin | None: + def model(self) -> Record | Admin | None: """Retrieves the stored model data (if any).""" return self.base_model - def save(self, token: str = "", model: User | Admin | None = None) -> None: + def save(self, token: str = "", model: Record | Admin | None = None) -> None: """Saves the provided new token and model data in the auth store.""" - self.base_token = token - self.base_model = model + + self.base_token = token if token else "" + self.base_model = model if model else None def clear(self) -> None: """Removes the stored token and model data form the auth store.""" diff --git a/pocketbase/stores/local_auth_store.py b/pocketbase/stores/local_auth_store.py index ebb4100..0dcf344 100644 --- a/pocketbase/stores/local_auth_store.py +++ b/pocketbase/stores/local_auth_store.py @@ -5,7 +5,7 @@ import pickle import os from pocketbase.stores.base_auth_store import BaseAuthStore -from pocketbase.models.user import User +from pocketbase.models.record import Record from pocketbase.models.admin import Admin @@ -18,7 +18,7 @@ class LocalAuthStore(BaseAuthStore): filename: str = "pocketbase_auth.data", filepath: str = "", base_token: str = "", - base_model: User | Admin | None = None, + base_model: Record | Admin | None = None, ) -> None: super().__init__(base_token, base_model) self.filename = filename @@ -33,13 +33,13 @@ class LocalAuthStore(BaseAuthStore): return data["token"] @property - def model(self) -> User | Admin | None: + def model(self) -> Record | Admin | None: data = self._storage_get(self.complete_filepath) if not data or "model" not in data: return None return data["model"] - def save(self, token: str = "", model: User | Admin | None = None) -> None: + def save(self, token: str = "", model: Record | Admin | None = None) -> None: self._storage_set(self.complete_filepath, {"token": token, "model": model}) super().save(token, model) diff --git a/pocketbase/utils.py b/pocketbase/utils.py index 57c804b..4eb25c3 100644 --- a/pocketbase/utils.py +++ b/pocketbase/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations import re import datetime +from typing import Any def camel_to_snake(name: str) -> str: @@ -17,3 +18,19 @@ def to_datetime( return datetime.datetime.strptime(str_datetime, format) except Exception: return str_datetime + + +class ClientResponseError(Exception): + url: str = "" + status: int = 0 + data: dict = {} + is_abort: bool = False + original_error: Any | None = None + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args) + self.url = kwargs.get("url", "") + self.status = kwargs.get("status", 0) + self.data = kwargs.get("data", {}) + self.is_abort = kwargs.get("is_abort", False) + self.original_error = kwargs.get("original_error", None) diff --git a/poetry.lock b/poetry.lock index ce34ccf..58de259 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "anyio" -version = "3.6.1" +version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -14,7 +14,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +trio = ["trio (>=0.16,<0.22)"] [[package]] name = "attrs" @@ -28,15 +28,15 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "black" -version = "22.8.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -75,25 +75,22 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] -name = "coverage" -version = "6.4.4" -description = "Code coverage measurement for Python" +name = "exceptiongroup" +version = "1.0.1" +description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] -toml = ["tomli"] +test = ["pytest (>=6)"] [[package]] name = "flake8" @@ -224,15 +221,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.3" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -249,14 +246,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pycodestyle" version = "2.9.1" @@ -286,7 +275,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -295,31 +284,16 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - [[package]] name = "rfc3986" version = "1.5.0" @@ -360,7 +334,7 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -368,54 +342,52 @@ python-versions = ">=3.7" [[package]] name = "zipp" -version = "3.8.1" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "78af276171c1e41e0cb3205383b288b9ed15cb8fdf68e7184954909e8b07c7c6" +content-hash = "8f9534771a19adba002263f1c9c563e5d1bd7e1f134fd42aa85689b614fd6e0a" [metadata.files] anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, @@ -426,60 +398,12 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, +exceptiongroup = [ + {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"}, + {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"}, ] flake8 = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, @@ -526,17 +450,13 @@ pathspec = [ {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"}, + {file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] pycodestyle = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, @@ -550,12 +470,8 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, @@ -596,10 +512,10 @@ typed-ast = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index c981631..b9d9b7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ requires-python = ">=3.7" license = "MIT" authors = [ { name = "Vithor Jaeger", email = "vaphes@gmail.com" }, + { name = "Max Amling", email = "max-amling@web.de" }, ] classifiers = [ "Programming Language :: Python :: 3",