Compare commits

..

4 Commits

Author SHA1 Message Date
Vithor Jaeger
bda1d2846d Revert "Add example of file upload to record (#9)"
This reverts commit 428d18c387547b94d6caaddbcb278d30d0e12dd2.
2022-11-25 20:45:26 -04:00
Martin
428d18c387
Add example of file upload to record (#9) 2022-11-25 20:44:11 -04:00
Martin
00c9365b5f
Port of PR #6 to branch 0.8.0 (#7)
* Switch from JSON to multipart file encoding for file upload

File upload is detected when body data contains a value of class FileUpload
Remaining JSON is converted to FormData
Enitre message is sent as multipart

* Switch from JSON to multipart file encoding for file upload (#6)

File upload is detected when body data contains a value of class FileUpload
Remaining JSON is converted to FormData
Enitre message is sent as multipart
2022-11-24 09:35:21 -04:00
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
14 changed files with 86 additions and 173 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

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

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,6 +1,5 @@
from httpx._types import FileTypes
from typing import Sequence, Union
FileUploadTypes = Union[FileTypes, Sequence[FileTypes]]

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