Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use jiter instead of built-in json module to improve performance #360

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/fix_title_of_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def main(path: Path) -> None:
for file in files:
if file.startswith('atproto_client.models.'):
file_path = os.path.join(root, file)
with open(file_path, 'r', encoding='UTF-8') as f:
with open(file_path, encoding='UTF-8') as f:
content = f.read()
content = content.replace('atproto\\_client.models.', '')
with open(file_path, 'w', encoding='UTF-8') as f:
Expand Down
2 changes: 1 addition & 1 deletion docs/gen_aliases_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
def main(init_path: Path, output_path: Path) -> None:
aliases_db = ['ALIASES_DB = {']

with open(init_path, 'r', encoding='UTF-8') as f:
with open(init_path, encoding='UTF-8') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.names:
Expand Down
4 changes: 2 additions & 2 deletions examples/advanced_usage/session_reuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

def get_session() -> Optional[str]:
try:
with open('session.txt') as f:
with open('session.txt', encoding='UTF-8') as f:
return f.read()
except FileNotFoundError:
return None


def save_session(session_string: str) -> None:
with open('session.txt', 'w') as f:
with open('session.txt', 'w', encoding='UTF-8') as f:
f.write(session_string)


Expand Down
19 changes: 11 additions & 8 deletions packages/atproto_client/models/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import json
import types
import typing as t

import typing_extensions as te
from pydantic import ValidationError
from pydantic_core import from_json, to_json

