Compare commits

..

1 Commits

Author SHA1 Message Date
perfalle
10496553ed
update to pocketbase version 0.8.0-rc2 (#4)
update to pocketbase version 0.8.0-rc1

fixed flake8 errors,
but there are still remaining errors:
F401 ... imported but unused
E501 line too long (... > 79 characters)
E722 do not use bare 'except'
2022-11-08 16:50:38 +01:00
15 changed files with 84 additions and 204 deletions

View File

@ -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 }}

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2023 vaphes Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -10,10 +10,10 @@ This is in early development, and at first is just a translation of <a href="htt
## Installation ## Installation
Install PocketBase using PIP: Install PocketBase using pip:
```shell ```shell
python3 -m pip install pocketbase $ pip install pocketbase
``` ```
## Usage ## 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! 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 ```python
from pocketbase import PocketBase # Client also works the same from pocketbase import PocketBase # Client also works the same
from pocketbase.client import FileUpload
client = PocketBase('http://127.0.0.1:8090') client = PocketBase('http://127.0.0.1:8090')
# 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 # list and filter "example" collection records
result = client.collection("example").get_list( result = client.records.get_list(
1, 20, {"filter": 'status = true && created > "2022-08-01 10:00:00"'}) "example", 1, 20, {"filter": 'status = true && created > "2022-08-01 10:00:00"'}
)
# create record and upload file to image field # authenticate as regular user
result = client.collection("example").create( user_data = client.users.auth_via_email("test@example.com", "123456")
{
"status": "true", # or as admin
"image": FileUpload(("image.png", open("image.png", "rb"))), admin_data = client.admins.auth_via_email("test@example.com", "123456")
})
# and much more... # 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' 🙃. > 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: <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>
* 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.

View File

@ -1,9 +1,9 @@
__title__ = "pocketbase" __title__ = "pocketbase"
__description__ = "PocketBase client SDK for python." __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): class PocketBase(Client):

View File

@ -5,7 +5,7 @@ from urllib.parse import quote, urlencode
import httpx import httpx
from pocketbase.models import FileUpload from pocketbase.utils import ClientResponseError
from pocketbase.models.record import Record from pocketbase.models.record import Record
from pocketbase.services.admin_service import AdminService from pocketbase.services.admin_service import AdminService
from pocketbase.services.collection_service import CollectionService from pocketbase.services.collection_service import CollectionService
@ -14,7 +14,6 @@ from pocketbase.services.realtime_service import RealtimeService
from pocketbase.services.record_service import RecordService from pocketbase.services.record_service import RecordService
from pocketbase.services.settings_service import SettingsService from pocketbase.services.settings_service import SettingsService
from pocketbase.stores.base_auth_store import BaseAuthStore from pocketbase.stores.base_auth_store import BaseAuthStore
from pocketbase.utils import ClientResponseError
class Client: class Client:
@ -35,12 +34,10 @@ class Client:
base_url: str = "/", base_url: str = "/",
lang: str = "en-US", lang: str = "en-US",
auth_store: BaseAuthStore | None = None, auth_store: BaseAuthStore | None = None,
timeout: float = 120,
) -> None: ) -> None:
self.base_url = base_url self.base_url = base_url
self.lang = lang self.lang = lang
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore() self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
self.timeout = timeout
# services # services
self.admins = AdminService(self) self.admins = AdminService(self)
self.collections = CollectionService(self) self.collections = CollectionService(self)
@ -64,7 +61,9 @@ class Client:
"headers" not in config or "Authorization" not in config["headers"] "headers" not in config or "Authorization" not in config["headers"]
): ):
config["headers"] = config.get("headers", {}) config["headers"] = config.get("headers", {})
config["headers"].update({"Authorization": self.auth_store.token}) config["headers"].update(
{"Authorization": self.auth_store.token}
)
# build url + path # build url + path
url = self.build_url(path) url = self.build_url(path)
# send the request # send the request
@ -72,21 +71,6 @@ class Client:
params = config.get("params", None) params = config.get("params", None)
headers = config.get("headers", None) headers = config.get("headers", None)
body = config.get("body", None) body = config.get("body", None)
# handle requests including files as multipart:
data = {}
files = ()
for k, v in (body if isinstance(body, dict) else {}).items():
if isinstance(v, FileUpload):
files += v.get(k)
else:
data[k] = v
if len(files) > 0:
# discard body, switch to multipart encoding
body = None
else:
# discard files+data (do not use multipart encoding)
files = None
data = None
try: try:
response = httpx.request( response = httpx.request(
method=method, method=method,
@ -94,9 +78,7 @@ class Client:
params=params, params=params,
headers=headers, headers=headers,
json=body, json=body,
data=data, timeout=120,
files=files,
timeout=self.timeout,
) )
except Exception as e: except Exception as e:
raise ClientResponseError( raise ClientResponseError(
@ -118,16 +100,16 @@ class Client:
def get_file_url(self, record: Record, filename: str, query_params: dict): def get_file_url(self, record: Record, filename: str, query_params: dict):
parts = [ parts = [
"api", 'api',
"files", 'files',
quote(record.collection_id or record.collection_name), quote(record.collection_id or record.collection_name),
quote(record.id), quote(record.id),
quote(filename), quote(filename),
] ]
result = self.build_url("/".join(parts)) result = self.build_url('/'.join(parts))
if len(query_params) != 0: if len(query_params) != 0:
params: str = urlencode(query_params) params: str = urlencode(query_params)
result += "&" if "?" in result else "?" result += '&' if '?' in result else '?'
result += params result += params
return result return result

View File

@ -3,4 +3,3 @@ from .collection import Collection
from .external_auth import ExternalAuth from .external_auth import ExternalAuth
from .log_request import LogRequest from .log_request import LogRequest
from .record import Record from .record import Record
from .file_upload import FileUpload

View File

@ -37,10 +37,10 @@ class Collection(BaseModel):
self.schema.append(SchemaField(**field)) self.schema.append(SchemaField(**field))
def is_base(self): def is_base(self):
return self.type == "base" return self.type == 'base'
def is_auth(self): def is_auth(self):
return self.type == "auth" return self.type == 'auth'
def is_single(self): def is_single(self):
return self.type == "single" return self.type == 'single'

View File

@ -1,14 +0,0 @@
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),)

