first commit
This commit is contained in:
commit
28d24981f8
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/.venv
|
||||||
|
/.vscode
|
||||||
|
/dist
|
||||||
|
/poetry.lock
|
||||||
|
/pyproject.toml
|
||||||
|
/tests
|
||||||
0
README.rst
Normal file
0
README.rst
Normal file
3
pocketbase/__init__.py
Normal file
3
pocketbase/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
from .client import Client, ClientResponseError
|
||||||
BIN
pocketbase/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/__pycache__/client.cpython-310.pyc
Normal file
BIN
pocketbase/__pycache__/client.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/__pycache__/utils.cpython-310.pyc
Normal file
BIN
pocketbase/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
144
pocketbase/client.py
Normal file
144
pocketbase/client.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from typing import Any
|
||||||
|
from urllib.parse import 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.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)
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
base_url: str
|
||||||
|
lang: str
|
||||||
|
auth_store: BaseAuthStore
|
||||||
|
settings: Settings
|
||||||
|
admins: Admins
|
||||||
|
users: Users
|
||||||
|
collections: Collections
|
||||||
|
records: Records
|
||||||
|
logs: Logs
|
||||||
|
realtime: Realtime
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str = "/",
|
||||||
|
lang: str = "en-US",
|
||||||
|
auth_store: BaseAuthStore = None,
|
||||||
|
) -> None:
|
||||||
|
self.base_url = base_url
|
||||||
|
self.lang = lang
|
||||||
|
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
|
||||||
|
# services
|
||||||
|
self.admins = Admins(self)
|
||||||
|
self.users = Users(self)
|
||||||
|
self.records = Records(self)
|
||||||
|
self.collections = Collections(self)
|
||||||
|
self.logs = Logs(self)
|
||||||
|
self.settings = Settings(self)
|
||||||
|
self.realtime = Realtime(self)
|
||||||
|
|
||||||
|
def cancel_request(self, cancel_key: str):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def cancel_all_requests(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def send(self, path: str, req_config: dict[str:Any]) -> Any:
|
||||||
|
"""Sends an api http request."""
|
||||||
|
config = {"method": "GET"}
|
||||||
|
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"]
|
||||||
|
):
|
||||||
|
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}"}
|
||||||
|
)
|
||||||
|
# build url + path
|
||||||
|
url = self.build_url(path)
|
||||||
|
# send the request
|
||||||
|
method = config.get("method", "GET")
|
||||||
|
params = config.get("params", None)
|
||||||
|
headers = config.get("headers", None)
|
||||||
|
body = config.get("body", None)
|
||||||
|
try:
|
||||||
|
response = httpx.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
json=body,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ClientResponseError(
|
||||||
|
f"General request error. Original error: {e}",
|
||||||
|
original_error=e,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except Exception:
|
||||||
|
data = None
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise ClientResponseError(
|
||||||
|
f"Response error. Status code:{response.status_code}",
|
||||||
|
url=response.url,
|
||||||
|
status=response.status_code,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def build_url(self, path: str) -> str:
|
||||||
|
url = self.base_url
|
||||||
|
if not self.base_url.endswith("/"):
|
||||||
|
url += "/"
|
||||||
|
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)
|
||||||
6
pocketbase/models/__init__.py
Normal file
6
pocketbase/models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .admin import Admin
|
||||||
|
from .collection import Collection
|
||||||
|
from .external_auth import ExternalAuth
|
||||||
|
from .log_request import LogRequest
|
||||||
|
from .record import Record
|
||||||
|
from .user import User
|
||||||
BIN
pocketbase/models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/__pycache__/collection.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/collection.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/__pycache__/log_request.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/log_request.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/__pycache__/record.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/record.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/__pycache__/user.cpython-310.pyc
Normal file
BIN
pocketbase/models/__pycache__/user.cpython-310.pyc
Normal file
Binary file not shown.
17
pocketbase/models/admin.py
Normal file
17
pocketbase/models/admin.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from typing import Any, Union
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pocketbase.utils import to_datetime
|
||||||
|
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:
|
||||||
|
super().load(data)
|
||||||
|
self.avatar = data.get("avatar", 0)
|
||||||
|
self.email = data.get("email", "")
|
||||||
|
self.last_reset_sent_at = to_datetime(data.get("lastResetSentAt", ""))
|
||||||
29
pocketbase/models/collection.py
Normal file
29
pocketbase/models/collection.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
from pocketbase.models.utils.schema_field import SchemaField
|
||||||
|
|
||||||
|
|
||||||
|
class Collection(BaseModel):
|
||||||
|
name: 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]
|
||||||
|
|
||||||
|
def load(self, data: dict[str:Any]) -> None:
|
||||||
|
super().load(data)
|
||||||
|
self.name = data.get("name", "")
|
||||||
|
self.system = data.get("system", False)
|
||||||
|
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 = data.get("schema", [])
|
||||||
|
self.schema = []
|
||||||
|
for field in schema:
|
||||||
|
self.schema.append(SchemaField(**field))
|
||||||
14
pocketbase/models/external_auth.py
Normal file
14
pocketbase/models/external_auth.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import Any
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalAuth(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
provider: str
|
||||||
|
provider_id: str
|
||||||
|
|
||||||
|
def load(self, data: dict[str:Any]) -> None:
|
||||||
|
super().load(data)
|
||||||
|
self.user_id = data.get("userId", "")
|
||||||
|
self.provider = data.get("provider", "")
|
||||||
|
self.provider_id = data.get("providerId", "")
|
||||||
27
pocketbase/models/log_request.py
Normal file
27
pocketbase/models/log_request.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class LogRequest(BaseModel):
|
||||||
|
url: str
|
||||||
|
method: str
|
||||||
|
status: int
|
||||||
|
auth: str
|
||||||
|
remote_ip: str
|
||||||
|
user_ip: str
|
||||||
|
referer: str
|
||||||
|
user_agent: str
|
||||||
|
meta: dict[str:Any]
|
||||||
|
|
||||||
|
def load(self, data: dict[str:Any]) -> None:
|
||||||
|
super().load(data)
|
||||||
|
self.url = data.get("url", "")
|
||||||
|
self.method = data.get("method", "")
|
||||||
|
self.status = data.get("status", 200)
|
||||||
|
self.auth = data.get("auth", "guest")
|
||||||
|
self.remote_ip = data.get("remoteIp", data.get("ip", ""))
|
||||||
|
self.user_ip = data.get("userIp", "")
|
||||||
|
self.referer = data.get("referer", "")
|
||||||
|
self.user_agent = data.get("userAgent", "")
|
||||||
|
self.meta = data.get("meta", {})
|
||||||
30
pocketbase/models/record.py
Normal file
30
pocketbase/models/record.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
from pocketbase.utils import camel_to_snake
|
||||||
|
|
||||||
|
|
||||||
|
class Record(BaseModel):
|
||||||
|
collection_id: str
|
||||||
|
collection_name: str
|
||||||
|
expand: dict[str:Any]
|
||||||
|
|
||||||
|
def load(self, data: dict[str:Any]) -> None:
|
||||||
|
super().load(data)
|
||||||
|
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]):
|
||||||
|
return cls(data)
|
||||||
|
|
||||||
|
def load_expanded(self) -> None:
|
||||||
|
for key, value in self.expand.items():
|
||||||
|
self.expand[key] = self.parse_expanded(value)
|
||||||
27
pocketbase/models/user.py
Normal file
27
pocketbase/models/user.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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)
|
||||||
3
pocketbase/models/utils/__init__.py
Normal file
3
pocketbase/models/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .base_model import BaseModel
|
||||||
|
from .list_result import ListResult
|
||||||
|
from .schema_field import SchemaField
|
||||||
BIN
pocketbase/models/utils/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/models/utils/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/utils/__pycache__/base_model.cpython-310.pyc
Normal file
BIN
pocketbase/models/utils/__pycache__/base_model.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/utils/__pycache__/list_result.cpython-310.pyc
Normal file
BIN
pocketbase/models/utils/__pycache__/list_result.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/models/utils/__pycache__/schema_field.cpython-310.pyc
Normal file
BIN
pocketbase/models/utils/__pycache__/schema_field.cpython-310.pyc
Normal file
Binary file not shown.
26
pocketbase/models/utils/base_model.py
Normal file
26
pocketbase/models/utils/base_model.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from typing import Any, Union
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pocketbase.utils import to_datetime
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(ABC):
|
||||||
|
id: str
|
||||||
|
created: Union[str, datetime.datetime]
|
||||||
|
updated: Union[str, datetime.datetime]
|
||||||
|
|
||||||
|
def __init__(self, data: dict[str:Any] = {}) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.load(data)
|
||||||
|
|
||||||
|
def load(self, data: dict[str:Any]) -> None:
|
||||||
|
"""Loads `data` into the current model."""
|
||||||
|
self.id = data.pop("id", "")
|
||||||
|
self.created = to_datetime(data.pop("created", ""))
|
||||||
|
self.updated = to_datetime(data.pop("updated", ""))
|
||||||
|
|
||||||
|
@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"
|
||||||
12
pocketbase/models/utils/list_result.py
Normal file
12
pocketbase/models/utils/list_result.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ListResult:
|
||||||
|
page: int = 1
|
||||||
|
per_page: int = 0
|
||||||
|
total_items: int = 0
|
||||||
|
total_pages: int = 0
|
||||||
|
items: list[BaseModel] = field(default_factory=list)
|
||||||
13
pocketbase/models/utils/schema_field.py
Normal file
13
pocketbase/models/utils/schema_field.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SchemaField:
|
||||||
|
id: str = ""
|
||||||
|
name: str = ""
|
||||||
|
type: str = "text"
|
||||||
|
system: bool = False
|
||||||
|
required: bool = False
|
||||||
|
unique: bool = False
|
||||||
|
options: dict[str:Any] = field(default_factory=dict)
|
||||||
7
pocketbase/services/__init__.py
Normal file
7
pocketbase/services/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
BIN
pocketbase/services/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/admins.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/admins.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/collections.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/collections.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/logs.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/logs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/realtime.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/realtime.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/records.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/records.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/settings.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/services/__pycache__/users.cpython-310.pyc
Normal file
BIN
pocketbase/services/__pycache__/users.cpython-310.pyc
Normal file
Binary file not shown.
110
pocketbase/services/admins.py
Normal file
110
pocketbase/services/admins.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
from typing import Any
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
from pocketbase.services.utils.crud_service import CrudService
|
||||||
|
from pocketbase.models.admin import Admin
|
||||||
|
|
||||||
|
|
||||||
|
class AdminAuthResponse:
|
||||||
|
token: str
|
||||||
|
admin: Admin
|
||||||
|
|
||||||
|
def __init__(self, token: str, admin: Admin, **kwargs) -> None:
|
||||||
|
self.token = token
|
||||||
|
self.admin = admin
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Admins(CrudService):
|
||||||
|
def decode(self, data: dict[str:Any]) -> BaseModel:
|
||||||
|
return Admin(data)
|
||||||
|
|
||||||
|
def base_crud_path(self) -> str:
|
||||||
|
return "/api/admins"
|
||||||
|
|
||||||
|
def auth_response(self, response_data: dict) -> AdminAuthResponse:
|
||||||
|
"""Prepare successful authorize response."""
|
||||||
|
admin = self.decode(response_data.pop("admin", {}))
|
||||||
|
token = response_data.pop("token", "")
|
||||||
|
if token and admin:
|
||||||
|
self.client.auth_store.save(token, admin)
|
||||||
|
return AdminAuthResponse(token=token, admin=admin, **response_data)
|
||||||
|
|
||||||
|
def auth_via_email(
|
||||||
|
self, email: str, password: str, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> AdminAuthResponse:
|
||||||
|
"""
|
||||||
|
Authenticate an admin account by 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})
|
||||||
|
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 refresh(
|
||||||
|
self, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> AdminAuthResponse:
|
||||||
|
"""
|
||||||
|
Refreshes the current admin authenticated instance and
|
||||||
|
returns a new token and admin data.
|
||||||
|
|
||||||
|
On success this method 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 requestPasswordReset(
|
||||||
|
self, email: str, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> bool:
|
||||||
|
"""Sends admin 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 confirmPasswordReset(
|
||||||
|
self,
|
||||||
|
password_reset_token: str,
|
||||||
|
password: str,
|
||||||
|
password_confirm: str,
|
||||||
|
body_params: dict = {},
|
||||||
|
query_params: dict = {},
|
||||||
|
) -> AdminAuthResponse:
|
||||||
|
"""Confirms admin 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
36
pocketbase/services/collections.py
Normal file
36
pocketbase/services/collections.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
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:
|
||||||
|
return Collection(data)
|
||||||
|
|
||||||
|
def base_crud_path(self) -> str:
|
||||||
|
return "/api/collections"
|
||||||
|
|
||||||
|
def import_collections(
|
||||||
|
self,
|
||||||
|
collections: list[Collection],
|
||||||
|
delete_missing: bool = False,
|
||||||
|
query_params: dict = {},
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Imports the provided collections.
|
||||||
|
|
||||||
|
If `delete_missing` is `True`, all local collections and schema fields,
|
||||||
|
that are not present in the imported configuration, WILL BE DELETED
|
||||||
|
(including their related records data)!
|
||||||
|
"""
|
||||||
|
self.client.send(
|
||||||
|
self.base_crud_path() + "/import",
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"params": query_params,
|
||||||
|
"body": {"collections": collections, "deleteMissing": delete_missing},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
58
pocketbase/services/logs.py
Normal file
58
pocketbase/services/logs.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Union
|
||||||
|
from urllib.parse import quote
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pocketbase.services.utils.base_service import BaseService
|
||||||
|
from pocketbase.models.utils.list_result import ListResult
|
||||||
|
from pocketbase.models.log_request import LogRequest
|
||||||
|
from pocketbase.utils import to_datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HourlyStats:
|
||||||
|
total: int
|
||||||
|
date: Union[str, datetime.datetime]
|
||||||
|
|
||||||
|
|
||||||
|
class Logs(BaseService):
|
||||||
|
def get_request_list(
|
||||||
|
self, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||||
|
) -> ListResult:
|
||||||
|
"""Returns paginated logged requests list."""
|
||||||
|
query_params.update({"page": page, "perPage": per_page})
|
||||||
|
response_data = self.client.send(
|
||||||
|
"/api/logs/requests",
|
||||||
|
{"method": "GET", "params": query_params},
|
||||||
|
)
|
||||||
|
items: list[LogRequest] = []
|
||||||
|
if "items" in response_data:
|
||||||
|
response_data["items"] = response_data["items"] or []
|
||||||
|
for item in response_data["items"]:
|
||||||
|
items.append(LogRequest(item))
|
||||||
|
return ListResult(
|
||||||
|
response_data.get("page", 1),
|
||||||
|
response_data.get("perPage", 0),
|
||||||
|
response_data.get("totalItems", 0),
|
||||||
|
response_data.get("totalPages", 0),
|
||||||
|
items,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_request(self, id: str, query_params: dict = {}) -> LogRequest:
|
||||||
|
"""Returns a single logged request by its id."""
|
||||||
|
return LogRequest(
|
||||||
|
self.client.send(
|
||||||
|
"/api/logs/requests/" + quote(id),
|
||||||
|
{"method": "GET", "params": query_params},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_requests_stats(self, query_params: dict = {}) -> list[HourlyStats]:
|
||||||
|
"""Returns request logs statistics."""
|
||||||
|
return [
|
||||||
|
HourlyStats(total=stat["total"], date=to_datetime(stat["date"]))
|
||||||
|
for stat in self.client.send(
|
||||||
|
"/api/logs/requests/stats",
|
||||||
|
{"method": "GET", "params": query_params},
|
||||||
|
)
|
||||||
|
]
|
||||||
49
pocketbase/services/realtime.py
Normal file
49
pocketbase/services/realtime.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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
|
||||||
26
pocketbase/services/records.py
Normal file
26
pocketbase/services/records.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
50
pocketbase/services/settings.py
Normal file
50
pocketbase/services/settings.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from pocketbase.services.utils.base_service import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseService):
|
||||||
|
def get_all(self, query_params: dict = {}) -> dict:
|
||||||
|
"""Fetch all available app settings."""
|
||||||
|
return self.client.send(
|
||||||
|
"/api/settings",
|
||||||
|
{"method": "GET", "params": query_params},
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, body_params: dict = {}, query_params: dict = {}) -> dict:
|
||||||
|
"""Bulk updates app settings."""
|
||||||
|
return self.client.send(
|
||||||
|
"/api/settings",
|
||||||
|
{
|
||||||
|
"method": "PATCH",
|
||||||
|
"params": query_params,
|
||||||
|
"body": body_params,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_s3(self, query_params: dict = {}) -> bool:
|
||||||
|
"""Performs a S3 storage connection test."""
|
||||||
|
self.client.send(
|
||||||
|
"/api/settings/test/s3",
|
||||||
|
{"method": "POST", "params": query_params},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_email(
|
||||||
|
self, to_email: str, email_template: str, query_params: dict = {}
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Sends a test email.
|
||||||
|
|
||||||
|
The possible `email_template` values are:
|
||||||
|
- verification
|
||||||
|
- password-reset
|
||||||
|
- email-change
|
||||||
|
"""
|
||||||
|
self.client.send(
|
||||||
|
"/api/settings/test/email",
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"params": query_params,
|
||||||
|
"body": {"email": to_email, "template": email_template},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
280
pocketbase/services/users.py
Normal file
280
pocketbase/services/users.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
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
|
||||||
4
pocketbase/services/utils/__init__.py
Normal file
4
pocketbase/services/utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .base_crud_service import BaseCrudService
|
||||||
|
from .base_service import BaseService
|
||||||
|
from .crud_service import CrudService
|
||||||
|
from .sub_crud_service import SubCrudService
|
||||||
BIN
pocketbase/services/utils/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/services/utils/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
83
pocketbase/services/utils/base_crud_service.py
Normal file
83
pocketbase/services/utils/base_crud_service.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from urllib.parse import quote
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
from pocketbase.models.utils.base_model import BaseModel
|
||||||
|
from pocketbase.models.utils.list_result import ListResult
|
||||||
|
from pocketbase.services.utils.base_service import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCrudService(BaseService, ABC):
|
||||||
|
def decode(self, data: dict[str:Any]) -> BaseModel:
|
||||||
|
"""Response data decoder"""
|
||||||
|
|
||||||
|
def _get_full_list(
|
||||||
|
self, base_path: str, batch_size: int = 100, query_params: dict = {}
|
||||||
|
) -> list[BaseModel]:
|
||||||
|
|
||||||
|
result: list[BaseModel] = []
|
||||||
|
|
||||||
|
def request(result: list[BaseModel], page: int) -> list[Any]:
|
||||||
|
list = self._get_list(base_path, page, batch_size, query_params)
|
||||||
|
items = list.items
|
||||||
|
total_items = list.total_items
|
||||||
|
result += items
|
||||||
|
if len(items) > 0 and total_items > len(result):
|
||||||
|
return request(result, page + 1)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return request(result, 1)
|
||||||
|
|
||||||
|
def _get_list(
|
||||||
|
self, base_path: str, page: int = 1, per_page: int = 30, query_params: dict = {}
|
||||||
|
) -> ListResult:
|
||||||
|
query_params.update({"page": page, "perPage": per_page})
|
||||||
|
response_data = self.client.send(
|
||||||
|
base_path, {"method": "GET", "params": query_params}
|
||||||
|
)
|
||||||
|
items: list[BaseModel] = []
|
||||||
|
if "items" in response_data:
|
||||||
|
response_data["items"] = response_data["items"] or []
|
||||||
|
for item in response_data["items"]:
|
||||||
|
items.append(self.decode(item))
|
||||||
|
return ListResult(
|
||||||
|
response_data.get("page", 1),
|
||||||
|
response_data.get("perPage", 0),
|
||||||
|
response_data.get("totalItems", 0),
|
||||||
|
response_data.get("totalPages", 0),
|
||||||
|
items,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_one(self, base_path: str, id: str, query_params: dict = {}) -> BaseModel:
|
||||||
|
return self.decode(
|
||||||
|
self.client.send(
|
||||||
|
f"{base_path}/{quote(id)}", {"method": "GET", "params": query_params}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create(
|
||||||
|
self, base_path: str, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> BaseModel:
|
||||||
|
return self.decode(
|
||||||
|
self.client.send(
|
||||||
|
base_path,
|
||||||
|
{"method": "POST", "params": query_params, "body": body_params},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update(
|
||||||
|
self, base_path: str, id: str, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> BaseModel:
|
||||||
|
return self.decode(
|
||||||
|
self.client.send(
|
||||||
|
f"{base_path}/{quote(id)}",
|
||||||
|
{"method": "PATCH", "params": query_params, "body": body_params},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _delete(self, base_path: str, id: str, query_params: dict = {}) -> bool:
|
||||||
|
self.client.send(
|
||||||
|
f"{base_path}/{quote(id)}", {"method": "DELETE", "params": query_params}
|
||||||
|
)
|
||||||
|
return True
|
||||||
7
pocketbase/services/utils/base_service.py
Normal file
7
pocketbase/services/utils/base_service.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService(ABC):
|
||||||
|
def __init__(self, client) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.client = client
|
||||||
34
pocketbase/services/utils/crud_service.py
Normal file
34
pocketbase/services/utils/crud_service.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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 CrudService(BaseCrudService, ABC):
|
||||||
|
def base_crud_path(self) -> str:
|
||||||
|
"""Base path for the crud actions (without trailing slash, eg. '/admins')."""
|
||||||
|
|
||||||
|
def get_full_list(
|
||||||
|
self, batch_size: int = 100, query_params: dict = {}
|
||||||
|
) -> list[BaseModel]:
|
||||||
|
return self._get_full_list(self.base_crud_path(), batch_size, 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_one(self, id: str, query_params: dict = {}) -> BaseModel:
|
||||||
|
return self._get_one(self.base_crud_path(), id, query_params)
|
||||||
|
|
||||||
|
def create(self, body_params: dict = {}, query_params: dict = {}) -> BaseModel:
|
||||||
|
return self._create(self.base_crud_path(), body_params, query_params)
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, id: str, body_params: dict = {}, query_params: dict = {}
|
||||||
|
) -> BaseModel:
|
||||||
|
return self._update(self.base_crud_path(), id, body_params, query_params)
|
||||||
|
|
||||||
|
def delete(self, id: str, query_params: dict = {}) -> bool:
|
||||||
|
return self._delete(self.base_crud_path(), id, query_params)
|
||||||
36
pocketbase/services/utils/sub_crud_service.py
Normal file
36
pocketbase/services/utils/sub_crud_service.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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)
|
||||||
2
pocketbase/stores/__init__.py
Normal file
2
pocketbase/stores/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .base_auth_store import BaseAuthStore
|
||||||
|
from .local_auth_store import LocalAuthStore
|
||||||
BIN
pocketbase/stores/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pocketbase/stores/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/stores/__pycache__/base_auth_store.cpython-310.pyc
Normal file
BIN
pocketbase/stores/__pycache__/base_auth_store.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pocketbase/stores/__pycache__/local_auth_store.cpython-310.pyc
Normal file
BIN
pocketbase/stores/__pycache__/local_auth_store.cpython-310.pyc
Normal file
Binary file not shown.
42
pocketbase/stores/base_auth_store.py
Normal file
42
pocketbase/stores/base_auth_store.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from typing import Union, Optional
|
||||||
|
|
||||||
|
from pocketbase.models.admin import Admin
|
||||||
|
from pocketbase.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAuthStore(ABC):
|
||||||
|
"""
|
||||||
|
Base AuthStore class that is intended to be extended by all other
|
||||||
|
PocketBase AuthStore implementations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_token: str
|
||||||
|
base_model: Union[User, Admin, None]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, base_token: str = "", base_model: Optional[Union[User, Admin]] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.base_token = base_token
|
||||||
|
self.base_model = base_model
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self) -> Union[str, None]:
|
||||||
|
"""Retrieves the stored token (if any)."""
|
||||||
|
return self.base_token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> Union[User, 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:
|
||||||
|
"""Saves the provided new token and model data in the auth store."""
|
||||||
|
self.base_token = token
|
||||||
|
self.base_model = model
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Removes the stored token and model data form the auth store."""
|
||||||
|
self.base_token = None
|
||||||
|
self.base_model = None
|
||||||
59
pocketbase/stores/local_auth_store.py
Normal file
59
pocketbase/stores/local_auth_store.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from typing import Any, Optional, Union
|
||||||
|
import pickle
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||||
|
from pocketbase.models.user import User
|
||||||
|
from pocketbase.models.admin import Admin
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAuthStore(BaseAuthStore):
|
||||||
|
filename: str
|
||||||
|
filepath: str
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filename: str = "pocketbase_auth.data",
|
||||||
|
filepath: str = "",
|
||||||
|
base_token: str = "",
|
||||||
|
base_model: Optional[Union[User, Admin]] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(base_token, base_model)
|
||||||
|
self.filename = filename
|
||||||
|
self.filepath = filepath
|
||||||
|
self.complete_filepath = os.path.join(filepath, filename)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self) -> str:
|
||||||
|
data = self._storage_get(self.complete_filepath)
|
||||||
|
if not data or not "token" in data:
|
||||||
|
return None
|
||||||
|
return data["token"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> Union[User, Admin, None]:
|
||||||
|
data = self._storage_get(self.complete_filepath)
|
||||||
|
if not data or not "model" in data:
|
||||||
|
return None
|
||||||
|
return data["model"]
|
||||||
|
|
||||||
|
def save(self, token: str = "", model: Optional[Union[User, Admin]] = None) -> None:
|
||||||
|
self._storage_set(self.complete_filepath, {"token": token, "model": model})
|
||||||
|
super().save(token, model)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self._storage_remove(self.complete_filepath)
|
||||||
|
super().clear()
|
||||||
|
|
||||||
|
def _storage_set(self, key: str, value: Any) -> None:
|
||||||
|
with open(key, "wb") as f:
|
||||||
|
pickle.dump(value, f)
|
||||||
|
|
||||||
|
def _storage_get(self, key: str) -> Any:
|
||||||
|
with open(key, "rb") as f:
|
||||||
|
value = pickle.load(f)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _storage_remove(self, key: str) -> None:
|
||||||
|
if os.path.exists(key):
|
||||||
|
os.remove(key)
|
||||||
18
pocketbase/utils.py
Normal file
18
pocketbase/utils.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
def camel_to_snake(name: str) -> str:
|
||||||
|
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
||||||
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def to_datetime(
|
||||||
|
str_datetime: str, format: str = "%Y-%m-%d %H:%M:%S"
|
||||||
|
) -> Union[datetime.datetime, str]:
|
||||||
|
str_datetime = str_datetime.split(".")[0]
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(str_datetime, format)
|
||||||
|
except Exception:
|
||||||
|
return str_datetime
|
||||||
Loading…
x
Reference in New Issue
Block a user