from atproto_client import models
from atproto_client.exceptions import (
Expand Down Expand Up @@ -126,20 +126,23 @@ def get_model_as_dict(model: t.Union[DotDict, BlobRef, ModelBase]) -> t.Dict[str

def get_model_as_json(model: t.Union[DotDict, BlobRef, ModelBase]) -> str:
if isinstance(model, DotDict):
return json.dumps(get_model_as_dict(model))
return to_json(get_model_as_dict(model)).decode('UTF-8')

return model.model_dump_json(exclude_none=True, by_alias=True)


def is_json(json_data: t.Union[str, bytes]) -> bool:
if isinstance(json_data, bytes):
json_data.decode('UTF-8')
return load_json(json_data, strict=False) is not None


def load_json(json_data: t.Union[str, bytes], strict: bool = True) -> t.Optional[t.Dict[str, t.Any]]:
try:
json.loads(json_data)
return True
except: # noqa
return False
return from_json(json_data)
except ValueError as e:
if strict:
raise e

return None


def is_record_type(model: t.Union[ModelBase, DotDict], expected_type: t.Union[str, types.ModuleType]) -> bool:
Expand Down
12 changes: 6 additions & 6 deletions packages/atproto_client/request.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import json
import typing as t
from dataclasses import dataclass

import httpx
import typing_extensions as te
from pydantic_core import from_json

from atproto_client import exceptions
from atproto_client.models.common import XrpcError
from atproto_client.models.utils import get_or_create, is_json
from atproto_client.models.utils import get_or_create, load_json


@dataclass
Expand Down Expand Up @@ -35,7 +35,7 @@ def _convert_headers_to_dict(headers: httpx.Headers) -> t.Dict[str, str]:
def _parse_response(response: httpx.Response) -> Response:
content = response.content
if response.headers.get('content-type') == 'application/json; charset=utf-8':
content = response.json()
content = from_json(response.content)

return Response(
success=True,
Expand Down Expand Up @@ -65,9 +65,9 @@ def _handle_response(response: httpx.Response) -> httpx.Response:
content=response.content,
headers=_convert_headers_to_dict(response.headers),
)
if response.content and is_json(response.content):
data: t.Dict[str, t.Any] = json.loads(response.content)
error_response.content = t.cast(XrpcError, get_or_create(data, XrpcError, strict=False))
error_content = load_json(response.content, strict=False)
if error_content:
error_response.content = t.cast(XrpcError, get_or_create(error_content, XrpcError, strict=False))

if response.status_code in {401, 403}:
raise exceptions.UnauthorizedError(error_response)
Expand Down
2 changes: 1 addition & 1 deletion packages/atproto_codegen/clients/generate_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def gen_client(input_filename: str, output_filename: str) -> None:
with open(_CLIENT_DIR.joinpath(input_filename), 'r', encoding='UTF-8') as f:
with open(_CLIENT_DIR.joinpath(input_filename), encoding='UTF-8') as f:
code = f.read()

# TODO(MarshalX): Get automatically
Expand Down
5 changes: 3 additions & 2 deletions packages/atproto_identity/did/resolvers/plc_resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing as t

import httpx
from pydantic_core import from_json

from atproto_identity.did.resolvers.base_resolver import AsyncBaseResolver, BaseResolver
from atproto_identity.exceptions import DidPlcResolverError
Expand Down Expand Up @@ -31,7 +32,7 @@ def resolve_without_validation(self, did: str) -> t.Optional[t.Dict[str, t.Any]]
return None

response.raise_for_status()
return response.json()
return from_json(response.content)
except httpx.HTTPError as e:
raise DidPlcResolverError(f'Error resolving DID {did}') from e

Expand All @@ -58,6 +59,6 @@ async def resolve_without_validation(self, did: str) -> t.Optional[t.Dict[str, t
return None

response.raise_for_status()
return response.json()
return from_json(response.content)
except httpx.HTTPError as e:
raise DidPlcResolverError(f'Error resolving DID {did}') from e
5 changes: 3 additions & 2 deletions packages/atproto_identity/did/resolvers/web_resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing as t

import httpx
from pydantic_core import from_json

from atproto_identity.did.resolvers.base_resolver import AsyncBaseResolver, BaseResolver
from atproto_identity.exceptions import DidWebResolverError, PoorlyFormattedDidError, UnsupportedDidWebPathError
Expand Down Expand Up @@ -45,7 +46,7 @@ def resolve_without_validation(self, did: str) -> t.Dict[str, t.Any]:
try:
response = self._client.get(url, timeout=self._timeout)
response.raise_for_status()
return response.json()
return from_json(response.content)
except httpx.HTTPError as e:
raise DidWebResolverError(f'Error resolving DID {did}') from e

Expand All @@ -68,6 +69,6 @@ async def resolve_without_validation(self, did: str) -> t.Dict[str, t.Any]:
try:
response = await self._client.get(url, timeout=self._timeout)
response.raise_for_status()
return response.json()
return from_json(response.content)
except httpx.HTTPError as e:
raise DidWebResolverError(f'Error resolving DID {did}') from e
5 changes: 3 additions & 2 deletions packages/atproto_lexicon/parser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import json
import os
import typing as t
from pathlib import Path

from pydantic_core import from_json

from atproto_lexicon import models
from atproto_lexicon.exceptions import LexiconParsingError

Expand All @@ -19,7 +20,7 @@ def lexicon_parse(data: dict, model_class: t.Optional[t.Type[L]] = models.Lexico
def lexicon_parse_file(lexicon_path: t.Union[Path, str], *, soft_fail: bool = False) -> t.Optional[models.LexiconDoc]:
try:
with open(lexicon_path, encoding='UTF-8') as f:
plain_lexicon = json.loads(f.read())
plain_lexicon = from_json(f.read())
return lexicon_parse(plain_lexicon)
except Exception as e: # noqa: BLE001
if soft_fail:
Expand Down
8 changes: 4 additions & 4 deletions packages/atproto_server/auth/jwt.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import binascii
import json
import typing as t
from datetime import datetime, timezone

from atproto_crypto.verify import verify_signature
from pydantic import BaseModel, ConfigDict
from pydantic_core import from_json

from atproto_server.auth.utils import base64url_decode
from atproto_server.exceptions import (
Expand Down Expand Up @@ -64,14 +64,14 @@ def parse_jwt(jwt: t.Union[str, bytes]) -> t.Tuple[bytes, bytes, t.Dict[str, t.A
raise TokenDecodeError('Invalid header padding') from e

try:
header = json.loads(header_data)
header = from_json(header_data)
except ValueError as e:
raise TokenDecodeError(f'Invalid header string: {e}') from e

if not isinstance(header, dict):
raise TokenDecodeError('Invalid header string: must be a json object')

header = t.cast(t.Dict[str, t.Any], json.loads(header_data)) # we expect object in header
header = t.cast(t.Dict[str, t.Any], from_json(header_data)) # we expect an object in header

try:
payload = base64url_decode(payload_segment)
Expand All @@ -96,7 +96,7 @@ def decode_jwt_payload(payload: t.Union[str, bytes]) -> JwtPayload:
:obj:`JwtPayload`: The decoded payload of the given JWT.
"""
try:
plain_payload = json.loads(payload)
plain_payload = from_json(payload)
except ValueError as e:
raise TokenDecodeError(f'Invalid payload string: {e}') from e
if not isinstance(plain_payload, dict):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_atproto_client/models/fetch_test_data.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json
import logging
import os
import typing as t

from atproto_client import Client
from atproto_client.request import Request, Response
from pydantic_core import to_json

if t.TYPE_CHECKING:
from atproto_client.models.common import XrpcError
Expand Down Expand Up @@ -73,7 +73,7 @@ def get_unique_filename(name: str) -> str:


def get_pretty_json(data: dict) -> str:
return json.dumps(data, indent=4)
return to_json(data, indent=4).decode('UTF-8')


def save_response(name: str) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"uri": "at://did:plc:s6jnht6koorxz7trghirytmf/app.bsky.feed.generator/atproto",
"cid": "bafyreifiexhek65jxj3ucz6y6zstj45rmtigybzygjkg6lretyqgtge5ai",
"value": {
"did": "did:web:feed.atproto.blue",
"$type": "app.bsky.feed.generator",
"avatar": {
"$type": "blob",
Expand All @@ -14,6 +13,7 @@
},
"createdAt": "2023-07-20T10:17:40.298101",
"description": "Posts related to the protocol. Powered by The AT Protocol SDK for Python",
"did": "did:web:feed.atproto.blue",
"displayName": "AT Protocol"
}
}
41 changes: 27 additions & 14 deletions tests/test_atproto_client/models/test_data/get_follows.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:vpkhqolt662uhesyj6nxm7ys/bafkreietenc2aywhzxdyvalgffxl35tvxri6wv2iwryfdcdoecdfg553fy@jpeg",
"associated": {
"chat": {
"allowIncoming": "following"
"allowIncoming": "all"
}
},
"viewer": {
Expand All @@ -16,17 +16,18 @@
"following": "at://did:plc:kvwvcn5iqfooopmyzvb4qzba/app.bsky.graph.follow/3jueqtsliw22n"
},
"labels": [],
"description": "Technical advisor to @bluesky, first engineer at Protocol Labs. Wizard Utopian",
"indexedAt": "2024-05-14T15:42:22.889Z"
"createdAt": "2022-11-17T01:04:43.624Z",
"description": "Technical advisor to @bluesky, first engineer at Protocol Labs. Wizard Utopian.",
"indexedAt": "2024-06-07T03:19:53.642Z"
},
{
"did": "did:plc:l3rouwludahu3ui3bt66mfvj",
"handle": "divy.zone",
"displayName": "devin ivy \ud83d\udc0b",
"displayName": "devin ivy 🐋",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:l3rouwludahu3ui3bt66mfvj/bafkreicg6y3mlr3eszmbjm3swyuncwf4ruzohcsvtcbrjzyhkwibtx7nyy@jpeg",
"associated": {
"chat": {
"allowIncoming": "following"
"allowIncoming": "all"
}
},
"viewer": {
Expand All @@ -36,6 +37,7 @@
"cid": "bafyreigk3kmjipz5emav5vqmvdtivwz757tfpg5lnsbfoscnqx7wpjjime",
"name": "test mute list",
"purpose": "app.bsky.graph.defs#modlist",
"listItemCount": 1,
"indexedAt": "2023-08-28T10:08:27.442Z",
"labels": [],
"viewer": {
Expand All @@ -46,13 +48,14 @@
"following": "at://did:plc:kvwvcn5iqfooopmyzvb4qzba/app.bsky.graph.follow/3jueqt6dbqs2g"
},
"labels": [],
"description": "\ud83c\udf00 bluesky team",
"indexedAt": "2024-03-08T04:03:32.618Z"
"createdAt": "2022-11-17T00:39:19.084Z",
"description": "🌀 bluesky team",
"indexedAt": "2024-06-14T20:22:02.642Z"
},
{
"did": "did:plc:oky5czdrnfjpqslsw2a5iclo",
"handle": "jay.bsky.team",
"displayName": "Jay \ud83e\udd8b",
"displayName": "Jay 🦋",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreihidru2xruxdxlvvcixc7lbgoudzicjbrdgacdhdhxyfw4yut4nfq@jpeg",
"associated": {
"chat": {
Expand All @@ -65,7 +68,8 @@
"following": "at://did:plc:kvwvcn5iqfooopmyzvb4qzba/app.bsky.graph.follow/3judl7ak7gp2f"
},
"labels": [],
"description": "CEO of Bluesky, steward of AT Protocol. \n\nLet\u2019s build a federated republic, starting with this server. \ud83c\udf31 \ud83e\udeb4 \ud83c\udf33 ",
"createdAt": "2022-11-17T06:31:40.296Z",
"description": "CEO of Bluesky, steward of AT Protocol. \n\nLet’s build a federated republic, starting with this server. 🌱 🪴 🌳 ",
"indexedAt": "2024-02-06T22:21:45.352Z"
},
{
Expand All @@ -84,21 +88,28 @@
"following": "at://did:plc:kvwvcn5iqfooopmyzvb4qzba/app.bsky.graph.follow/3judkza2vrb2y"
},
"labels": [],
"description": "Official Bluesky account (check domain\ud83d\udc46)\n\nFollow for updates and announcements",
"createdAt": "2023-04-12T04:53:57.057Z",
"description": "Official Bluesky account (check domain👆)\n\nFollow for updates and announcements",
"indexedAt": "2024-01-25T23:46:28.776Z"
},
{
"did": "did:plc:m2jwplpernhxkzbo4ev5ljwj",
"handle": "vercel.com",
"displayName": "Vercel",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:m2jwplpernhxkzbo4ev5ljwj/bafkreicebob2yf5lv6yg72luzv5qwsr6ob65j6oc3jciyowqkfiz736oqu@jpeg",
"associated": {
"chat": {
"allowIncoming": "following"
}
},
"viewer": {
"muted": false,
"blockedBy": false,
"following": "at://did:plc:kvwvcn5iqfooopmyzvb4qzba/app.bsky.graph.follow/3judkwijszc25"
},
"labels": [],
"description": "Vercel\u2019s frontend cloud gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web. Creators of @nextjs.org.",
"createdAt": "2023-04-25T00:08:45.850Z",
"description": "Vercel’s frontend cloud gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web. Creators of @nextjs.org.",
"indexedAt": "2024-02-16T21:57:28.740Z"
},
{
Expand All @@ -118,7 +129,8 @@
"followedBy": "at://did:plc:s6jnht6koorxz7trghirytmf/app.bsky.graph.follow/3jucc25a7qs2k"
},
"labels": [],
"description": "Software Engineer\n\n\ud83d\udc0d The AT Protocol SDK for Python: https://atproto.blue/\n\ud83c\udf7f Custom Feed in Python: https://github.com/MarshalX/bluesky-feed-generator\n\ud83c\udfce\ufe0f Fast DAG-CBOR decoder for Python: https://github.com/MarshalX/python-libipld\n\nhttps://marshal.dev",
"createdAt": "2023-04-12T11:14:00.501Z",
"description": "Software Engineer\n\n🐍 The AT Protocol SDK for Python: https://atproto.blue/\n🍿 Custom Feed in Python: https://github.com/MarshalX/bluesky-feed-generator\n🏎️ Fast DAG-CBOR decoder for Python: https://github.com/MarshalX/python-libipld\n\nhttps://marshal.dev",
"indexedAt": "2024-01-26T00:15:07.447Z"
}
],
Expand All @@ -137,7 +149,8 @@
"blockedBy": false
},
"labels": [],
"description": "account for tests",
"indexedAt": "2024-05-22T21:04:02.588Z"
"createdAt": "2023-04-26T19:05:34.249Z",
"description": "account for tests\n\nAuthor: @marshal.dev\nGitHub: https://github.com/MarshalX/atproto\nWebsite: https://atproto.blue",
"indexedAt": "2024-05-22T21:19:48.088Z"
}
}
Loading
Loading