View File

@ -2,21 +2,24 @@ from __future__ import annotations
from pocketbase.models.utils.base_model import BaseModel from pocketbase.models.utils.base_model import BaseModel
from pocketbase.utils import camel_to_snake from pocketbase.utils import camel_to_snake
from dataclasses import dataclass, field
@dataclass
class Record(BaseModel): class Record(BaseModel):
collection_id: str collection_id: str
collection_name: str = "" collection_name: str
expand: dict = field(default_factory=dict) expand: dict
def load(self, data: dict) -> None: def load(self, data: dict) -> None:
super().load(data) super().load(data)
self.expand = {}
for key, value in data.items(): for key, value in data.items():
key = camel_to_snake(key).replace("@", "") key = camel_to_snake(key).replace("@", "")
setattr(self, key, value) setattr(self, key, value)
self.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 @classmethod
def parse_expanded(cls, data: dict): def parse_expanded(cls, data: dict):

View File

@ -30,10 +30,7 @@ class AdminService(CrudService):
""" """
item = super(AdminService).update(id, body_params) item = super(AdminService).update(id, body_params)
try: try:
if ( if self.client.auth_store.model.collection_id is not None and item.id == self.client.auth_store.model.id:
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) self.client.auth_store.save(self.client.auth_store.token, item)
except: except:
pass pass
@ -46,10 +43,7 @@ class AdminService(CrudService):
""" """
item = super(AdminService).delete(id, body_params) item = super(AdminService).delete(id, body_params)
try: try:
if ( if self.client.auth_store.model.collection_id is not None and item.id == self.client.auth_store.model.id:
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) self.client.auth_store.save(self.client.auth_store.token, item)
except: except:
pass pass

View File

@ -76,7 +76,9 @@ class RealtimeService(BaseService):
found = False found = False
for sub in self.subscriptions: for sub in self.subscriptions:
found = True 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) self.subscriptions.pop(sub)
if not found: if not found:
return return
@ -139,13 +141,15 @@ class RealtimeService(BaseService):
def _connect(self) -> None: def _connect(self) -> None:
self._disconnect() self._disconnect()
self.event_source = SSEClient(self.client.build_url("/api/realtime")) 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: def _disconnect(self) -> None:
self._remove_subscription_listeners() self._remove_subscription_listeners()
self.client_id = "" self.client_id = ""
if not self.event_source: if not self.event_source:
return 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.close()
self.event_source = None self.event_source = None

View File

