Skip to content

Commit 10e24f1

Browse files
antharuuAntharuu
andauthored
Add configurable option to disable automatic snake_case conversion (#123)
* feat: implement snake case field names for Record model * docs: add note about auto_snake_case conversion behavior --------- Co-authored-by: Antharuu <[email protected]>
1 parent 5e3482e commit 10e24f1

File tree

6 files changed

+58
-3
lines changed

6 files changed

+58
-3
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ result = client.collection("example").create(
5454
```
5555
> 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' 🙃.
5656
57+
---
58+
59+
**Note:** By default, camelCase (or any other key style) from the API is converted to snake_case in Python. You can keep the original keys by setting `auto_snake_case=False` when creating the client.
60+
61+
```python
62+
from pocketbase import Client
63+
client = Client(auto_snake_case=False)
64+
# Fields will keep their original names from the API
65+
```
66+
5767
## Development
5868

5969
These are the requirements for local development:

pocketbase/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ def __init__(
2727
auth_store: AuthStore | None = None,
2828
timeout: float = 120,
2929
http_client: httpx.Client | None = None,
30+
auto_snake_case: bool = True,
3031
) -> None:
3132
self.base_url = base_url
3233
self.lang = lang
3334
self.auth_store = auth_store or BaseAuthStore() # LocalAuthStore()
3435
self.timeout = timeout
3536
self.http_client = http_client or httpx.Client()
37+
self.auto_snake_case = auto_snake_case
3638
# services
3739
self.admins = AdminService(self)
3840
self.backups = BackupsService(self)

pocketbase/models/record.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def load(self, data: dict[str, Any]) -> None:
1515
super().load(data)
1616
self.expand = {}
1717
for key, value in data.items():
18-
key = camel_to_snake(key).replace("@", "")
18+
key = camel_to_snake(key, getattr(self, 'client', None) and getattr(self.client, 'auto_snake_case', True)).replace("@", "")
1919
setattr(self, key, value)
2020
self.load_expanded()
2121

pocketbase/services/record_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def list_auth_methods(
179179

180180
def apply_pythonic_keys(ap: dict[str, Any]) -> dict[str, Any]:
181181
pythonic_keys_ap = {
182-
camel_to_snake(key).replace("@", ""): value
182+
camel_to_snake(key, getattr(self.client, 'auto_snake_case', True)).replace("@", ""): value
183183
for key, value in ap.items()
184184
}
185185
return pythonic_keys_ap

pocketbase/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from .errors import ClientResponseError # noqa: F401
99

1010

11-
def camel_to_snake(name: str) -> str:
11+
def camel_to_snake(name: str, enabled: bool = True) -> str:
12+
if not enabled:
13+
return name
1214
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
1315
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
1416

tests/test_client_snake_case.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
from pocketbase import Client
3+
from pocketbase.models.record import Record
4+
from pocketbase.utils import camel_to_snake
5+
6+
class DummyClient:
7+
def __init__(self, auto_snake_case=True):
8+
self.auto_snake_case = auto_snake_case
9+
10+
class DummyRecord(Record):
11+
def __init__(self, data, client=None):
12+
self.client = client
13+
self.expand = {}
14+
self.load(data)
15+
16+
def test_camel_to_snake_enabled():
17+
assert camel_to_snake("TestCase") == "test_case"
18+
assert camel_to_snake("testCase") == "test_case"
19+
assert camel_to_snake("test_case") == "test_case"
20+
assert camel_to_snake("TestBS123") == "test_bs123"
21+
assert camel_to_snake("TestCase", enabled=False) == "TestCase"
22+
23+
def test_record_snake_case_enabled():
24+
client = DummyClient(auto_snake_case=True)
25+
data = {"myFieldName": "value", "anotherField": 42}
26+
record = DummyRecord(data, client=client)
27+
assert hasattr(record, "my_field_name")
28+
assert hasattr(record, "another_field")
29+
assert record.my_field_name == "value"
30+
assert record.another_field == 42
31+
32+
def test_record_snake_case_disabled():
33+
client = DummyClient(auto_snake_case=False)
34+
data = {"myFieldName": "value", "anotherField": 42}
35+
record = DummyRecord(data, client=client)
36+
assert hasattr(record, "myFieldName")
37+
assert hasattr(record, "anotherField")
38+
assert not hasattr(record, "my_field_name")
39+
assert not hasattr(record, "another_field")
40+
assert record.myFieldName == "value"
41+
assert record.anotherField == 42

0 commit comments

Comments
 (0)