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

View File

@ -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
@ -22,65 +22,25 @@ The rule of thumb here is just to use it as you would <a href="https://github.co
```python
from pocketbase import PocketBase # Client also works the same
from pocketbase.client import FileUpload
client = PocketBase('http://127.0.0.1:8090')
# authenticate as regular user
user_data = client.collection("users").auth_with_password(
"user@example.com", "0123456789")
# or as admin
admin_data = client.admins.auth_with_password("test@example.com", "0123456789")
...
# list and filter "example" collection records
result = client.collection("example").get_list(
1, 20, {"filter": 'status = true && created > "2022-08-01 10:00:00"'})
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>

View File

@ -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):

View File

@ -5,7 +5,7 @@ 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 +14,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 +34,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 +61,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
@ -72,21 +71,6 @@ class Client:
params = config.get("params", None)
headers = config.get("headers", None)
body = config.get("body", None)
# handle requests including files as multipart:
data = {}
files = ()
for k, v in (body if isinstance(body, dict) else {}).items():
if isinstance(v, FileUpload):
files += v.get(k)
else:
data[k] = v
if len(files) > 0:
# discard body, switch to multipart encoding
body = None
else:
# discard files+data (do not use multipart encoding)
files = None
data = None
try:
response = httpx.request(
method=method,
@ -94,9 +78,7 @@ class Client:
params=params,
headers=headers,
json=body,
data=data,
files=files,
timeout=self.timeout,
timeout=120,
)
except Exception as e:
raise ClientResponseError(
@ -118,16 +100,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

View File

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

View File

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

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,20 +2,23 @@ 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.collection_id = data.get("@collectionId", "")
self.collection_name = data.get("@collectionName", "")
expand = data.get("@expand", {})
if expand:
self.expand = expand
self.load_expanded()
@classmethod

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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():