Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99aefa0cda | ||
|
|
9ff559b85e | ||
|
|
99fccb3b0e | ||
|
|
c0afb20503 | ||
|
|
90bd223664 | ||
|
|
aacd12bafe | ||
|
|
a690d451ab | ||
|
|
d320125c58 | ||
|
|
efe4bd8f67 | ||
|
|
f953d6723c | ||
|
|
cf40d82d28 | ||
|
|
7bb6e97880 | ||
|
|
c8297852ce | ||
|
|
9ae4712c29 | ||
|
|
129b42613d | ||
|
|
f917b60ea0 |
39
.github/workflows/python-publish.yml
vendored
Normal file
39
.github/workflows/python-publish.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# This workflow will upload a Python Package using Twine when a release is created
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install build
|
||||||
|
- name: Build package
|
||||||
|
run: python -m build
|
||||||
|
- name: Publish package
|
||||||
|
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
name: Python 3.7-3.10
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -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
|
||||||
|
|||||||
77
README.md
77
README.md
@ -1,6 +1,6 @@
|
|||||||
# PocketBase Python SDK
|
# PocketBase Python SDK
|
||||||
|
|
||||||
[](https://github.com/vaphes/pocketbase/actions/workflows/python-versions.yml)
|
[](https://github.com/vaphes/pocketbase/actions/workflows/tests.yml)
|
||||||
|
|
||||||
Python client SDK for the <a href="https://pocketbase.io/">PocketBase</a> backend.
|
Python client SDK for the <a href="https://pocketbase.io/">PocketBase</a> backend.
|
||||||
|
|
||||||
@ -8,10 +8,79 @@ This is in early development, and at first is just a translation of <a href="htt
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Install PocketBase using pip:
|
## Installation
|
||||||
|
|
||||||
|
Install PocketBase using PIP:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ pip install pocketbase
|
python3 -m pip install pocketbase
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"><i>The PocketBase Python SDK is <a href="https://github.com/vaphes/pocketbase/blob/master/LICENCE.txt">MIT licensed</a> code.</p>
|
## Usage
|
||||||
|
|
||||||
|
The rule of thumb here is just to use it as you would <a href="https://github.com/pocketbase/js-sdk">the javascript lib</a>, but in a pythonic way of course!
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pocketbase import PocketBase # Client also works the same
|
||||||
|
from pocketbase.client import FileUpload
|
||||||
|
|
||||||
|
client = PocketBase('http://127.0.0.1:8090')
|
||||||
|
|
||||||
|
# authenticate as regular user
|
||||||
|
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", "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' 🙃.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
These are the requirements for local development:
|
||||||
|
|
||||||
|
* Python 3.7+
|
||||||
|
* Poetry (https://python-poetry.org/)
|
||||||
|
|
||||||
|
You can install locally:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
Or can build and generate a package:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
poetry build
|
||||||
|
```
|
||||||
|
|
||||||
|
But if you are using only PIP, use this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python3 -m pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
To execute the tests use this command:
|
||||||
|
|
||||||
|
```
|
||||||
|
poetry run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The PocketBase Python SDK is <a href="https://github.com/vaphes/pocketbase/blob/master/LICENCE.txt">MIT licensed</a> code.
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
__title__ = "pocketbase"
|
__title__ = "pocketbase"
|
||||||
__description__ = "PocketBase client SDK for python."
|
__description__ = "PocketBase client SDK for python."
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.8.1"
|
||||||
|
|
||||||
|
|
||||||
from .client import Client, ClientResponseError
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
|
class PocketBase(Client):
|
||||||
|
"""
|
||||||
|
Proxy class for `Client`
|
||||||
|
|
||||||
|
This is for cosmetic reasons only as you can use the
|
||||||
|
`Client` class just the same
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|||||||
@ -1,64 +1,59 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Dict
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from pocketbase.services.admins import Admins
|
from pocketbase.models import FileUpload
|
||||||
from pocketbase.services.collections import Collections
|
from pocketbase.models.record import Record
|
||||||
from pocketbase.services.logs import Logs
|
from pocketbase.services.admin_service import AdminService
|
||||||
from pocketbase.services.realtime import Realtime
|
from pocketbase.services.collection_service import CollectionService
|
||||||
from pocketbase.services.records import Records
|
from pocketbase.services.log_service import LogService
|
||||||
from pocketbase.services.users import Users
|
from pocketbase.services.realtime_service import RealtimeService
|
||||||
from pocketbase.services.settings import Settings
|
from pocketbase.services.record_service import RecordService
|
||||||
|
from pocketbase.services.settings_service import SettingsService
|
||||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||||
|
from pocketbase.utils import ClientResponseError
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
base_url: str = "/",
|
base_url: str = "/",
|
||||||
lang: str = "en-US",
|
lang: str = "en-US",
|
||||||
auth_store: BaseAuthStore | None = None,
|
auth_store: BaseAuthStore | None = None,
|
||||||
|
timeout: float = 120,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.lang = lang
|
self.lang = lang
|
||||||
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
||||||
|
self.timeout = timeout
|
||||||
# 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."""
|
||||||
@ -66,15 +61,10 @@ class Client:
|
|||||||
config.update(req_config)
|
config.update(req_config)
|
||||||
# check if Authorization header can be added
|
# check if Authorization header can be added
|
||||||
if self.auth_store.token and (
|
if self.auth_store.token and (
|
||||||
not "headers" in config or "Authorization" not in config["headers"]
|
"headers" not in config or "Authorization" not in config["headers"]
|
||||||
):
|
):
|
||||||
auth_type = "Admin"
|
|
||||||
if hasattr(self.auth_store.model, "verified"):
|
|
||||||
auth_type = "User"
|
|
||||||
config["headers"] = config.get("headers", {})
|
config["headers"] = config.get("headers", {})
|
||||||
config["headers"].update(
|
config["headers"].update({"Authorization": self.auth_store.token})
|
||||||
{"Authorization": f"{auth_type} {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
|
||||||
@ -82,6 +72,21 @@ class Client:
|
|||||||
params = config.get("params", None)
|
params = config.get("params", None)
|
||||||
headers = config.get("headers", None)
|
headers = config.get("headers", None)
|
||||||
body = config.get("body", None)
|
body = config.get("body", None)
|
||||||
|
# handle requests including files as multipart:
|
||||||
|
data = {}
|
||||||
|
files = ()
|
||||||
|
for k, v in (body if isinstance(body, dict) else {}).items():
|
||||||
|
if isinstance(v, FileUpload):
|
||||||
|
files += v.get(k)
|
||||||
|
else:
|
||||||
|
data[k] = v
|
||||||
|
if len(files) > 0:
|
||||||
|
# discard body, switch to multipart encoding
|
||||||
|
body = None
|
||||||
|
else:
|
||||||
|
# discard files+data (do not use multipart encoding)
|
||||||
|
files = None
|
||||||
|
data = None
|
||||||
try:
|
try:
|
||||||
response = httpx.request(
|
response = httpx.request(
|
||||||
method=method,
|
method=method,
|
||||||
@ -89,7 +94,9 @@ class Client:
|
|||||||
params=params,
|
params=params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=body,
|
json=body,
|
||||||
timeout=120,
|
data=data,
|
||||||
|
files=files,
|
||||||
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ClientResponseError(
|
raise ClientResponseError(
|
||||||
@ -109,6 +116,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,4 +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
|
||||||
|
|||||||
@ -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", "")
|
||||||
|
|||||||
14
pocketbase/models/file_upload.py
Normal file
14
pocketbase/models/file_upload.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from httpx._types import FileTypes
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
FileUploadTypes = Union[FileTypes, Sequence[FileTypes]]
|
||||||
|
|
||||||
|
|
||||||
|
class FileUpload:
|
||||||
|
def __init__(self, *args):
|
||||||
|
self.files: FileUploadTypes = args
|
||||||
|
|
||||||
|
def get(self, key: str):
|
||||||
|
if isinstance(self.files[0], Sequence):
|
||||||
|
return tuple((key, i) for i in self.files)
|
||||||
|
return ((key, self.files),)
|
||||||
@ -2,24 +2,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pocketbase.models.utils.base_model import BaseModel
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
from pocketbase.utils import camel_to_snake
|
from pocketbase.utils import camel_to_snake
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class Record(BaseModel):
|
class Record(BaseModel):
|
||||||
collection_id: str
|
collection_id: str
|
||||||
collection_name: str
|
collection_name: str = ""
|
||||||
expand: dict
|
expand: dict = field(default_factory=dict)
|
||||||
|
|
||||||
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.load_expanded()
|
||||||
self.collection_name = data.get("@collectionName", "")
|
|
||||||
expand = data.get("@expand", {})
|
|
||||||
if expand:
|
|
||||||
self.expand = expand
|
|
||||||
self.load_expanded()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_expanded(cls, data: dict):
|
def parse_expanded(cls, data: dict):
|
||||||
|
|||||||
@ -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", {}))
|
||||||
@ -31,18 +63,18 @@ class Admins(CrudService):
|
|||||||
self.client.auth_store.save(token, admin)
|
self.client.auth_store.save(token, admin)
|
||||||
return AdminAuthResponse(token=token, admin=admin, **response_data)
|
return AdminAuthResponse(token=token, admin=admin, **response_data)
|
||||||
|
|
||||||
def auth_via_email(
|
def auth_with_password(
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
body_params.update({"email": email, "password": password})
|
body_params.update({"identity": email, "password": password})
|
||||||
response_data = self.client.send(
|
response_data = self.client.send(
|
||||||
self.base_crud_path() + "/auth-via-email",
|
self.base_crud_path() + "/auth-with-password",
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": query_params,
|
"params": query_params,
|
||||||
@ -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:
|
||||||
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:
|
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-oauth2",
|
||||||
|
{
|
||||||
|
"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
|
||||||
@ -28,18 +28,18 @@ class LocalAuthStore(BaseAuthStore):
|
|||||||
@property
|
@property
|
||||||
def token(self) -> str:
|
def token(self) -> str:
|
||||||
data = self._storage_get(self.complete_filepath)
|
data = self._storage_get(self.complete_filepath)
|
||||||
if not data or not "token" in data:
|
if not data or "token" not in data:
|
||||||
return None
|
return None
|
||||||
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 not "model" 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)
|
||||||
|
|||||||
307
poetry.lock
generated
307
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
|
||||||
@ -9,19 +9,12 @@ python-versions = ">=3.6.2"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
idna = ">=2.8"
|
idna = ">=2.8"
|
||||||
sniffio = ">=1.1"
|
sniffio = ">=1.1"
|
||||||
|
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]]
|
|
||||||
name = "atomicwrites"
|
|
||||||
version = "1.4.1"
|
|
||||||
description = "Atomic file writes."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
@ -35,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"
|
||||||
@ -51,6 +44,8 @@ mypy-extensions = ">=0.4.3"
|
|||||||
pathspec = ">=0.9.0"
|
pathspec = ">=0.9.0"
|
||||||
platformdirs = ">=2"
|
platformdirs = ">=2"
|
||||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||||
|
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
|
||||||
|
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
@ -60,7 +55,7 @@ uvloop = ["uvloop (>=0.15.2)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2022.6.15.2"
|
version = "2022.9.24"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -76,14 +71,40 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
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]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.0.1"
|
||||||
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "5.0.4"
|
||||||
|
description = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""}
|
||||||
|
mccabe = ">=0.7.0,<0.8.0"
|
||||||
|
pycodestyle = ">=2.9.0,<2.10.0"
|
||||||
|
pyflakes = ">=2.5.0,<2.6.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
@ -140,12 +161,36 @@ optional = false
|
|||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "more-itertools"
|
name = "importlib-metadata"
|
||||||
version = "8.14.0"
|
version = "4.2.0"
|
||||||
description = "More routines for operating on iterables, beyond itertools"
|
description = "Read metadata from Python packages"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"]
|
||||||
|
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "1.1.1"
|
||||||
|
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
@ -176,34 +221,46 @@ 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"
|
||||||
version = "0.13.1"
|
version = "1.0.0"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "pycodestyle"
|
||||||
version = "1.11.0"
|
version = "2.9.1"
|
||||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
description = "Python style guide checker"
|
||||||
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.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "2.5.0"
|
||||||
|
description = "passive checker of Python programs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
@ -218,25 +275,24 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "5.4.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
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
attrs = ">=19.2.0"
|
||||||
attrs = ">=17.4.0"
|
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
more-itertools = ">=4.0.0"
|
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
iniconfig = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pluggy = ">=0.12,<1.0"
|
pluggy = ">=0.12,<2.0"
|
||||||
py = ">=1.5.0"
|
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||||
wcwidth = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
checkqa-mypy = ["mypy (==v0.761)"]
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfc3986"
|
name = "rfc3986"
|
||||||
@ -269,66 +325,89 @@ optional = false
|
|||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wcwidth"
|
name = "typed-ast"
|
||||||
version = "0.2.5"
|
version = "1.5.4"
|
||||||
description = "Measures the displayed width of unicode strings in a terminal"
|
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.4.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
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 = ["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]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.7"
|
||||||
content-hash = "8e1b477e300709c958c814a3937ee98aa175988857e7452bc5e3e2b97f58f7ca"
|
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"},
|
||||||
]
|
|
||||||
atomicwrites = [
|
|
||||||
{file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
|
|
||||||
]
|
]
|
||||||
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.6.15.2-py3-none-any.whl", hash = "sha256:0aa1a42fbd57645fabeb6290a7687c21755b0344ecaeaa05f4e9f6207ae2e9a8"},
|
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||||
{file = "certifi-2022.6.15.2.tar.gz", hash = "sha256:aa08c101214127b9b0472ca6338315113c9487d45376fd3e669201b477c71003"},
|
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||||
]
|
]
|
||||||
click = [
|
click = [
|
||||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
{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"},
|
||||||
|
]
|
||||||
|
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"},
|
||||||
|
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
|
||||||
]
|
]
|
||||||
h11 = [
|
h11 = [
|
||||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||||
@ -346,9 +425,17 @@ idna = [
|
|||||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||||
]
|
]
|
||||||
more-itertools = [
|
importlib-metadata = [
|
||||||
{file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"},
|
{file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
|
||||||
{file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"},
|
{file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"},
|
||||||
|
]
|
||||||
|
iniconfig = [
|
||||||
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
]
|
||||||
|
mccabe = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
]
|
]
|
||||||
mypy-extensions = [
|
mypy-extensions = [
|
||||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
@ -363,24 +450,28 @@ 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-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||||
]
|
]
|
||||||
py = [
|
pycodestyle = [
|
||||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
|
||||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
|
||||||
|
]
|
||||||
|
pyflakes = [
|
||||||
|
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
|
||||||
|
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
|
||||||
]
|
]
|
||||||
pyparsing = [
|
pyparsing = [
|
||||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||||
]
|
]
|
||||||
pytest = [
|
pytest = [
|
||||||
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
|
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||||
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
|
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||||
]
|
]
|
||||||
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"},
|
||||||
@ -394,7 +485,37 @@ tomli = [
|
|||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
]
|
]
|
||||||
wcwidth = [
|
typed-ast = [
|
||||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
||||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||||
|
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{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.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
|
||||||
|
{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",
|
||||||
@ -28,7 +29,7 @@ dynamic = ["readme", "version"]
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pocketbase"
|
name = "pocketbase"
|
||||||
version = "0.2.0"
|
version = "0.8.1"
|
||||||
description = "PocketBase SDK for python."
|
description = "PocketBase SDK for python."
|
||||||
authors = ["Vithor Jaeger <vaphes@gmail.com>"]
|
authors = ["Vithor Jaeger <vaphes@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -43,8 +44,9 @@ keywords = ["pocketbase", "sdk"]
|
|||||||
python = "^3.7"
|
python = "^3.7"
|
||||||
httpx = "^0.23.0"
|
httpx = "^0.23.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^5.2"
|
flake8 = "^5.0.4"
|
||||||
|
pytest = "^7.1.3"
|
||||||
black = {version = "^22.8.0", allow-prereleases = true}
|
black = {version = "^22.8.0", allow-prereleases = true}
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
from pocketbase import __version__
|
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
|
||||||
assert __version__ == "0.2.0"
|
|
||||||
18
tests/test_utils.py
Normal file
18
tests/test_utils.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from pocketbase import __version__
|
||||||
|
from pocketbase.utils import camel_to_snake, to_datetime
|
||||||
|
|
||||||
|
|
||||||
|
def test_version():
|
||||||
|
assert __version__ == "0.8.1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_utils():
|
||||||
|
assert camel_to_snake("TestCase") == "test_case"
|
||||||
|
assert camel_to_snake("test_case") == "test_case"
|
||||||
|
assert camel_to_snake("TestBS123") == "test_bs123"
|
||||||
|
assert to_datetime("2022-01-31 12:01:05") == datetime.datetime(
|
||||||
|
2022, 1, 31, 12, 1, 5
|
||||||
|
)
|
||||||
|
assert isinstance(to_datetime("2022-01-31"), str)
|
||||||
Loading…
x
Reference in New Issue
Block a user