@ -74,16 +74,14 @@ class RecordService(CrudService):
def subscribeOne(self, record_id: str, callback: Callable[[MessageData], None]): def subscribeOne(self, record_id: str, callback: Callable[[MessageData], None]):
"""Subscribe to the realtime changes of a single record in the collection.""" """Subscribe to the realtime changes of a single record in the collection."""
return self.client.realtime.subscribe( return self.client.realtime.subscribe(self.collection_id_or_name + '/' + record_id, callback)
self.collection_id_or_name + "/" + record_id, callback
)
def unsubscribe(self, *record_ids: List[str]): def unsubscribe(self, *record_ids: List[str]):
"""Subscribe to the realtime changes of a single record in the collection.""" """Subscribe to the realtime changes of a single record in the collection."""
if record_ids and len(record_ids) == 0: if record_ids and len(record_ids) == 0:
subs = [] subs = []
for id in record_ids: 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.unsubscribe(*subs)
return self.client.realtime.subscribe_by_prefix(self.collection_id_or_name) 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 item = super().update(id, body_params) # super(Record).update
try: try:
if ( if self.client.auth_store.model.collection_id is not None and item.id == self.client.auth_store.model.id:
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) self.client.auth_store.save(self.client.auth_store.token, item)
except: except:
pass pass
@ -110,11 +105,7 @@ class RecordService(CrudService):
""" """
success = super().delete(id, body_params) # super(Record).delete success = super().delete(id, body_params) # super(Record).delete
try: try:
if ( if success and self.client.auth_store.model.collection_id is not None and id == self.client.auth_store.model.id:
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() self.client.auth_store.clear()
except: except:
pass pass
@ -138,25 +129,18 @@ class RecordService(CrudService):
email_password = response_data.pop("emailPassword", False) email_password = response_data.pop("emailPassword", False)
def apply_pythonic_keys(ap): def apply_pythonic_keys(ap):
pythonic_keys_ap = { pythonic_keys_ap = {camel_to_snake(key).replace(
camel_to_snake(key).replace("@", ""): value for key, value in ap.items() "@", ""): value for key, value in ap.items()}
}
return pythonic_keys_ap return pythonic_keys_ap
auth_providers = [ auth_providers = [
AuthProviderInfo(**auth_provider) AuthProviderInfo(**auth_provider)
for auth_provider in map( for auth_provider in map(apply_pythonic_keys, response_data.get("authProviders", []))
apply_pythonic_keys, response_data.get("authProviders", [])
)
] ]
return AuthMethodsList(username_password, email_password, auth_providers) return AuthMethodsList(username_password, email_password, auth_providers)
def auth_with_password( def auth_with_password(
self, self, username_or_email: str, password: str, body_params: dict = {}, query_params: dict = {}
username_or_email: str,
password: str,
body_params: dict = {},
query_params: dict = {},
) -> RecordAuthResponse: ) -> RecordAuthResponse:
""" """
Authenticate a single auth collection record via its username/email and password. Authenticate a single auth collection record via its username/email and password.
@ -166,7 +150,8 @@ class RecordService(CrudService):
- the authentication token - the authentication token
- the authenticated record model - 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( response_data = self.client.send(
self.base_collection_path() + "/auth-with-password", self.base_collection_path() + "/auth-with-password",
{ {
@ -197,17 +182,15 @@ class RecordService(CrudService):
- the authenticated record model - the authenticated record model
- the OAuth2 account data (eg. name, email, avatar, etc.) - the OAuth2 account data (eg. name, email, avatar, etc.)
""" """
body_params.update( body_params.update({
{ 'provider': provider,
"provider": provider, 'code': code,
"code": code, 'codeVerifier': code_verifier,
"codeVerifier": code_verifier, 'redirectUrl': redirct_url,
"redirectUrl": redirct_url, 'createData': create_data,
"createData": create_data, })
}
)
response_data = self.client.send( response_data = self.client.send(
self.base_collection_path() + "/auth-with-oauth2", self.base_collection_path() + "/auth-with-password",
{ {
"method": "POST", "method": "POST",
"params": query_params, "params": query_params,
@ -228,7 +211,11 @@ class RecordService(CrudService):
return self.auth_response( return self.auth_response(
self.client.send( self.client.send(
self.base_collection_path() + "/auth-refresh", self.base_collection_path() + "/auth-refresh",
{"method": "POST", "params": query_params, "body": body_params}, {
"method": "POST",
"params": query_params,
"body": body_params
},
) )
) )

View File

@ -53,24 +53,27 @@ class BaseCrudService(BaseService, ABC):
def _get_one(self, base_path: str, id: str, query_params: dict = {}) -> BaseModel: def _get_one(self, base_path: str, id: str, query_params: dict = {}) -> BaseModel:
return self.decode( return self.decode(
self.client.send( 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={}): def _get_first_list_item(self, base_path: str, filter: str, query_params={}):
query_params.update( query_params.update({
{ 'filter': filter,
"filter": filter, '$cancelKey': 'one_by_filter_' + base_path + '_' + filter,
"$cancelKey": "one_by_filter_" + base_path + "_" + filter, })
}
)
result = self._get_list(base_path, 1, 1, query_params) result = self._get_list(base_path, 1, 1, query_params)
try: try:
if len(result.items) == 0: if len(result.items) == 0:
raise raise
except: except:
raise ClientResponseError( raise ClientResponseError(
"The requested resource wasn't found.", status=404 "The requested resource wasn't found.",
status=404
) )
def _create( def _create(
@ -95,6 +98,7 @@ class BaseCrudService(BaseService, ABC):
def _delete(self, base_path: str, id: str, query_params: dict = {}) -> bool: def _delete(self, base_path: str, id: str, query_params: dict = {}) -> bool:
self.client.send( 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 return True

View File

@ -29,7 +29,7 @@ dynamic = ["readme", "version"]
[tool.poetry] [tool.poetry]
name = "pocketbase" name = "pocketbase"
version = "0.8.1" version = "0.2.2"
description = "PocketBase SDK for python." description = "PocketBase SDK for python."
authors = ["Vithor Jaeger <vaphes@gmail.com>"] authors = ["Vithor Jaeger <vaphes@gmail.com>"]
readme = "README.md" readme = "README.md"

View File

@ -5,7 +5,7 @@ from pocketbase.utils import camel_to_snake, to_datetime
def test_version(): def test_version():
assert __version__ == "0.8.1" assert __version__ == "0.2.2"
def test_utils(): def test_utils():