Compare commits
4 Commits
master
...
revert-9-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda1d2846d | ||
|
|
428d18c387 | ||
|
|
00c9365b5f | ||
|
|
10496553ed |
39
.github/workflows/python-publish.yml
vendored
39
.github/workflows/python-publish.yml
vendored
@ -1,39 +0,0 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 vaphes
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
66
README.md
66
README.md
@ -10,10 +10,10 @@ This is in early development, and at first is just a translation of <a href="htt
|
||||
|
||||
## Installation
|
||||
|
||||
Install PocketBase using PIP:
|
||||
Install PocketBase using pip:
|
||||
|
||||
```shell
|
||||
python3 -m pip install pocketbase
|
||||
$ pip install pocketbase
|
||||
```
|
||||
|
||||
## Usage
|
||||
@ -21,66 +21,26 @@ python3 -m pip install pocketbase
|
||||
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
|
||||
from pocketbase import PocketBase # Client also works the same
|
||||
|
||||
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"'})
|
||||
result = client.records.get_list(
|
||||
"example", 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"))),
|
||||
})
|
||||
# authenticate as regular user
|
||||
user_data = client.users.auth_via_email("test@example.com", "123456")
|
||||
|
||||
# or as admin
|
||||
admin_data = client.admins.auth_via_email("test@example.com", "123456")
|
||||
|
||||
# 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.
|
||||
<p align="center"><i>The PocketBase Python SDK is <a href="https://github.com/vaphes/pocketbase/blob/master/LICENCE.txt">MIT licensed</a> code.</p>
|
||||
@ -1,9 +1,9 @@
|
||||
__title__ = "pocketbase"
|
||||
__description__ = "PocketBase client SDK for python."
|
||||
__version__ = "0.8.1"
|
||||
__version__ = "0.2.2"
|
||||
|
||||
|
||||
from .client import Client
|
||||
from .client import Client, ClientResponseError
|
||||
|
||||
|
||||
class PocketBase(Client):
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||
from pocketbase.models import FileUpload
|
||||
|
||||
from typing import Any, Dict
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from pocketbase.models import FileUpload
|
||||
from pocketbase.utils import ClientResponseError
|
||||
from pocketbase.models.record import Record
|
||||
from pocketbase.services.admin_service import AdminService
|
||||
from pocketbase.services.collection_service import CollectionService
|
||||
@ -14,7 +16,6 @@ from pocketbase.services.realtime_service import RealtimeService
|
||||
from pocketbase.services.record_service import RecordService
|
||||
from pocketbase.services.settings_service import SettingsService
|
||||
from pocketbase.stores.base_auth_store import BaseAuthStore
|
||||
from pocketbase.utils import ClientResponseError
|
||||
|
||||
|
||||
class Client:
|
||||
@ -35,12 +36,10 @@ class Client:
|
||||
base_url: str = "/",
|
||||
lang: str = "en-US",
|
||||
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 = AdminService(self)
|
||||
self.collections = CollectionService(self)
|
||||
@ -64,7 +63,9 @@ class Client:
|
||||
"headers" not in config or "Authorization" not in config["headers"]
|
||||
):
|
||||
config["headers"] = config.get("headers", {})
|
||||
config["headers"].update({"Authorization": self.auth_store.token})
|
||||
config["headers"].update(
|
||||
{"Authorization": self.auth_store.token}
|
||||
)
|
||||
# build url + path
|
||||
url = self.build_url(path)
|
||||
# send the request
|
||||
@ -96,7 +97,7 @@ class Client:
|
||||
json=body,
|
||||
data=data,
|
||||
files=files,
|
||||
timeout=self.timeout,
|
||||
timeout=120,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ClientResponseError(
|
||||
@ -118,16 +119,16 @@ class Client:
|
||||
|
||||
def get_file_url(self, record: Record, filename: str, query_params: dict):
|
||||
parts = [
|
||||
"api",
|
||||
"files",
|
||||
'api',
|
||||
'files',
|
||||
quote(record.collection_id or record.collection_name),
|
||||
quote(record.id),
|
||||
quote(filename),
|
||||
]
|
||||
result = self.build_url("/".join(parts))
|
||||
result = self.build_url('/'.join(parts))
|
||||
if len(query_params) != 0:
|
||||
params: str = urlencode(query_params)
|
||||
result += "&" if "?" in result else "?"
|
||||
result += '&' if '?' in result else '?'
|
||||
result += params
|
||||
return result
|
||||
|
||||
|
||||
@ -37,10 +37,10 @@ class Collection(BaseModel):
|
||||
self.schema.append(SchemaField(**field))
|
||||
|
||||
def is_base(self):
|
||||
return self.type == "base"
|
||||
return self.type == 'base'
|
||||
|
||||
def is_auth(self):
|
||||
return self.type == "auth"
|
||||
return self.type == 'auth'
|
||||
|
||||
def is_single(self):
|
||||
return self.type == "single"
|
||||
return self.type == 'single'
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from httpx._types import FileTypes
|
||||
from typing import Sequence, Union
|
||||
|
||||
FileUploadTypes = Union[FileTypes, Sequence[FileTypes]]
|
||||
|
||||
|
||||
|
||||
@ -2,21 +2,24 @@ 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 = field(default_factory=dict)
|
||||
collection_name: str
|
||||
expand: dict
|
||||
|
||||
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.load_expanded()
|
||||
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):
|
||||
|
||||
@ -30,10 +30,7 @@ class AdminService(CrudService):
|
||||
"""
|
||||
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
|
||||
):
|
||||
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
|
||||
@ -46,10 +43,7 @@ class AdminService(CrudService):
|
||||
"""
|
||||
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
|
||||
):
|
||||
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
|
||||
|
||||
@ -76,7 +76,9 @@ class RealtimeService(BaseService):
|
||||
found = False
|
||||
for sub in self.subscriptions:
|
||||
found = True
|
||||
self.event_source.remove_event_listener(sub, self.subscriptions[sub])
|
||||
self.event_source.remove_event_listener(
|
||||
sub, self.subscriptions[sub]
|
||||
)
|
||||
self.subscriptions.pop(sub)
|
||||
if not found:
|
||||
return
|
||||
@ -139,13 +141,15 @@ class RealtimeService(BaseService):
|
||||
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)
|
||||
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.remove_event_listener(
|
||||
"PB_CONNECT", self._connect_handler)
|
||||
self.event_source.close()
|
||||
self.event_source = None
|
||||
|
||||
@ -74,16 +74,14 @@ class RecordService(CrudService):
|
||||
|
||||
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
|
||||
)
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -94,10 +92,7 @@ class RecordService(CrudService):
|
||||
"""
|
||||
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
|
||||
):
|
||||
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
|
||||
@ -110,11 +105,7 @@ class RecordService(CrudService):
|
||||
"""
|
||||
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
|
||||
):
|
||||
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
|
||||
@ -138,25 +129,18 @@ class RecordService(CrudService):
|
||||
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()
|
||||
}
|
||||
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", [])
|
||||
)
|
||||
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 = {},
|
||||
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.
|
||||
@ -166,7 +150,8 @@ class RecordService(CrudService):
|
||||
- the authentication token
|
||||
- the authenticated record model
|
||||
"""
|
||||
body_params.update({"identity": username_or_email, "password": password})
|
||||
body_params.update(
|
||||
{"identity": username_or_email, "password": password})
|
||||
response_data = self.client.send(
|
||||
self.base_collection_path() + "/auth-with-password",
|
||||
{
|
||||
@ -197,17 +182,15 @@ class RecordService(CrudService):
|
||||
- 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,
|
||||
}
|
||||
)
|
||||
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",
|
||||
self.base_collection_path() + "/auth-with-password",
|
||||
{
|
||||
"method": "POST",
|
||||
"params": query_params,
|
||||
@ -228,7 +211,11 @@ class RecordService(CrudService):
|
||||
return self.auth_response(
|
||||
self.client.send(
|
||||
self.base_collection_path() + "/auth-refresh",
|
||||
{"method": "POST", "params": query_params, "body": body_params},
|
||||
{
|
||||
"method": "POST",
|
||||
"params": query_params,
|
||||
"body": body_params
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -53,24 +53,27 @@ class BaseCrudService(BaseService, ABC):
|
||||
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}
|
||||
f"{base_path}/{quote(id)}",
|
||||
{
|
||||
"method": "GET",
|
||||
"params": query_params
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
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
|
||||
"The requested resource wasn't found.",
|
||||
status=404
|
||||
)
|
||||
|
||||
def _create(
|
||||
@ -95,6 +98,7 @@ class BaseCrudService(BaseService, ABC):
|
||||
|
||||
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}
|
||||
f"{base_path}/{quote(id)}", {"method": "DELETE",
|
||||
"params": query_params}
|
||||
)
|
||||
return True
|
||||
|
||||
@ -29,7 +29,7 @@ dynamic = ["readme", "version"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "pocketbase"
|
||||
version = "0.8.1"
|
||||
version = "0.2.2"
|
||||
description = "PocketBase SDK for python."
|
||||
authors = ["Vithor Jaeger <vaphes@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@ -5,7 +5,7 @@ from pocketbase.utils import camel_to_snake, to_datetime
|
||||
|
||||
|
||||
def test_version():
|
||||
assert __version__ == "0.8.1"
|
||||
assert __version__ == "0.2.2"
|
||||
|
||||
|
||||
def test_utils():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user