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

Add opt-in strict string format validation #451

Merged
merged 11 commits into from
Dec 5, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
.idea
.vscode
.run
*.iml
.env
Expand Down
1 change: 1 addition & 0 deletions docs/source/atproto/atproto_client.models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Submodules
atproto_client.models.dot_dict
atproto_client.models.languages
atproto_client.models.models_loader
atproto_client.models.string_formats
atproto_client.models.type_conversion
atproto_client.models.unknown_type
atproto_client.models.utils
7 changes: 7 additions & 0 deletions docs/source/atproto/atproto_client.models.string_formats.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
string\_formats
======================================

.. automodule:: atproto_client.models.string_formats
:members:
:undoc-members:
:show-inheritance:
30 changes: 30 additions & 0 deletions examples/advanced_usage/validate_string_formats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from atproto_client.models import string_formats
from pydantic import TypeAdapter, ValidationError

some_good_handle = 'test.bsky.social'
some_bad_handle = 'invalid@ @handle'

strict_validation_context = {'strict_string_format': True}
HandleTypeAdapter = TypeAdapter(string_formats.Handle)

assert string_formats._OPT_IN_KEY == 'strict_string_format' # noqa: S101

# values will not be validated if not opting in
sneaky_bad_handle = HandleTypeAdapter.validate_python(some_bad_handle)

assert sneaky_bad_handle == some_bad_handle # noqa: S101

print(f'{sneaky_bad_handle=}\n\n')

# values will be validated if opting in
validated_good_handle = HandleTypeAdapter.validate_python(some_good_handle, context=strict_validation_context)

assert validated_good_handle == some_good_handle # noqa: S101

print(f'{validated_good_handle=}\n\n')

