Compare commits
40 Commits
pre-releas
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99aefa0cda | ||
|
|
9ff559b85e | ||
|
|
99fccb3b0e | ||
|
|
c0afb20503 | ||
|
|
90bd223664 | ||
|
|
aacd12bafe | ||
|
|
a690d451ab | ||
|
|
d320125c58 | ||
|
|
efe4bd8f67 | ||
|
|
f953d6723c | ||
|
|
cf40d82d28 | ||
|
|
7bb6e97880 | ||
|
|
c8297852ce | ||
|
|
9ae4712c29 | ||
|
|
129b42613d | ||
|
|
f917b60ea0 | ||
|
|
7882598922 | ||
|
|
85c818539a | ||
|
|
78010eafcf | ||
|
|
8363b315c3 | ||
|
|
976c7e580f | ||
|
|
c66e6cc66b | ||
|
|
cadd4889b6 | ||
|
|
1d92b331e2 | ||
|
|
c740884f5f | ||
|
|
58f9117df6 | ||
|
|
aa1684b9c4 | ||
|
|
115f8c42ec | ||
|
|
2b9d0e1212 | ||
|
|
4688b5bfd9 | ||
|
|
892b68f0aa | ||
|
|
9b2902e747 | ||
|
|
3bcfa1bd1c | ||
|
|
afe03f72f0 | ||
|
|
3f8d2bb108 | ||
|
|
0efe5c901e | ||
|
|
561b4db23b | ||
|
|
34a6e3b2f1 | ||
|
|
4b32c0133c | ||
|
|
a57d0e9efe |
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 }}
|
||||
39
.github/workflows/tests.yml
vendored
Normal file
39
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# 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
|
||||
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
21
LICENCE.txt
Normal file
21
LICENCE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 vaphes
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
87
README.md
87
README.md
@ -1,5 +1,86 @@
|
||||
# pocketbase
|
||||
# PocketBase Python SDK
|
||||
|
||||
Python client for PocketBase database.
|
||||
[](https://github.com/vaphes/pocketbase/actions/workflows/tests.yml)
|
||||
|
||||
This is in early development, and at first is just a translations for the javascript lib.
|
||||
Python client SDK for the <a href="https://pocketbase.io/">PocketBase</a> backend.
|
||||
|
||||
This is in early development, and at first is just a translation of <a href="https://github.com/pocketbase/js-sdk">the javascript lib</a> using <a href="https://github.com/encode/httpx/">HTTPX</a>.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Install PocketBase using PIP:
|
||||
|
||||
```shell
|
||||
python3 -m pip install pocketbase
|
||||
```
|
||||
|
||||
## 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,3 +1,17 @@
|
||||
__version__ = "0.1.0"
|
||||
__title__ = "pocketbase"
|
||||
__description__ = "PocketBase client SDK for python."
|
||||
__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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,71 +1,59 @@
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from pocketbase.services.admins import Admins
|
||||
from pocketbase.services.collections import Collections
|
||||
from pocketbase.services.logs import Logs
|
||||
from pocketbase.services.realtime import Realtime
|
||||
from pocketbase.services.records import Records
|
||||
from pocketbase.services.users import Users
|
||||
from pocketbase.services.settings import Settings
|
||||
from pocketbase.models import FileUpload
|
||||
from pocketbase.models.record import Record
|
||||
from pocketbase.services.admin_service import AdminService
|
||||
from pocketbase.services.collection_service import CollectionService
|
||||
from pocketbase.services.log_service import LogService
|
||||
from pocketbase.services.realtime_service import RealtimeService
|
||||
from pocketbase.services.record_service import RecordService
|
||||
from pocketbase.services.settings_service import SettingsService
|
||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||
|
||||
# from pocketbase.stores.local_auth_store import LocalAuthStore
|
||||
|
||||
|
||||
class ClientResponseError(Exception):
|
||||
url: str = ""
|
||||
status: int = 0
|
||||
data: dict = {}
|
||||
is_abort: bool = False
|
||||
original_error: Any = None
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args)
|
||||
self.url = kwargs.get("url", "")
|
||||
self.status = kwargs.get("status", 0)
|
||||
self.data = kwargs.get("data", {})
|
||||
self.is_abort = kwargs.get("is_abort", False)
|
||||
self.original_error = kwargs.get("original_error", None)
|
||||
from pocketbase.utils import ClientResponseError
|
||||
|
||||
|
||||
class Client:
|
||||
base_url: str
|
||||
lang: str
|
||||
auth_store: BaseAuthStore
|
||||
settings: Settings
|
||||
admins: Admins
|
||||
users: Users
|
||||
collections: Collections
|
||||
records: Records
|
||||
logs: Logs
|
||||
realtime: Realtime
|
||||
settings: SettingsService
|
||||
admins: AdminService
|
||||
records: Record
|
||||
collections: CollectionService
|
||||
records: RecordService
|
||||
logs: LogService
|
||||
realtime: RealtimeService
|
||||
record_service: Dict[str, RecordService]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = "/",
|
||||
lang: str = "en-US",
|
||||
auth_store: BaseAuthStore = None,
|
||||
auth_store: BaseAuthStore | None = None,
|
||||
timeout: float = 120,
|
||||
) -> None:
|
||||
self.base_url = base_url
|
||||
self.lang = lang
|
||||
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
||||
self.timeout = timeout
|
||||
# services
|
||||
self.admins = Admins(self)
|
||||
self.users = Users(self)
|
||||
self.records = Records(self)
|
||||
self.collections = Collections(self)
|
||||
self.logs = Logs(self)
|
||||
self.settings = Settings(self)
|
||||
self.realtime = Realtime(self)
|
||||
self.admins = AdminService(self)
|
||||
self.collections = CollectionService(self)
|
||||
self.logs = LogService(self)
|
||||
self.settings = SettingsService(self)
|
||||
self.realtime = RealtimeService(self)
|
||||
self.record_service = {}
|
||||
|
||||
def cancel_request(self, cancel_key: str):
|
||||
return self
|
||||
|
||||
def cancel_all_requests(self):
|
||||
return 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:
|
||||
"""Sends an api http request."""
|
||||
@ -73,15 +61,10 @@ class Client:
|
||||
config.update(req_config)
|
||||
# check if Authorization header can be added
|
||||
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"].update(
|
||||
{"Authorization": f"{auth_type} {self.auth_store.token}"}
|
||||
)
|
||||
config["headers"].update({"Authorization": self.auth_store.token})
|
||||
# build url + path
|
||||
url = self.build_url(path)
|
||||
# send the request
|
||||
@ -89,6 +72,21 @@ class Client:
|
||||
params = config.get("params", None)
|
||||
headers = config.get("headers", 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:
|
||||
response = httpx.request(
|
||||
method=method,
|
||||
@ -96,7 +94,9 @@ class Client:
|
||||
params=params,
|
||||
headers=headers,
|
||||
json=body,
|
||||
timeout=120,
|
||||
data=data,
|
||||
files=files,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ClientResponseError(
|
||||
@ -116,6 +116,21 @@ class Client:
|
||||
)
|
||||
return data
|
||||
|
||||
def get_file_url(self, record: Record, filename: str, query_params: dict):
|
||||
parts = [
|
||||
"api",
|
||||
"files",
|
||||
quote(record.collection_id or record.collection_name),
|
||||
quote(record.id),
|
||||
quote(filename),
|
||||
]
|
||||
result = self.build_url("/".join(parts))
|
||||
if len(query_params) != 0:
|
||||
params: str = urlencode(query_params)
|
||||
result += "&" if "?" in result else "?"
|
||||
result += params
|
||||
return result
|
||||
|
||||
def build_url(self, path: str) -> str:
|
||||
url = self.base_url
|
||||
if not self.base_url.endswith("/"):
|
||||
@ -123,22 +138,3 @@ class Client:
|
||||
if path.startswith("/"):
|
||||
path = path[1:]
|
||||
return url + path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pocketbase.stores.local_auth_store import LocalAuthStore
|
||||
|
||||
pb = Client(base_url="http://ares.olimpo:8090/", auth_store=LocalAuthStore())
|
||||
# pb.admins.auth_via_email("vaphes@gmail.com", "vaphes2007")
|
||||
print(pb.auth_store.token)
|
||||
books = pb.collections.get_one("books")
|
||||
print("ok")
|
||||
# sacd = "nwvgaw6iiibv4fp"
|
||||
# book = {
|
||||
# "author": sacd,
|
||||
# "name": "A study in red",
|
||||
# "rating": 4.5,
|
||||
# "summary": "The worst Sherlock Homes book",
|
||||
# }
|
||||
# data = pb.records.create("books", book)
|
||||
# print(data)
|
||||
|
||||
@ -3,4 +3,4 @@ from .collection import Collection
|
||||
from .external_auth import ExternalAuth
|
||||
from .log_request import LogRequest
|
||||
from .record import Record
|
||||
from .user import User
|
||||
from .file_upload import FileUpload
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
from typing import Any, Union
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from pocketbase.utils import to_datetime
|
||||
@ -8,10 +9,8 @@ from pocketbase.models.utils.base_model import BaseModel
|
||||
class Admin(BaseModel):
|
||||
avatar: int
|
||||
email: str
|
||||
last_reset_sent_at: Union[str, datetime.datetime]
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def load(self, data: dict) -> None:
|
||||
super().load(data)
|
||||
self.avatar = data.get("avatar", 0)
|
||||
self.email = data.get("email", "")
|
||||
self.last_reset_sent_at = to_datetime(data.get("lastResetSentAt", ""))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any, Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
from pocketbase.models.utils.schema_field import SchemaField
|
||||
@ -6,24 +6,41 @@ from pocketbase.models.utils.schema_field import SchemaField
|
||||
|
||||
class Collection(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
schema: list[SchemaField]
|
||||
system: bool
|
||||
list_rule: Optional[str]
|
||||
view_rule: Optional[str]
|
||||
create_rule: Optional[str]
|
||||
update_rule: Optional[str]
|
||||
delete_rule: Optional[str]
|
||||
list_rule: str | None
|
||||
view_rule: str | None
|
||||
create_rule: str | None
|
||||
update_rule: str | None
|
||||
delete_rule: str | None
|
||||
options: dict
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def load(self, data: dict) -> None:
|
||||
super().load(data)
|
||||
self.name = data.get("name", "")
|
||||
self.system = data.get("system", False)
|
||||
self.type = data.get("type", "base")
|
||||
self.options = data.get("options", {})
|
||||
|
||||
# rules
|
||||
self.list_rule = data.get("listRule", None)
|
||||
self.view_rule = data.get("viewRule", None)
|
||||
self.create_rule = data.get("createRule", None)
|
||||
self.update_rule = data.get("updateRule", None)
|
||||
self.delete_rule = data.get("deleteRule", "")
|
||||
|
||||
# schema
|
||||
schema = data.get("schema", [])
|
||||
self.schema = []
|
||||
for field in schema:
|
||||
self.schema.append(SchemaField(**field))
|
||||
|
||||
def is_base(self):
|
||||
return self.type == "base"
|
||||
|
||||
def is_auth(self):
|
||||
return self.type == "auth"
|
||||
|
||||
def is_single(self):
|
||||
return self.type == "single"
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
|
||||
|
||||
class ExternalAuth(BaseModel):
|
||||
user_id: str
|
||||
record_id: str
|
||||
collection_id: str
|
||||
provider: str
|
||||
provider_id: str
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def load(self, data: dict) -> None:
|
||||
super().load(data)
|
||||
self.user_id = data.get("userId", "")
|
||||
self.record_id = data.get("recordId", "")
|
||||
self.collection_id = data.get("collectionId", "")
|
||||
self.provider = data.get("provider", "")
|
||||
self.provider_id = data.get("providerId", "")
|
||||
|
||||
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),)
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
|
||||
@ -12,9 +12,9 @@ class LogRequest(BaseModel):
|
||||
user_ip: str
|
||||
referer: str
|
||||
user_agent: str
|
||||
meta: dict[str:Any]
|
||||
meta: dict
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def load(self, data: dict) -> None:
|
||||
super().load(data)
|
||||
self.url = data.get("url", "")
|
||||
self.method = data.get("method", "")
|
||||
|
||||
@ -1,28 +1,25 @@
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
from pocketbase.utils import camel_to_snake
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Record(BaseModel):
|
||||
collection_id: str
|
||||
collection_name: str
|
||||
expand: dict[str:Any]
|
||||
collection_name: str = ""
|
||||
expand: dict = field(default_factory=dict)
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def load(self, data: dict) -> None:
|
||||
super().load(data)
|
||||
self.expand = {}
|
||||
for key, value in data.items():
|
||||
key = camel_to_snake(key).replace("@", "")
|
||||
setattr(self, key, value)
|
||||
self.collection_id = data.get("@collectionId", "")
|
||||
self.collection_name = data.get("@collectionName", "")
|
||||
expand = data.get("@expand", {})
|
||||
if expand:
|
||||
self.expand = expand
|
||||
self.load_expanded()
|
||||
|
||||
@classmethod
|
||||
def parse_expanded(cls, data: dict[str:Any]):
|
||||
def parse_expanded(cls, data: dict):
|
||||
return cls(data)
|
||||
|
||||
def load_expanded(self) -> None:
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
from typing import Any, 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: Union[str, datetime.datetime]
|
||||
last_verification_sent_at: Union[str, datetime.datetime]
|
||||
profile: Optional[Record]
|
||||
|
||||
def load(self, data: dict[str:Any]) -> 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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import Any, Union
|
||||
import datetime
|
||||
|
||||
from pocketbase.utils import to_datetime
|
||||
@ -7,14 +8,20 @@ from pocketbase.utils import to_datetime
|
||||
|
||||
class BaseModel(ABC):
|
||||
id: str
|
||||
created: Union[str, datetime.datetime]
|
||||
updated: Union[str, datetime.datetime]
|
||||
created: str | datetime.datetime
|
||||
updated: str | datetime.datetime
|
||||
|
||||
def __init__(self, data: dict[str:Any] = {}) -> None:
|
||||
def __init__(self, data: dict = {}) -> None:
|
||||
super().__init__()
|
||||
self.load(data)
|
||||
|
||||
def load(self, data: dict[str:Any]) -> None:
|
||||
def __str__(self) -> str:
|
||||
return f"<{self.__class__.__name__}: {self.id}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def load(self, data: dict) -> None:
|
||||
"""Loads `data` into the current model."""
|
||||
self.id = data.pop("id", "")
|
||||
self.created = to_datetime(data.pop("created", ""))
|
||||
@ -23,4 +30,4 @@ class BaseModel(ABC):
|
||||
@property
|
||||
def is_new(self) -> bool:
|
||||
"""Returns whether the current loaded data represent a stored db record."""
|
||||
return not self.id or self.id == "00000000-0000-0000-0000-000000000000"
|
||||
return not self.id
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -10,4 +11,4 @@ class SchemaField:
|
||||
system: bool = False
|
||||
required: bool = False
|
||||
unique: bool = False
|
||||
options: dict[str:Any] = field(default_factory=dict)
|
||||
options: dict = field(default_factory=dict)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from .admins import Admins, AdminAuthResponse
|
||||
from .collections import Collections
|
||||
from .logs import Logs, HourlyStats
|
||||
from .realtime import Realtime
|
||||
from .records import Records
|
||||
from .settings import Settings
|
||||
from .users import Users, UserAuthResponse, AuthMethodsList, AuthProviderInfo
|
||||
from .admin_service import AdminService, AdminAuthResponse
|
||||
from .collection_service import CollectionService
|
||||
from .log_service import LogService, HourlyStats
|
||||
from .realtime_service import RealtimeService
|
||||
from .record_service import RecordService
|
||||
from .settings_service import SettingsService
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
from pocketbase.services.utils.crud_service import CrudService
|
||||
from pocketbase.models.admin import Admin
|
||||
@ -15,13 +16,45 @@ class AdminAuthResponse:
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class Admins(CrudService):
|
||||
def decode(self, data: dict[str:Any]) -> BaseModel:
|
||||
class AdminService(CrudService):
|
||||
def decode(self, data: dict) -> BaseModel:
|
||||
return Admin(data)
|
||||
|
||||
def base_crud_path(self) -> str:
|
||||
return "/api/admins"
|
||||
|
||||
def update(self, id: str, body_params: dict, query_params: dict) -> BaseModel:
|
||||
"""
|
||||
If the current `client.auth_store.model` matches with the updated id,
|
||||
then on success the `client.auth_store.model` will be updated with the result.
|
||||
"""
|
||||
item = super(AdminService).update(id, body_params)
|
||||
try:
|
||||
if (
|
||||
self.client.auth_store.model.collection_id is not None
|
||||
and item.id == self.client.auth_store.model.id
|
||||
):
|
||||
self.client.auth_store.save(self.client.auth_store.token, item)
|
||||
except:
|
||||
pass
|
||||
return item
|
||||
|
||||
def delete(self, id: str, body_params: dict, query_params: dict) -> BaseModel:
|
||||
"""
|
||||
If the current `client.auth_store.model` matches with the deleted id,
|
||||
then on success the `client.auth_store` will be cleared.
|
||||
"""
|
||||
item = super(AdminService).delete(id, body_params)
|
||||
try:
|
||||
if (
|
||||
self.client.auth_store.model.collection_id is not None
|
||||
and item.id == self.client.auth_store.model.id
|
||||
):
|
||||
self.client.auth_store.save(self.client.auth_store.token, item)
|
||||
except:
|
||||
pass
|
||||
return item
|
||||
|
||||
def auth_response(self, response_data: dict) -> AdminAuthResponse:
|
||||
"""Prepare successful authorize response."""
|
||||
admin = self.decode(response_data.pop("admin", {}))
|
||||
@ -30,18 +63,18 @@ class Admins(CrudService):
|
||||
self.client.auth_store.save(token, admin)
|
||||
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 = {}
|
||||
) -> AdminAuthResponse:
|
||||
"""
|
||||
Authenticate an admin account by its email and password
|
||||
Authenticate an admin account with its email and password
|
||||
and returns a new admin token and data.
|
||||
|
||||
On success this method automatically updates the client's AuthStore data.
|
||||
"""
|
||||
body_params.update({"email": email, "password": password})
|
||||
body_params.update({"identity": email, "password": password})
|
||||
response_data = self.client.send(
|
||||
self.base_crud_path() + "/auth-via-email",
|
||||
self.base_crud_path() + "/auth-with-password",
|
||||
{
|
||||
"method": "POST",
|
||||
"params": query_params,
|
||||
@ -51,7 +84,7 @@ class Admins(CrudService):
|
||||
)
|
||||
return self.auth_response(response_data)
|
||||
|
||||
def refresh(
|
||||
def authRefresh(
|
||||
self, body_params: dict = {}, query_params: dict = {}
|
||||
) -> AdminAuthResponse:
|
||||
"""
|
||||
@ -62,7 +95,7 @@ class Admins(CrudService):
|
||||
"""
|
||||
return self.auth_response(
|
||||
self.client.send(
|
||||
self.base_crud_path() + "/refresh",
|
||||
self.base_crud_path() + "/auth-refresh",
|
||||
{"method": "POST", "params": query_params, "body": body_params},
|
||||
)
|
||||
)
|
||||
@ -1,12 +1,12 @@
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.services.utils.crud_service import CrudService
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
from pocketbase.models.collection import Collection
|
||||
|
||||
|
||||
class Collections(CrudService):
|
||||
def decode(self, data: dict[str:Any]) -> BaseModel:
|
||||
class CollectionService(CrudService):
|
||||
def decode(self, data: dict) -> BaseModel:
|
||||
return Collection(data)
|
||||
|
||||
def base_crud_path(self) -> str:
|
||||
@ -14,7 +14,7 @@ class Collections(CrudService):
|
||||
|
||||
def import_collections(
|
||||
self,
|
||||
collections: list[Collection],
|
||||
collections: list,
|
||||
delete_missing: bool = False,
|
||||
query_params: dict = {},
|
||||
) -> bool:
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from urllib.parse import quote
|
||||
@ -15,7 +17,7 @@ class HourlyStats:
|
||||
date: Union[str, datetime.datetime]
|
||||
|
||||
|
||||
class Logs(BaseService):
|
||||
class LogService(BaseService):
|
||||
def get_request_list(
|
||||
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||
) -> ListResult:
|
||||
@ -1,49 +0,0 @@
|
||||
from typing import Callable, Optional
|
||||
from pocketbase.services.utils.base_service import BaseService
|
||||
from pocketbase.models.record import Record
|
||||
|
||||
|
||||
class Realtime(BaseService):
|
||||
client_id: str
|
||||
subscriptions: dict
|
||||
|
||||
def subscribe(self, subscription: str, callback: Callable) -> None:
|
||||
"""Inits the sse connection (if not already) and register the subscription."""
|
||||
self.subscriptions[subscription] = callback
|
||||
|
||||
def unsubscribe(self, subscription: Optional[str] = None) -> None:
|
||||
"""
|
||||
Unsubscribe from a subscription.
|
||||
|
||||
If the `subscription` argument is not set,
|
||||
then the client will unsubscribe from all registered subscriptions.
|
||||
|
||||
The related sse connection will be autoclosed if after the
|
||||
unsubscribe operations there are no active subscriptions left.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _submit_subscriptions(self) -> bool:
|
||||
self.client.send(
|
||||
"/api/realtime",
|
||||
{
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"clientId": self.client_id,
|
||||
"subscriptions": self.subscriptions.keys(),
|
||||
},
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
def _add_subscription_listeners(self) -> None:
|
||||
pass
|
||||
|
||||
def _remove_subscription_listeners(self) -> None:
|
||||
pass
|
||||
|
||||
def _connect(self) -> None:
|
||||
pass
|
||||
|
||||
def _disconnect(self) -> None:
|
||||
pass
|
||||
151
pocketbase/services/realtime_service.py
Normal file
151
pocketbase/services/realtime_service.py
Normal file
@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, List
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
from pocketbase.services.utils.base_service import BaseService
|
||||
from pocketbase.services.utils.sse import Event, SSEClient
|
||||
from pocketbase.models.record import Record
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MessageData:
|
||||
action: str
|
||||
record: Record
|
||||
|
||||
|
||||
class RealtimeService(BaseService):
|
||||
subscriptions: dict
|
||||
client_id: str = ""
|
||||
event_source: SSEClient | None = None
|
||||
|
||||
def __init__(self, client) -> None:
|
||||
super().__init__(client)
|
||||
self.subscriptions = {}
|
||||
self.client_id = ""
|
||||
self.event_source = None
|
||||
|
||||
def subscribe(
|
||||
self, subscription: str, callback: Callable[[MessageData], None]
|
||||
) -> None:
|
||||
"""Inits the sse connection (if not already) and register the subscription."""
|
||||
# unsubscribe existing
|
||||
if subscription in self.subscriptions and self.event_source:
|
||||
self.event_source.remove_event_listener(subscription, callback)
|
||||
# register subscription
|
||||
self.subscriptions[subscription] = self._make_subscription(callback)
|
||||
if not self.event_source:
|
||||
self._connect()
|
||||
elif self.client_id:
|
||||
self._submit_subscriptions()
|
||||
|
||||
def unsubscribe_by_prefix(self, subscription_prefix: str):
|
||||
"""
|
||||
Unsubscribe from all subscriptions starting with the provided prefix.
|
||||
|
||||
This method is no-op if there are no active subscriptions with the provided prefix.
|
||||
|
||||
The related sse connection will be autoclosed if after the
|
||||
unsubscribe operation there are no active subscriptions left.
|
||||
"""
|
||||
to_unsubscribe = []
|
||||
for sub in self.subscriptions:
|
||||
if sub.startswith(subscription_prefix):
|
||||
to_unsubscribe.append(sub)
|
||||
if len(to_unsubscribe) == 0:
|
||||
return
|
||||
return self.unsubscribe(*to_unsubscribe)
|
||||
|
||||
def unsubscribe(self, subscriptions: List[str] | None = None) -> None:
|
||||
"""
|
||||
Unsubscribe from a subscription.
|
||||
|
||||
If the `subscriptions` argument is not set,
|
||||
then the client will unsubscribe from all registered subscriptions.
|
||||
|
||||
The related sse connection will be autoclosed if after the
|
||||
unsubscribe operations there are no active subscriptions left.
|
||||
"""
|
||||
if not subscriptions or len(subscriptions) == 0:
|
||||
# remove all subscriptions
|
||||
self._remove_subscription_listeners()
|
||||
self.subscriptions = {}
|
||||
else:
|
||||
# remove each passed subscription
|
||||
found = False
|
||||
for sub in self.subscriptions:
|
||||
found = True
|
||||
self.event_source.remove_event_listener(sub, self.subscriptions[sub])
|
||||
self.subscriptions.pop(sub)
|
||||
if not found:
|
||||
return
|
||||
|
||||
if self.client_id:
|
||||
self._submit_subscriptions()
|
||||
|
||||
# no more subscriptions -> close the sse connection
|
||||
if not self.subscriptions:
|
||||
self._disconnect()
|
||||
|
||||
def _make_subscription(
|
||||
self, callback: Callable[[MessageData], None]
|
||||
) -> Callable[[Event], None]:
|
||||
def listener(event: Event) -> None:
|
||||
data = json.loads(event.data)
|
||||
if "record" in data and "action" in data:
|
||||
callback(
|
||||
MessageData(
|
||||
action=data["action"],
|
||||
record=Record(
|
||||
data=data["record"],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return listener
|
||||
|
||||
def _submit_subscriptions(self) -> bool:
|
||||
self._add_subscription_listeners()
|
||||
self.client.send(
|
||||
"/api/realtime",
|
||||
{
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"clientId": self.client_id,
|
||||
"subscriptions": list(self.subscriptions.keys()),
|
||||
},
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
def _add_subscription_listeners(self) -> None:
|
||||
if not self.event_source:
|
||||
return
|
||||
self._remove_subscription_listeners()
|
||||
for subscription, callback in self.subscriptions.items():
|
||||
self.event_source.add_event_listener(subscription, callback)
|
||||
|
||||
def _remove_subscription_listeners(self) -> None:
|
||||
if not self.event_source:
|
||||
return
|
||||
for subscription, callback in self.subscriptions.items():
|
||||
self.event_source.remove_event_listener(subscription, callback)
|
||||
|
||||
def _connect_handler(self, event: Event) -> None:
|
||||
self.client_id = event.id
|
||||
self._submit_subscriptions()
|
||||
|
||||
def _connect(self) -> None:
|
||||
self._disconnect()
|
||||
self.event_source = SSEClient(self.client.build_url("/api/realtime"))
|
||||
self.event_source.add_event_listener("PB_CONNECT", self._connect_handler)
|
||||
|
||||
def _disconnect(self) -> None:
|
||||
self._remove_subscription_listeners()
|
||||
self.client_id = ""
|
||||
if not self.event_source:
|
||||
return
|
||||
self.event_source.remove_event_listener("PB_CONNECT", self._connect_handler)
|
||||
self.event_source.close()
|
||||
self.event_source = None
|
||||
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,26 +0,0 @@
|
||||
from typing import Any
|
||||
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[str:Any]) -> 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:
|
||||
results += "?" + urlencode(query_params)
|
||||
return result
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pocketbase.services.utils.base_service import BaseService
|
||||
|
||||
|
||||
class Settings(BaseService):
|
||||
class SettingsService(BaseService):
|
||||
def get_all(self, query_params: dict = {}) -> dict:
|
||||
"""Fetch all available app settings."""
|
||||
return self.client.send(
|
||||
@ -1,280 +0,0 @@
|
||||
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[str:Any]) -> 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_service import BaseService
|
||||
from .crud_service import CrudService
|
||||
from .sub_crud_service import SubCrudService
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from urllib.parse import quote
|
||||
from typing import Any
|
||||
|
||||
from pocketbase.utils import ClientResponseError
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
from pocketbase.models.utils.list_result import ListResult
|
||||
@ -9,7 +10,7 @@ from pocketbase.services.utils.base_service import BaseService
|
||||
|
||||
|
||||
class BaseCrudService(BaseService, ABC):
|
||||
def decode(self, data: dict[str:Any]) -> BaseModel:
|
||||
def decode(self, data: dict) -> BaseModel:
|
||||
"""Response data decoder"""
|
||||
|
||||
def _get_full_list(
|
||||
@ -18,7 +19,7 @@ class BaseCrudService(BaseService, ABC):
|
||||
|
||||
result: list[BaseModel] = []
|
||||
|
||||
def request(result: list[BaseModel], page: int) -> list[Any]:
|
||||
def request(result: list[BaseModel], page: int) -> list:
|
||||
list = self._get_list(base_path, page, batch_size, query_params)
|
||||
items = list.items
|
||||
total_items = list.total_items
|
||||
@ -56,6 +57,22 @@ class BaseCrudService(BaseService, ABC):
|
||||
)
|
||||
)
|
||||
|
||||
def _get_first_list_item(self, base_path: str, filter: str, query_params={}):
|
||||
query_params.update(
|
||||
{
|
||||
"filter": filter,
|
||||
"$cancelKey": "one_by_filter_" + base_path + "_" + filter,
|
||||
}
|
||||
)
|
||||
result = self._get_list(base_path, 1, 1, query_params)
|
||||
try:
|
||||
if len(result.items) == 0:
|
||||
raise
|
||||
except:
|
||||
raise ClientResponseError(
|
||||
"The requested resource wasn't found.", status=404
|
||||
)
|
||||
|
||||
def _create(
|
||||
self, base_path: str, body_params: dict = {}, query_params: dict = {}
|
||||
) -> BaseModel:
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
|
||||
from pocketbase.models.utils.base_model import BaseModel
|
||||
@ -10,16 +12,31 @@ class CrudService(BaseCrudService, ABC):
|
||||
"""Base path for the crud actions (without trailing slash, eg. '/admins')."""
|
||||
|
||||
def get_full_list(
|
||||
self, batch_size: int = 100, query_params: dict = {}
|
||||
self, batch: int = 200, query_params: dict = {}
|
||||
) -> list[BaseModel]:
|
||||
return self._get_full_list(self.base_crud_path(), batch_size, query_params)
|
||||
return self._get_full_list(self.base_crud_path(), batch, query_params)
|
||||
|
||||
def get_list(
|
||||
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||
) -> ListResult:
|
||||
return self._get_list(self.base_crud_path(), page, per_page, query_params)
|
||||
|
||||
def _get_first_list_item(self, base_path: str, filter: str, query_params):
|
||||
"""
|
||||
Returns the first found item by the specified filter.
|
||||
|
||||
Internally it calls `getList(1, 1, { filter })` and returns the
|
||||
first found item.
|
||||
|
||||
For consistency with `getOne`, this method will throw a 404
|
||||
ClientResponseError if no item was found.
|
||||
"""
|
||||
return self._get_first_list_item(base_path, filter, query_params)
|
||||
|
||||
def get_one(self, id: str, query_params: dict = {}) -> BaseModel:
|
||||
"""
|
||||
Returns single item by its id.
|
||||
"""
|
||||
return self._get_one(self.base_crud_path(), id, query_params)
|
||||
|
||||
def create(self, body_params: dict = {}, query_params: dict = {}) -> BaseModel:
|
||||
|
||||
139
pocketbase/services/utils/sse.py
Normal file
139
pocketbase/services/utils/sse.py
Normal file
@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
import dataclasses
|
||||
import threading
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Event:
|
||||
"""Representation of an event"""
|
||||
|
||||
id: str = ""
|
||||
event: str = "message"
|
||||
data: str = ""
|
||||
retry: int | None = None
|
||||
|
||||
|
||||
class EventLoop(threading.Thread):
|
||||
FIELD_SEPARATOR = ":"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
headers: dict | None = None,
|
||||
payload: dict | None = None,
|
||||
encoding="utf-8",
|
||||
listeners: dict[str, Callable] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
threading.Thread.__init__(self, **kwargs)
|
||||
self.kill = False
|
||||
self.client = httpx.Client()
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.headers = headers
|
||||
self.payload = payload
|
||||
self.encoding = encoding
|
||||
self.listeners = listeners or {}
|
||||
|
||||
def _read(self):
|
||||
"""Read the incoming event source stream and yield event chunks"""
|
||||
data = b""
|
||||
with self.client.stream(
|
||||
self.method,
|
||||
self.url,
|
||||
headers=self.headers,
|
||||
data=self.payload,
|
||||
timeout=None,
|
||||
) as r:
|
||||
for chunk in r.iter_bytes():
|
||||
for line in chunk.splitlines(True):
|
||||
data += line
|
||||
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
|
||||
yield data
|
||||
data = b""
|
||||
if data:
|
||||
yield data
|
||||
|
||||
def _events(self):
|
||||
for chunk in self._read():
|
||||
event = Event()
|
||||
for line in chunk.splitlines():
|
||||
line = line.decode(self.encoding)
|
||||
if not line.strip() or line.startswith(self.FIELD_SEPARATOR):
|
||||
continue
|
||||
data = line.split(self.FIELD_SEPARATOR, 1)
|
||||
field = data[0]
|
||||
if field not in event.__dict__:
|
||||
continue
|
||||
if len(data) > 1:
|
||||
if data[1].startswith(" "):
|
||||
value = data[1][1:]
|
||||
else:
|
||||
value = data[1]
|
||||
else:
|
||||
value = ""
|
||||
if field == "data":
|
||||
event.data += value + "\n"
|
||||
else:
|
||||
setattr(event, field, value)
|
||||
if not event.data:
|
||||
continue
|
||||
if event.data.endswith("\n"):
|
||||
event.data = event.data[0:-1]
|
||||
event.event = event.event or "message"
|
||||
yield event
|
||||
|
||||
def run(self):
|
||||
for event in self._events():
|
||||
if self.kill:
|
||||
break
|
||||
if event.event in self.listeners:
|
||||
self.listeners[event.event](event)
|
||||
|
||||
|
||||
class SSEClient:
|
||||
"""Implementation of a server side event client"""
|
||||
|
||||
_listeners: dict = {}
|
||||
_loop_thread: threading.Thread | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
headers: dict | None = None,
|
||||
payload: dict | None = None,
|
||||
encoding="utf-8",
|
||||
) -> None:
|
||||
self._listeners = {}
|
||||
self._loop_thread = EventLoop(
|
||||
url=url,
|
||||
method=method,
|
||||
headers=headers,
|
||||
payload=payload,
|
||||
encoding=encoding,
|
||||
listeners=self._listeners,
|
||||
name="loop",
|
||||
)
|
||||
self._loop_thread.daemon = True
|
||||
self._loop_thread.start()
|
||||
|
||||
def add_event_listener(self, event: str, callback: Callable[[Event], None]) -> None:
|
||||
self._listeners[event] = callback
|
||||
self._loop_thread.listeners = self._listeners
|
||||
|
||||
def remove_event_listener(
|
||||
self, event: str, callback: Callable[[Event], None]
|
||||
) -> None:
|
||||
if event in self._listeners:
|
||||
self._listeners.pop(event)
|
||||
self._loop_thread.listeners = self._listeners
|
||||
|
||||
def close(self) -> None:
|
||||
# TODO: does not work like this
|
||||
self._loop_thread.kill = True
|
||||
@ -1,36 +0,0 @@
|
||||
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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import Union, Optional
|
||||
|
||||
from pocketbase.models.admin import Admin
|
||||
from pocketbase.models.user import User
|
||||
from pocketbase.models.record import Record
|
||||
|
||||
|
||||
class BaseAuthStore(ABC):
|
||||
@ -12,29 +13,30 @@ class BaseAuthStore(ABC):
|
||||
"""
|
||||
|
||||
base_token: str
|
||||
base_model: Union[User, Admin, None]
|
||||
base_model: Record | Admin | None
|
||||
|
||||
def __init__(
|
||||
self, base_token: str = "", base_model: Optional[Union[User, Admin]] = None
|
||||
self, base_token: str = "", base_model: Record | Admin | None = None
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.base_token = base_token
|
||||
self.base_model = base_model
|
||||
|
||||
@property
|
||||
def token(self) -> Union[str, None]:
|
||||
def token(self) -> str | None:
|
||||
"""Retrieves the stored token (if any)."""
|
||||
return self.base_token
|
||||
|
||||
@property
|
||||
def model(self) -> Union[User, Admin, None]:
|
||||
def model(self) -> Record | Admin | None:
|
||||
"""Retrieves the stored model data (if any)."""
|
||||
return self.base_model
|
||||
|
||||
def save(self, token: str = "", model: Optional[Union[User, Admin]] = None) -> None:
|
||||
def save(self, token: str = "", model: Record | Admin | None = None) -> None:
|
||||
"""Saves the provided new token and model data in the auth store."""
|
||||
self.base_token = token
|
||||
self.base_model = model
|
||||
|
||||
self.base_token = token if token else ""
|
||||
self.base_model = model if model else None
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Removes the stored token and model data form the auth store."""
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from typing import Any, Optional, Union
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
import pickle
|
||||
import os
|
||||
|
||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||
from pocketbase.models.user import User
|
||||
from pocketbase.models.record import Record
|
||||
from pocketbase.models.admin import Admin
|
||||
|
||||
|
||||
@ -16,7 +18,7 @@ class LocalAuthStore(BaseAuthStore):
|
||||
filename: str = "pocketbase_auth.data",
|
||||
filepath: str = "",
|
||||
base_token: str = "",
|
||||
base_model: Optional[Union[User, Admin]] = None,
|
||||
base_model: Record | Admin | None = None,
|
||||
) -> None:
|
||||
super().__init__(base_token, base_model)
|
||||
self.filename = filename
|
||||
@ -26,18 +28,18 @@ class LocalAuthStore(BaseAuthStore):
|
||||
@property
|
||||
def token(self) -> str:
|
||||
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 data["token"]
|
||||
|
||||
@property
|
||||
def model(self) -> Union[User, Admin, None]:
|
||||
def model(self) -> Record | Admin | None:
|
||||
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 data["model"]
|
||||
|
||||
def save(self, token: str = "", model: Optional[Union[User, Admin]] = None) -> None:
|
||||
def save(self, token: str = "", model: Record | Admin | None = None) -> None:
|
||||
self._storage_set(self.complete_filepath, {"token": token, "model": model})
|
||||
super().save(token, model)
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import datetime
|
||||
from typing import Union
|
||||
from typing import Any
|
||||
|
||||
|
||||
def camel_to_snake(name: str) -> str:
|
||||
@ -10,9 +12,25 @@ def camel_to_snake(name: str) -> str:
|
||||
|
||||
def to_datetime(
|
||||
str_datetime: str, format: str = "%Y-%m-%d %H:%M:%S"
|
||||
) -> Union[datetime.datetime, str]:
|
||||
) -> datetime.datetime | str:
|
||||
str_datetime = str_datetime.split(".")[0]
|
||||
try:
|
||||
return datetime.datetime.strptime(str_datetime, format)
|
||||
except Exception:
|
||||
return str_datetime
|
||||
|
||||
|
||||
class ClientResponseError(Exception):
|
||||
url: str = ""
|
||||
status: int = 0
|
||||
data: dict = {}
|
||||
is_abort: bool = False
|
||||
original_error: Any | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args)
|
||||
self.url = kwargs.get("url", "")
|
||||
self.status = kwargs.get("status", 0)
|
||||
self.data = kwargs.get("data", {})
|
||||
self.is_abort = kwargs.get("is_abort", False)
|
||||
self.original_error = kwargs.get("original_error", None)
|
||||
|
||||
307
poetry.lock
generated
307
poetry.lock
generated
@ -1,6 +1,6 @@
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.6.1"
|
||||
version = "3.6.2"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -9,19 +9,12 @@ python-versions = ">=3.6.2"
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
|
||||
trio = ["trio (>=0.16)"]
|
||||
|
||||
[[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.*"
|
||||
trio = ["trio (>=0.16,<0.22)"]
|
||||
|
||||
[[package]]
|
||||
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"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.8.0"
|
||||
version = "22.10.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
@ -51,6 +44,8 @@ mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
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]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
@ -60,7 +55,7 @@ uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.6.15.2"
|
||||
version = "2022.9.24"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -76,14 +71,40 @@ python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "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]]
|
||||
name = "h11"
|
||||
@ -140,12 +161,36 @@ optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "8.14.0"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
name = "importlib-metadata"
|
||||
version = "4.2.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "dev"
|
||||
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]]
|
||||
name = "mypy-extensions"
|
||||
@ -176,34 +221,46 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "2.5.3"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "0.13.1"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
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]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
name = "pycodestyle"
|
||||
version = "2.9.1"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
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]]
|
||||
name = "pyparsing"
|
||||
@ -218,25 +275,24 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "5.4.3"
|
||||
version = "7.2.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=17.4.0"
|
||||
attrs = ">=19.2.0"
|
||||
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 = "*"
|
||||
pluggy = ">=0.12,<1.0"
|
||||
py = ">=1.5.0"
|
||||
wcwidth = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
checkqa-mypy = ["mypy (==v0.761)"]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "rfc3986"
|
||||
@ -269,66 +325,89 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.5"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
name = "typed-ast"
|
||||
version = "1.5.4"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
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]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "8e1b477e300709c958c814a3937ee98aa175988857e7452bc5e3e2b97f58f7ca"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "8f9534771a19adba002263f1c9c563e5d1bd7e1f134fd42aa85689b614fd6e0a"
|
||||
|
||||
[metadata.files]
|
||||
anyio = [
|
||||
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
|
||||
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
|
||||
]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
|
||||
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
|
||||
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
|
||||
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
|
||||
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
|
||||
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
|
||||
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
|
||||
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
|
||||
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
|
||||
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
|
||||
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
|
||||
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
|
||||
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
|
||||
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
|
||||
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
|
||||
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
|
||||
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
|
||||
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
|
||||
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
|
||||
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
|
||||
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
|
||||
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
|
||||
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
|
||||
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
|
||||
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
|
||||
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
|
||||
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
|
||||
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
|
||||
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
|
||||
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
|
||||
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
|
||||
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
|
||||
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
|
||||
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
|
||||
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
|
||||
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
|
||||
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
|
||||
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
|
||||
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
||||
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.6.15.2-py3-none-any.whl", hash = "sha256:0aa1a42fbd57645fabeb6290a7687c21755b0344ecaeaa05f4e9f6207ae2e9a8"},
|
||||
{file = "certifi-2022.6.15.2.tar.gz", hash = "sha256:aa08c101214127b9b0472ca6338315113c9487d45376fd3e669201b477c71003"},
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
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 = [
|
||||
{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.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
more-itertools = [
|
||||
{file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"},
|
||||
{file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"},
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
|
||||
{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 = [
|
||||
{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"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
{file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"},
|
||||
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
|
||||
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
|
||||
]
|
||||
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 = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
|
||||
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
]
|
||||
rfc3986 = [
|
||||
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
||||
@ -394,7 +485,37 @@ tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
@ -1,15 +1,52 @@
|
||||
[project]
|
||||
name = "pocketbase"
|
||||
description = "PocketBase SDK for python."
|
||||
requires-python = ">=3.7"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "Vithor Jaeger", email = "vaphes@gmail.com" },
|
||||
{ name = "Max Amling", email = "max-amling@web.de" },
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Intended Audience :: Developers",
|
||||
]
|
||||
keywords = "pocketbase sdk"
|
||||
dependencies = ["httpx>=0.23.0"]
|
||||
dynamic = ["readme", "version"]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/vaphes/pocketbase"
|
||||
"Source" = "https://github.com/vaphes/pocketbase"
|
||||
"Bug Tracker" = "https://github.com/vaphes/pocketbase/issues"
|
||||
|
||||
[tool.poetry]
|
||||
name = "pocketbase"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
version = "0.8.1"
|
||||
description = "PocketBase SDK for python."
|
||||
authors = ["Vithor Jaeger <vaphes@gmail.com>"]
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/vaphes/pocketbase"
|
||||
repository = "https://github.com/vaphes/pocketbase"
|
||||
keywords = ["pocketbase", "sdk"]
|
||||
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://github.com/vaphes/pocketbase/issues"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
python = "^3.7"
|
||||
httpx = "^0.23.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^5.2"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "^5.0.4"
|
||||
pytest = "^7.1.3"
|
||||
black = {version = "^22.8.0", allow-prereleases = true}
|
||||
|
||||
[build-system]
|
||||
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
httpx >= 0.23.0
|
||||
@ -1,5 +0,0 @@
|
||||
from pocketbase import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
assert __version__ == "0.1.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