0.8.0 rc2 (#8)
* fix services * Port of PR #6 to branch 0.8.0 (#7) * Switch from JSON to multipart file encoding for file upload File upload is detected when body data contains a value of class FileUpload Remaining JSON is converted to FormData Enitre message is sent as multipart * Switch from JSON to multipart file encoding for file upload (#6) File upload is detected when body data contains a value of class FileUpload Remaining JSON is converted to FormData Enitre message is sent as multipart * fix readme * fix client * Remove "@" chars (#11) * Remove "@" chars that led to empty collectionId, collectionName and expand * Make load method more generic * fix license --------- Co-authored-by: Paulo Coutinho <paulocoutinhox@gmail.com> Co-authored-by: Martin <mahe@quantentunnel.de> Co-authored-by: Eoin Fennessy <85010533+eoinfennessy@users.noreply.github.com>
This commit is contained in:
parent
efe4bd8f67
commit
d320125c58
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) [year] [fullname]
|
Copyright (c) 2023 vaphes
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
24
README.md
24
README.md
@ -22,21 +22,27 @@ The rule of thumb here is just to use it as you would <a href="https://github.co
|
|||||||
|
|
||||||
```python
|
```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')
|
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
|
# 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
|
# 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...
|
# and much more...
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,46 +1,34 @@
|
|||||||
from __future__ import annotations
|
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
|
import httpx
|
||||||
|
|
||||||
|
from pocketbase.models import FileUpload
|
||||||
class ClientResponseError(Exception):
|
from pocketbase.models.record import Record
|
||||||
url: str = ""
|
from pocketbase.services.admin_service import AdminService
|
||||||
status: int = 0
|
from pocketbase.services.collection_service import CollectionService
|
||||||
data: dict = {}
|
from pocketbase.services.log_service import LogService
|
||||||
is_abort: bool = False
|
from pocketbase.services.realtime_service import RealtimeService
|
||||||
original_error: Any | None = None
|
from pocketbase.services.record_service import RecordService
|
||||||
|
from pocketbase.services.settings_service import SettingsService
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||||
super().__init__(*args)
|
from pocketbase.utils import ClientResponseError
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
base_url: str
|
base_url: str
|
||||||
lang: str
|
lang: str
|
||||||
auth_store: BaseAuthStore
|
auth_store: BaseAuthStore
|
||||||
settings: Settings
|
settings: SettingsService
|
||||||
admins: Admins
|
admins: AdminService
|
||||||
users: Users
|
records: Record
|
||||||
collections: Collections
|
collections: CollectionService
|
||||||
records: Records
|
records: RecordService
|
||||||
logs: Logs
|
logs: LogService
|
||||||
realtime: Realtime
|
realtime: RealtimeService
|
||||||
|
record_service: Dict[str, RecordService]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -52,13 +40,18 @@ class Client:
|
|||||||
self.lang = lang
|
self.lang = lang
|
||||||
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
||||||
# services
|
# services
|
||||||
self.admins = Admins(self)
|
self.admins = AdminService(self)
|
||||||
self.users = Users(self)
|
self.collections = CollectionService(self)
|
||||||
self.records = Records(self)
|
self.logs = LogService(self)
|
||||||
self.collections = Collections(self)
|
self.settings = SettingsService(self)
|
||||||
self.logs = Logs(self)
|
self.realtime = RealtimeService(self)
|
||||||
self.settings = Settings(self)
|
self.record_service = {}
|
||||||
self.realtime = Realtime(self)
|
|
||||||
|
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:
|
def send(self, path: str, req_config: dict[str:Any]) -> Any:
|
||||||
"""Sends an api http request."""
|
"""Sends an api http request."""
|
||||||
@ -69,9 +62,7 @@ class Client:
|
|||||||
"headers" not in config or "Authorization" not in config["headers"]
|
"headers" not in config or "Authorization" not in config["headers"]
|
||||||
):
|
):
|
||||||
config["headers"] = config.get("headers", {})
|
config["headers"] = config.get("headers", {})
|
||||||
config["headers"].update(
|
config["headers"].update({"Authorization": self.auth_store.token})
|
||||||
{"Authorization": self.auth_store.token}
|
|
||||||
)
|
|
||||||
# build url + path
|
# build url + path
|
||||||
url = self.build_url(path)
|
url = self.build_url(path)
|
||||||
# send the request
|
# send the request
|
||||||
@ -123,6 +114,21 @@ class Client:
|
|||||||
)
|
)
|
||||||
return data
|
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:
|
def build_url(self, path: str) -> str:
|
||||||
url = self.base_url
|
url = self.base_url
|
||||||
if not self.base_url.endswith("/"):
|
if not self.base_url.endswith("/"):
|
||||||
|
|||||||
@ -3,5 +3,4 @@ from .collection import Collection
|
|||||||
from .external_auth import ExternalAuth
|
from .external_auth import ExternalAuth
|
||||||
from .log_request import LogRequest
|
from .log_request import LogRequest
|
||||||
from .record import Record
|
from .record import Record
|
||||||
from .user import User
|
|
||||||
from .file_upload import FileUpload
|
from .file_upload import FileUpload
|
||||||
|
|||||||
@ -9,10 +9,8 @@ from pocketbase.models.utils.base_model import BaseModel
|
|||||||
class Admin(BaseModel):
|
class Admin(BaseModel):
|
||||||
avatar: int
|
avatar: int
|
||||||
email: str
|
email: str
|
||||||
last_reset_sent_at: str | datetime.datetime
|
|
||||||
|
|
||||||
def load(self, data: dict) -> None:
|
def load(self, data: dict) -> None:
|
||||||
super().load(data)
|
super().load(data)
|
||||||
self.avatar = data.get("avatar", 0)
|
self.avatar = data.get("avatar", 0)
|
||||||
self.email = data.get("email", "")
|
self.email = data.get("email", "")
|
||||||
self.last_reset_sent_at = to_datetime(data.get("lastResetSentAt", ""))
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from pocketbase.models.utils.schema_field import SchemaField
|
|||||||
|
|
||||||
class Collection(BaseModel):
|
class Collection(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
type: str
|
||||||
schema: list[SchemaField]
|
schema: list[SchemaField]
|
||||||
system: bool
|
system: bool
|
||||||
list_rule: str | None
|
list_rule: str | None
|
||||||
@ -13,17 +14,33 @@ class Collection(BaseModel):
|
|||||||
create_rule: str | None
|
create_rule: str | None
|
||||||
update_rule: str | None
|
update_rule: str | None
|
||||||
delete_rule: str | None
|
delete_rule: str | None
|
||||||
|
options: dict
|
||||||
|
|
||||||
def load(self, data: dict) -> None:
|
def load(self, data: dict) -> None:
|
||||||
super().load(data)
|
super().load(data)
|
||||||
self.name = data.get("name", "")
|
self.name = data.get("name", "")
|
||||||
self.system = data.get("system", False)
|
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.list_rule = data.get("listRule", None)
|
||||||
self.view_rule = data.get("viewRule", None)
|
self.view_rule = data.get("viewRule", None)
|
||||||
self.create_rule = data.get("createRule", None)
|
self.create_rule = data.get("createRule", None)
|
||||||
self.update_rule = data.get("updateRule", None)
|
self.update_rule = data.get("updateRule", None)
|
||||||
self.delete_rule = data.get("deleteRule", "")
|
self.delete_rule = data.get("deleteRule", "")
|
||||||
|
|
||||||
|
# schema
|
||||||
schema = data.get("schema", [])
|
schema = data.get("schema", [])
|
||||||
self.schema = []
|
self.schema = []
|
||||||
for field in schema:
|
for field in schema:
|
||||||
self.schema.append(SchemaField(**field))
|
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"
|
||||||
|
|||||||
@ -4,12 +4,14 @@ from pocketbase.models.utils.base_model import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class ExternalAuth(BaseModel):
|
class ExternalAuth(BaseModel):
|
||||||
user_id: str
|
record_id: str
|
||||||
|
collection_id: str
|
||||||
provider: str
|
provider: str
|
||||||
provider_id: str
|
provider_id: str
|
||||||
|
|
||||||
def load(self, data: dict) -> None:
|
def load(self, data: dict) -> None:
|
||||||
super().load(data)
|
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 = data.get("provider", "")
|
||||||
self.provider_id = data.get("providerId", "")
|
self.provider_id = data.get("providerId", "")
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from httpx._types import FileTypes
|
from httpx._types import FileTypes
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
FileUploadTypes = Union[FileTypes, Sequence[FileTypes]]
|
FileUploadTypes = Union[FileTypes, Sequence[FileTypes]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,14 +11,10 @@ class Record(BaseModel):
|
|||||||
|
|
||||||
def load(self, data: dict) -> None:
|
def load(self, data: dict) -> None:
|
||||||
super().load(data)
|
super().load(data)
|
||||||
|
self.expand = {}
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
key = camel_to_snake(key).replace("@", "")
|
key = camel_to_snake(key).replace("@", "")
|
||||||
setattr(self, key, value)
|
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
|
@classmethod
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -30,4 +30,4 @@ class BaseModel(ABC):
|
|||||||
@property
|
@property
|
||||||
def is_new(self) -> bool:
|
def is_new(self) -> bool:
|
||||||
"""Returns whether the current loaded data represent a stored db record."""
|
"""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
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from .admins import Admins, AdminAuthResponse
|
from .admin_service import AdminService, AdminAuthResponse
|
||||||
from .collections import Collections
|
from .collection_service import CollectionService
|
||||||
from .logs import Logs, HourlyStats
|
from .log_service import LogService, HourlyStats
|
||||||
from .realtime import Realtime
|
from .realtime_service import RealtimeService
|
||||||
from .records import Records
|
from .record_service import RecordService
|
||||||
from .settings import Settings
|
from .settings_service import SettingsService
|
||||||
from .users import Users, UserAuthResponse, AuthMethodsList, AuthProviderInfo
|
|
||||||
|
|||||||
@ -16,13 +16,45 @@ class AdminAuthResponse:
|
|||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
class Admins(CrudService):
|
class AdminService(CrudService):
|
||||||
def decode(self, data: dict) -> BaseModel:
|
def decode(self, data: dict) -> BaseModel:
|
||||||
return Admin(data)
|
return Admin(data)
|
||||||
|
|
||||||
def base_crud_path(self) -> str:
|
def base_crud_path(self) -> str:
|
||||||
return "/api/admins"
|
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:
|
def auth_response(self, response_data: dict) -> AdminAuthResponse:
|
||||||
"""Prepare successful authorize response."""
|
"""Prepare successful authorize response."""
|
||||||
admin = self.decode(response_data.pop("admin", {}))
|
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 = {}
|
self, email: str, password: str, body_params: dict = {}, query_params: dict = {}
|
||||||
) -> AdminAuthResponse:
|
) -> 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.
|
and returns a new admin token and data.
|
||||||
|
|
||||||
On success this method automatically updates the client's AuthStore 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)
|
return self.auth_response(response_data)
|
||||||
|
|
||||||
def refresh(
|
def authRefresh(
|
||||||
self, body_params: dict = {}, query_params: dict = {}
|
self, body_params: dict = {}, query_params: dict = {}
|
||||||
) -> AdminAuthResponse:
|
) -> AdminAuthResponse:
|
||||||
"""
|
"""
|
||||||
@ -63,7 +95,7 @@ class Admins(CrudService):
|
|||||||
"""
|
"""
|
||||||
return self.auth_response(
|
return self.auth_response(
|
||||||
self.client.send(
|
self.client.send(
|
||||||
self.base_crud_path() + "/refresh",
|
self.base_crud_path() + "/auth-refresh",
|
||||||
{"method": "POST", "params": query_params, "body": body_params},
|
{"method": "POST", "params": query_params, "body": body_params},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -5,7 +5,7 @@ from pocketbase.models.utils.base_model import BaseModel
|
|||||||
from pocketbase.models.collection import Collection
|
from pocketbase.models.collection import Collection
|
||||||
|
|
||||||
|
|
||||||
class Collections(CrudService):
|
class CollectionService(CrudService):
|
||||||
def decode(self, data: dict) -> BaseModel:
|
def decode(self, data: dict) -> BaseModel:
|
||||||
return Collection(data)
|
return Collection(data)
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class HourlyStats:
|
|||||||
date: Union[str, datetime.datetime]
|
date: Union[str, datetime.datetime]
|
||||||
|
|
||||||
|
|
||||||
class Logs(BaseService):
|
class LogService(BaseService):
|
||||||
def get_request_list(
|
def get_request_list(
|
||||||
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||||
) -> ListResult:
|
) -> ListResult:
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable, List
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ class MessageData:
|
|||||||
record: Record
|
record: Record
|
||||||
|
|
||||||
|
|
||||||
class Realtime(BaseService):
|
class RealtimeService(BaseService):
|
||||||
subscriptions: dict
|
subscriptions: dict
|
||||||
client_id: str = ""
|
client_id: str = ""
|
||||||
event_source: SSEClient | None = None
|
event_source: SSEClient | None = None
|
||||||
@ -40,28 +40,51 @@ class Realtime(BaseService):
|
|||||||
elif self.client_id:
|
elif self.client_id:
|
||||||
self._submit_subscriptions()
|
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.
|
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.
|
then the client will unsubscribe from all registered subscriptions.
|
||||||
|
|
||||||
The related sse connection will be autoclosed if after the
|
The related sse connection will be autoclosed if after the
|
||||||
unsubscribe operations there are no active subscriptions left.
|
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._remove_subscription_listeners()
|
||||||
self.subscriptions = {}
|
self.subscriptions = {}
|
||||||
elif subscription in self.subscriptions:
|
|
||||||
self.event_source.remove_event_listener(
|
|
||||||
subscription, self.subscriptions[subscription]
|
|
||||||
)
|
|
||||||
self.subscriptions.pop(subscription)
|
|
||||||
else:
|
else:
|
||||||
|
# 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
|
return
|
||||||
|
|
||||||
if self.client_id:
|
if self.client_id:
|
||||||
self._submit_subscriptions()
|
self._submit_subscriptions()
|
||||||
|
|
||||||
|
# no more subscriptions -> close the sse connection
|
||||||
if not self.subscriptions:
|
if not self.subscriptions:
|
||||||
self._disconnect()
|
self._disconnect()
|
||||||
|
|
||||||
275
pocketbase/services/record_service.py
Normal file
275
pocketbase/services/record_service.py
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -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
|
|
||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from pocketbase.services.utils.base_service import BaseService
|
from pocketbase.services.utils.base_service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseService):
|
class SettingsService(BaseService):
|
||||||
def get_all(self, query_params: dict = {}) -> dict:
|
def get_all(self, query_params: dict = {}) -> dict:
|
||||||
"""Fetch all available app settings."""
|
"""Fetch all available app settings."""
|
||||||
return self.client.send(
|
return self.client.send(
|
||||||
@ -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
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
from .base_crud_service import BaseCrudService
|
from .base_crud_service import BaseCrudService
|
||||||
from .base_service import BaseService
|
from .base_service import BaseService
|
||||||
from .crud_service import CrudService
|
from .crud_service import CrudService
|
||||||
from .sub_crud_service import SubCrudService
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from pocketbase.utils import ClientResponseError
|
||||||
|
|
||||||
from pocketbase.models.utils.base_model import BaseModel
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
from pocketbase.models.utils.list_result import ListResult
|
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(
|
def _create(
|
||||||
self, base_path: str, body_params: dict = {}, query_params: dict = {}
|
self, base_path: str, body_params: dict = {}, query_params: dict = {}
|
||||||
) -> BaseModel:
|
) -> BaseModel:
|
||||||
|
|||||||
@ -12,16 +12,31 @@ class CrudService(BaseCrudService, ABC):
|
|||||||
"""Base path for the crud actions (without trailing slash, eg. '/admins')."""
|
"""Base path for the crud actions (without trailing slash, eg. '/admins')."""
|
||||||
|
|
||||||
def get_full_list(
|
def get_full_list(
|
||||||
self, batch_size: int = 100, query_params: dict = {}
|
self, batch: int = 200, query_params: dict = {}
|
||||||
) -> list[BaseModel]:
|
) -> 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(
|
def get_list(
|
||||||
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||||
) -> ListResult:
|
) -> ListResult:
|
||||||
return self._get_list(self.base_crud_path(), page, per_page, query_params)
|
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:
|
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)
|
return self._get_one(self.base_crud_path(), id, query_params)
|
||||||
|
|
||||||
def create(self, body_params: dict = {}, query_params: dict = {}) -> BaseModel:
|
def create(self, body_params: dict = {}, query_params: dict = {}) -> BaseModel:
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
|
||||||
from pocketbase.models.admin import Admin
|
from pocketbase.models.admin import Admin
|
||||||
from pocketbase.models.user import User
|
from pocketbase.models.record import Record
|
||||||
|
|
||||||
|
|
||||||
class BaseAuthStore(ABC):
|
class BaseAuthStore(ABC):
|
||||||
@ -13,10 +13,10 @@ class BaseAuthStore(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base_token: str
|
base_token: str
|
||||||
base_model: User | Admin | None
|
base_model: Record | Admin | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, base_token: str = "", base_model: User | Admin | None = None
|
self, base_token: str = "", base_model: Record | Admin | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.base_token = base_token
|
self.base_token = base_token
|
||||||
@ -28,14 +28,15 @@ class BaseAuthStore(ABC):
|
|||||||
return self.base_token
|
return self.base_token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self) -> User | Admin | None:
|
def model(self) -> Record | Admin | None:
|
||||||
"""Retrieves the stored model data (if any)."""
|
"""Retrieves the stored model data (if any)."""
|
||||||
return self.base_model
|
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."""
|
"""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:
|
def clear(self) -> None:
|
||||||
"""Removes the stored token and model data form the auth store."""
|
"""Removes the stored token and model data form the auth store."""
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import pickle
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
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
|
from pocketbase.models.admin import Admin
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class LocalAuthStore(BaseAuthStore):
|
|||||||
filename: str = "pocketbase_auth.data",
|
filename: str = "pocketbase_auth.data",
|
||||||
filepath: str = "",
|
filepath: str = "",
|
||||||
base_token: str = "",
|
base_token: str = "",
|
||||||
base_model: User | Admin | None = None,
|
base_model: Record | Admin | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(base_token, base_model)
|
super().__init__(base_token, base_model)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
@ -33,13 +33,13 @@ class LocalAuthStore(BaseAuthStore):
|
|||||||
return data["token"]
|
return data["token"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self) -> User | Admin | None:
|
def model(self) -> Record | Admin | None:
|
||||||
data = self._storage_get(self.complete_filepath)
|
data = self._storage_get(self.complete_filepath)
|
||||||
if not data or "model" not in data:
|
if not data or "model" not in data:
|
||||||
return None
|
return None
|
||||||
return data["model"]
|
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})
|
self._storage_set(self.complete_filepath, {"token": token, "model": model})
|
||||||
super().save(token, model)
|
super().save(token, model)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def camel_to_snake(name: str) -> str:
|
def camel_to_snake(name: str) -> str:
|
||||||
@ -17,3 +18,19 @@ def to_datetime(
|
|||||||
return datetime.datetime.strptime(str_datetime, format)
|
return datetime.datetime.strptime(str_datetime, format)
|
||||||
except Exception:
|
except Exception:
|
||||||
return str_datetime
|
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)
|
||||||
|
|||||||
202
poetry.lock
generated
202
poetry.lock
generated
@ -1,6 +1,6 @@
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "3.6.1"
|
version = "3.6.2"
|
||||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -14,7 +14,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "attrs"
|
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"]
|
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"]
|
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 = ["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]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "22.8.0"
|
version = "22.10.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.2"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = ">=8.0.0"
|
click = ">=8.0.0"
|
||||||
@ -75,25 +75,22 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.5"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
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]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "exceptiongroup"
|
||||||
version = "6.4.4"
|
version = "1.0.1"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flake8"
|
name = "flake8"
|
||||||
@ -224,15 +221,15 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "2.5.2"
|
version = "2.5.3"
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
|
||||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
@ -249,14 +246,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
|||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
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]]
|
[[package]]
|
||||||
name = "pycodestyle"
|
name = "pycodestyle"
|
||||||
version = "2.9.1"
|
version = "2.9.1"
|
||||||
@ -286,7 +275,7 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.1.3"
|
version = "7.2.0"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -295,31 +284,16 @@ python-versions = ">=3.7"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
attrs = ">=19.2.0"
|
attrs = ">=19.2.0"
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
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\""}
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
iniconfig = "*"
|
iniconfig = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pluggy = ">=0.12,<2.0"
|
pluggy = ">=0.12,<2.0"
|
||||||
py = ">=1.8.2"
|
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||||
tomli = ">=1.0.0"
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
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]]
|
[[package]]
|
||||||
name = "rfc3986"
|
name = "rfc3986"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -360,7 +334,7 @@ python-versions = ">=3.6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.3.0"
|
version = "4.4.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -368,54 +342,52 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zipp"
|
name = "zipp"
|
||||||
version = "3.8.1"
|
version = "3.10.0"
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
|
||||||
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)"]
|
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]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
content-hash = "78af276171c1e41e0cb3205383b288b9ed15cb8fdf68e7184954909e8b07c7c6"
|
content-hash = "8f9534771a19adba002263f1c9c563e5d1bd7e1f134fd42aa85689b614fd6e0a"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
anyio = [
|
anyio = [
|
||||||
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||||
]
|
]
|
||||||
attrs = [
|
attrs = [
|
||||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||||
]
|
]
|
||||||
black = [
|
black = [
|
||||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
|
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
|
||||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
|
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
|
||||||
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
|
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
|
||||||
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
|
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
|
||||||
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
|
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
|
||||||
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
|
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
|
||||||
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
|
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
|
||||||
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
|
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
|
||||||
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
|
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
|
||||||
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
|
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
|
||||||
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
|
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
|
||||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
|
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
|
||||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
|
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
|
||||||
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
|
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
|
||||||
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
|
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
|
||||||
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
|
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
|
||||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
|
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
|
||||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
|
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
|
||||||
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
|
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
|
||||||
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
|
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
||||||
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
|
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
||||||
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
|
|
||||||
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
|
|
||||||
]
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
{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"},
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
]
|
]
|
||||||
colorama = [
|
colorama = [
|
||||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
coverage = [
|
exceptiongroup = [
|
||||||
{file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"},
|
{file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
|
||||||
{file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"},
|
{file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
|
||||||
{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"},
|
|
||||||
]
|
]
|
||||||
flake8 = [
|
flake8 = [
|
||||||
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
|
{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"},
|
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||||
]
|
]
|
||||||
platformdirs = [
|
platformdirs = [
|
||||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
{file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"},
|
||||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||||
]
|
]
|
||||||
pluggy = [
|
pluggy = [
|
||||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
{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 = [
|
pycodestyle = [
|
||||||
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
|
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
|
||||||
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
|
{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"},
|
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||||
]
|
]
|
||||||
pytest = [
|
pytest = [
|
||||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||||
]
|
|
||||||
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"},
|
|
||||||
]
|
]
|
||||||
rfc3986 = [
|
rfc3986 = [
|
||||||
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
{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"},
|
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||||
]
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||||
]
|
]
|
||||||
zipp = [
|
zipp = [
|
||||||
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
|
{file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
|
||||||
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
|
{file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,6 +5,7 @@ requires-python = ">=3.7"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Vithor Jaeger", email = "vaphes@gmail.com" },
|
{ name = "Vithor Jaeger", email = "vaphes@gmail.com" },
|
||||||
|
{ name = "Max Amling", email = "max-amling@web.de" },
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user