try:
print('Trying to validate a bad handle with strict validation...')
HandleTypeAdapter.validate_python(some_bad_handle, context=strict_validation_context)
except ValidationError as e:
print(e)
MarshalX marked this conversation as resolved.
Show resolved Hide resolved
56 changes: 30 additions & 26 deletions packages/atproto_client/models/app/bsky/actor/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import typing_extensions as te
from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models import base
Expand All @@ -18,11 +20,11 @@
class ProfileViewBasic(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

did: str #: Did.
handle: str #: Handle.
did: string_formats.Did #: Did.
handle: string_formats.Handle #: Handle.
associated: t.Optional['models.AppBskyActorDefs.ProfileAssociated'] = None #: Associated.
avatar: t.Optional[str] = None #: Avatar.
created_at: t.Optional[str] = None #: Created at.
avatar: t.Optional[string_formats.Uri] = None #: Avatar.
created_at: t.Optional[string_formats.DateTime] = None #: Created at.
display_name: t.Optional[str] = Field(default=None, max_length=640) #: Display name.
labels: t.Optional[t.List['models.ComAtprotoLabelDefs.Label']] = None #: Labels.
viewer: t.Optional['models.AppBskyActorDefs.ViewerState'] = None #: Viewer.
Expand All @@ -35,14 +37,14 @@ class ProfileViewBasic(base.ModelBase):
class ProfileView(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

did: str #: Did.
handle: str #: Handle.
did: string_formats.Did #: Did.
handle: string_formats.Handle #: Handle.
associated: t.Optional['models.AppBskyActorDefs.ProfileAssociated'] = None #: Associated.
avatar: t.Optional[str] = None #: Avatar.
created_at: t.Optional[str] = None #: Created at.
avatar: t.Optional[string_formats.Uri] = None #: Avatar.
created_at: t.Optional[string_formats.DateTime] = None #: Created at.
description: t.Optional[str] = Field(default=None, max_length=2560) #: Description.
display_name: t.Optional[str] = Field(default=None, max_length=640) #: Display name.
indexed_at: t.Optional[str] = None #: Indexed at.
indexed_at: t.Optional[string_formats.DateTime] = None #: Indexed at.
labels: t.Optional[t.List['models.ComAtprotoLabelDefs.Label']] = None #: Labels.
viewer: t.Optional['models.AppBskyActorDefs.ViewerState'] = None #: Viewer.

Expand All @@ -54,17 +56,17 @@ class ProfileView(base.ModelBase):
class ProfileViewDetailed(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

did: str #: Did.
handle: str #: Handle.
did: string_formats.Did #: Did.
handle: string_formats.Handle #: Handle.
associated: t.Optional['models.AppBskyActorDefs.ProfileAssociated'] = None #: Associated.
avatar: t.Optional[str] = None #: Avatar.
banner: t.Optional[str] = None #: Banner.
created_at: t.Optional[str] = None #: Created at.
avatar: t.Optional[string_formats.Uri] = None #: Avatar.
banner: t.Optional[string_formats.Uri] = None #: Banner.
created_at: t.Optional[string_formats.DateTime] = None #: Created at.
description: t.Optional[str] = Field(default=None, max_length=2560) #: Description.
display_name: t.Optional[str] = Field(default=None, max_length=640) #: Display name.
followers_count: t.Optional[int] = None #: Followers count.
follows_count: t.Optional[int] = None #: Follows count.
indexed_at: t.Optional[str] = None #: Indexed at.
indexed_at: t.Optional[string_formats.DateTime] = None #: Indexed at.
joined_via_starter_pack: t.Optional['models.AppBskyGraphDefs.StarterPackViewBasic'] = (
None #: Joined via starter pack.
)
Expand Down Expand Up @@ -106,10 +108,10 @@ class ViewerState(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`. Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests."""

blocked_by: t.Optional[bool] = None #: Blocked by.
blocking: t.Optional[str] = None #: Blocking.
blocking: t.Optional[string_formats.AtUri] = None #: Blocking.
blocking_by_list: t.Optional['models.AppBskyGraphDefs.ListViewBasic'] = None #: Blocking by list.
followed_by: t.Optional[str] = None #: Followed by.
following: t.Optional[str] = None #: Following.
followed_by: t.Optional[string_formats.AtUri] = None #: Followed by.
following: t.Optional[string_formats.AtUri] = None #: Following.
known_followers: t.Optional['models.AppBskyActorDefs.KnownFollowers'] = None #: Known followers.
muted: t.Optional[bool] = None #: Muted.
muted_by_list: t.Optional['models.AppBskyGraphDefs.ListViewBasic'] = None #: Muted by list.
Expand Down Expand Up @@ -168,7 +170,9 @@ class ContentLabelPref(base.ModelBase):
visibility: t.Union[
t.Literal['ignore'], t.Literal['show'], t.Literal['warn'], t.Literal['hide'], str
] #: Visibility.
labeler_did: t.Optional[str] = None #: Which labeler does this preference apply to? If undefined, applies globally.
labeler_did: t.Optional[string_formats.Did] = (
None #: Which labeler does this preference apply to? If undefined, applies globally.
)

py_type: t.Literal['app.bsky.actor.defs#contentLabelPref'] = Field(
default='app.bsky.actor.defs#contentLabelPref', alias='$type', frozen=True
Expand Down Expand Up @@ -201,8 +205,8 @@ class SavedFeedsPrefV2(base.ModelBase):
class SavedFeedsPref(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

pinned: t.List[str] #: Pinned.
saved: t.List[str] #: Saved.
pinned: t.List[string_formats.AtUri] #: Pinned.
saved: t.List[string_formats.AtUri] #: Saved.
timeline_index: t.Optional[int] = None #: Timeline index.

py_type: t.Literal['app.bsky.actor.defs#savedFeedsPref'] = Field(
Expand All @@ -213,7 +217,7 @@ class SavedFeedsPref(base.ModelBase):
class PersonalDetailsPref(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

birth_date: t.Optional[str] = None #: The birth date of account owner.
birth_date: t.Optional[string_formats.DateTime] = None #: The birth date of account owner.

py_type: t.Literal['app.bsky.actor.defs#personalDetailsPref'] = Field(
default='app.bsky.actor.defs#personalDetailsPref', alias='$type', frozen=True
Expand Down Expand Up @@ -280,7 +284,7 @@ class MutedWord(base.ModelBase):
actor_target: t.Optional[t.Union[t.Literal['all'], t.Literal['exclude-following'], str]] = (
'all' #: Groups of users to apply the muted word to. If undefined, applies to all users.
)
expires_at: t.Optional[str] = (
expires_at: t.Optional[string_formats.DateTime] = (
None #: The date and time at which the muted word will expire and no longer be applied.
)
id: t.Optional[str] = None #: Id.
Expand All @@ -303,7 +307,7 @@ class MutedWordsPref(base.ModelBase):
class HiddenPostsPref(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

items: t.List[str] #: A list of URIs of posts the account owner has hidden.
items: t.List[string_formats.AtUri] #: A list of URIs of posts the account owner has hidden.

py_type: t.Literal['app.bsky.actor.defs#hiddenPostsPref'] = Field(
default='app.bsky.actor.defs#hiddenPostsPref', alias='$type', frozen=True
Expand All @@ -323,7 +327,7 @@ class LabelersPref(base.ModelBase):
class LabelerPrefItem(base.ModelBase):
"""Definition model for :obj:`app.bsky.actor.defs`."""

did: str #: Did.
did: string_formats.Did #: Did.

py_type: t.Literal['app.bsky.actor.defs#labelerPrefItem'] = Field(
default='app.bsky.actor.defs#labelerPrefItem', alias='$type', frozen=True
Expand Down Expand Up @@ -364,7 +368,7 @@ class Nux(base.ModelBase):
data: t.Optional[str] = Field(
default=None, max_length=3000
) #: Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.
expires_at: t.Optional[str] = (
expires_at: t.Optional[string_formats.DateTime] = (
None #: The date and time at which the NUX will expire and should be considered completed.
)

Expand Down
6 changes: 3 additions & 3 deletions packages/atproto_client/models/app/bsky/actor/get_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import typing as t

from atproto_client.models import base
from atproto_client.models import base, string_formats


class Params(base.ParamsModelBase):
"""Parameters model for :obj:`app.bsky.actor.getProfile`."""

actor: str #: Handle or DID of account to fetch profile of.
actor: string_formats.Handle #: Handle or DID of account to fetch profile of.


class ParamsDict(t.TypedDict):
actor: str #: Handle or DID of account to fetch profile of.
actor: string_formats.Handle #: Handle or DID of account to fetch profile of.
6 changes: 4 additions & 2 deletions packages/atproto_client/models/app/bsky/actor/get_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models import base
Expand All @@ -17,11 +19,11 @@
class Params(base.ParamsModelBase):
"""Parameters model for :obj:`app.bsky.actor.getProfiles`."""

actors: t.List[str] = Field(max_length=25) #: Actors.
actors: t.List[string_formats.Handle] = Field(max_length=25) #: Actors.


class ParamsDict(t.TypedDict):
actors: t.List[str] #: Actors.
actors: t.List[string_formats.Handle] #: Actors.


class Response(base.ResponseModelBase):
Expand Down
4 changes: 3 additions & 1 deletion packages/atproto_client/models/app/bsky/actor/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import typing_extensions as te
from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models.blob_ref import BlobRef
Expand All @@ -23,7 +25,7 @@ class Record(base.RecordModelBase):
None #: Small image to be displayed next to posts from account. AKA, 'profile picture'.
)
banner: t.Optional['BlobRef'] = None #: Larger horizontal image to display behind profile view.
created_at: t.Optional[str] = None #: Created at.
created_at: t.Optional[string_formats.DateTime] = None #: Created at.
description: t.Optional[str] = Field(default=None, max_length=2560) #: Free-form profile description text.
display_name: t.Optional[str] = Field(default=None, max_length=640) #: Display name.
joined_via_starter_pack: t.Optional['models.ComAtprotoRepoStrongRef.Main'] = None #: Joined via starter pack.
Expand Down
8 changes: 5 additions & 3 deletions packages/atproto_client/models/app/bsky/embed/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models.blob_ref import BlobRef
Expand All @@ -28,7 +30,7 @@ class External(base.ModelBase):

description: str #: Description.
title: str #: Title.
uri: str #: Uri.
uri: string_formats.Uri #: Uri.
thumb: t.Optional['BlobRef'] = None #: Thumb.

py_type: t.Literal['app.bsky.embed.external#external'] = Field(
Expand All @@ -51,8 +53,8 @@ class ViewExternal(base.ModelBase):

description: str #: Description.
title: str #: Title.
uri: str #: Uri.
thumb: t.Optional[str] = None #: Thumb.
uri: string_formats.Uri #: Uri.
thumb: t.Optional[string_formats.Uri] = None #: Thumb.

py_type: t.Literal['app.bsky.embed.external#viewExternal'] = Field(
default='app.bsky.embed.external#viewExternal', alias='$type', frozen=True
Expand Down
6 changes: 4 additions & 2 deletions packages/atproto_client/models/app/bsky/embed/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models.blob_ref import BlobRef
Expand Down Expand Up @@ -49,8 +51,8 @@ class ViewImage(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.images`."""

alt: str #: Alt text description of the image, for accessibility.
fullsize: str #: Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.
thumb: str #: Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.
fullsize: string_formats.Uri #: Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.
thumb: string_formats.Uri #: Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.
aspect_ratio: t.Optional['models.AppBskyEmbedDefs.AspectRatio'] = None #: Aspect ratio.

py_type: t.Literal['app.bsky.embed.images#viewImage'] = Field(
Expand Down
14 changes: 8 additions & 6 deletions packages/atproto_client/models/app/bsky/embed/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import typing_extensions as te
from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models.unknown_type import UnknownType
Expand Down Expand Up @@ -50,9 +52,9 @@ class ViewRecord(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.record`."""

author: 'models.AppBskyActorDefs.ProfileViewBasic' #: Author.
cid: str #: Cid.
indexed_at: str #: Indexed at.
uri: str #: Uri.
cid: string_formats.Cid #: Cid.
indexed_at: string_formats.DateTime #: Indexed at.
uri: string_formats.AtUri #: Uri.
value: 'UnknownType' #: The record data itself.
embeds: t.Optional[
t.List[
Expand Down Expand Up @@ -83,7 +85,7 @@ class ViewNotFound(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.record`."""

not_found: bool = Field(frozen=True) #: Not found.
uri: str #: Uri.
uri: string_formats.AtUri #: Uri.

py_type: t.Literal['app.bsky.embed.record#viewNotFound'] = Field(
default='app.bsky.embed.record#viewNotFound', alias='$type', frozen=True
Expand All @@ -95,7 +97,7 @@ class ViewBlocked(base.ModelBase):

author: 'models.AppBskyFeedDefs.BlockedAuthor' #: Author.
blocked: bool = Field(frozen=True) #: Blocked.
uri: str #: Uri.
uri: string_formats.AtUri #: Uri.

py_type: t.Literal['app.bsky.embed.record#viewBlocked'] = Field(
default='app.bsky.embed.record#viewBlocked', alias='$type', frozen=True
Expand All @@ -106,7 +108,7 @@ class ViewDetached(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.record`."""

detached: bool = Field(frozen=True) #: Detached.
uri: str #: Uri.
uri: string_formats.AtUri #: Uri.

py_type: t.Literal['app.bsky.embed.record#viewDetached'] = Field(
default='app.bsky.embed.record#viewDetached', alias='$type', frozen=True
Expand Down
10 changes: 6 additions & 4 deletions packages/atproto_client/models/app/bsky/embed/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from pydantic import Field

from atproto_client.models import string_formats

if t.TYPE_CHECKING:
from atproto_client import models
from atproto_client.models.blob_ref import BlobRef
Expand All @@ -32,7 +34,7 @@ class Caption(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.video`."""

file: 'BlobRef' #: File.
lang: str #: Lang.
lang: string_formats.Language #: Lang.

py_type: t.Literal['app.bsky.embed.video#caption'] = Field(
default='app.bsky.embed.video#caption', alias='$type', frozen=True
Expand All @@ -42,11 +44,11 @@ class Caption(base.ModelBase):
class View(base.ModelBase):
"""Definition model for :obj:`app.bsky.embed.video`."""

cid: str #: Cid.
playlist: str #: Playlist.
cid: string_formats.Cid #: Cid.
playlist: string_formats.Uri #: Playlist.
alt: t.Optional[str] = Field(default=None, max_length=10000) #: Alt.
aspect_ratio: t.Optional['models.AppBskyEmbedDefs.AspectRatio'] = None #: Aspect ratio.
thumbnail: t.Optional[str] = None #: Thumbnail.
thumbnail: t.Optional[string_formats.Uri] = None #: Thumbnail.

py_type: t.Literal['app.bsky.embed.video#view'] = Field(
default='app.bsky.embed.video#view', alias='$type', frozen=True
Expand Down
Loading
Loading