strapi-kit 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. strapi_kit/__init__.py +97 -0
  2. strapi_kit/__version__.py +15 -0
  3. strapi_kit/_version.py +34 -0
  4. strapi_kit/auth/__init__.py +7 -0
  5. strapi_kit/auth/api_token.py +48 -0
  6. strapi_kit/cache/__init__.py +5 -0
  7. strapi_kit/cache/schema_cache.py +211 -0
  8. strapi_kit/client/__init__.py +11 -0
  9. strapi_kit/client/async_client.py +1032 -0
  10. strapi_kit/client/base.py +460 -0
  11. strapi_kit/client/sync_client.py +980 -0
  12. strapi_kit/config_provider.py +368 -0
  13. strapi_kit/exceptions/__init__.py +37 -0
  14. strapi_kit/exceptions/errors.py +205 -0
  15. strapi_kit/export/__init__.py +10 -0
  16. strapi_kit/export/exporter.py +384 -0
  17. strapi_kit/export/importer.py +619 -0
  18. strapi_kit/export/media_handler.py +322 -0
  19. strapi_kit/export/relation_resolver.py +172 -0
  20. strapi_kit/models/__init__.py +104 -0
  21. strapi_kit/models/bulk.py +69 -0
  22. strapi_kit/models/config.py +174 -0
  23. strapi_kit/models/enums.py +97 -0
  24. strapi_kit/models/export_format.py +166 -0
  25. strapi_kit/models/import_options.py +142 -0
  26. strapi_kit/models/request/__init__.py +1 -0
  27. strapi_kit/models/request/fields.py +65 -0
  28. strapi_kit/models/request/filters.py +611 -0
  29. strapi_kit/models/request/pagination.py +168 -0
  30. strapi_kit/models/request/populate.py +281 -0
  31. strapi_kit/models/request/query.py +429 -0
  32. strapi_kit/models/request/sort.py +147 -0
  33. strapi_kit/models/response/__init__.py +1 -0
  34. strapi_kit/models/response/base.py +75 -0
  35. strapi_kit/models/response/component.py +67 -0
  36. strapi_kit/models/response/media.py +91 -0
  37. strapi_kit/models/response/meta.py +44 -0
  38. strapi_kit/models/response/normalized.py +168 -0
  39. strapi_kit/models/response/relation.py +48 -0
  40. strapi_kit/models/response/v4.py +70 -0
  41. strapi_kit/models/response/v5.py +57 -0
  42. strapi_kit/models/schema.py +93 -0
  43. strapi_kit/operations/__init__.py +16 -0
  44. strapi_kit/operations/media.py +226 -0
  45. strapi_kit/operations/streaming.py +144 -0
  46. strapi_kit/parsers/__init__.py +5 -0
  47. strapi_kit/parsers/version_detecting.py +171 -0
  48. strapi_kit/protocols.py +455 -0
  49. strapi_kit/utils/__init__.py +15 -0
  50. strapi_kit/utils/rate_limiter.py +201 -0
  51. strapi_kit/utils/uid.py +88 -0
  52. strapi_kit-0.0.1.dist-info/METADATA +1098 -0
  53. strapi_kit-0.0.1.dist-info/RECORD +55 -0
  54. strapi_kit-0.0.1.dist-info/WHEEL +4 -0
  55. strapi_kit-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,67 @@
1
+ """Component and dynamic zone models for Strapi responses.
2
+
3
+ Models for handling components and dynamic zones which have special structure.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class Component(BaseModel):
12
+ """Strapi component.
13
+
14
+ Components are reusable data structures that can be used across content types.
15
+ They have a special '__component' field identifying their type.
16
+
17
+ Attributes:
18
+ id: Component instance ID
19
+ document_id: Document ID (v5 only)
20
+ component_type: Component type identifier (from __component field)
21
+ Additional fields are allowed based on component schema.
22
+
23
+ Examples:
24
+ >>> component = Component(
25
+ ... id=1,
26
+ ... component_type="shared.seo",
27
+ ... title="SEO Title",
28
+ ... description="SEO Description"
29
+ ... )
30
+ >>> component.component_type
31
+ 'shared.seo'
32
+ """
33
+
34
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
35
+
36
+ id: int | None = Field(None, description="Component instance ID")
37
+ document_id: str | None = Field(None, alias="documentId", description="Document ID (v5)")
38
+ component_type: str = Field(..., alias="__component", description="Component type")
39
+
40
+
41
+ class DynamicZoneBlock(BaseModel):
42
+ """Dynamic zone block.
43
+
44
+ Dynamic zones are arrays of components of different types.
45
+ Each block has a '__component' field identifying its type.
46
+
47
+ Attributes:
48
+ id: Block instance ID
49
+ document_id: Document ID (v5 only)
50
+ component_type: Block type identifier (from __component field)
51
+ Additional fields are allowed based on block schema.
52
+
53
+ Examples:
54
+ >>> block = DynamicZoneBlock(
55
+ ... id=1,
56
+ ... component_type="content.rich-text",
57
+ ... body="<p>Rich text content</p>"
58
+ ... )
59
+ >>> block.component_type
60
+ 'content.rich-text'
61
+ """
62
+
63
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
64
+
65
+ id: int | None = Field(None, description="Block instance ID")
66
+ document_id: str | None = Field(None, alias="documentId", description="Document ID (v5)")
67
+ component_type: str = Field(..., alias="__component", description="Block type")
@@ -0,0 +1,91 @@
1
+ """Media file models for Strapi file uploads.
2
+
3
+ Models for parsing media/file fields with multiple formats.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+
14
+ class MediaFormat(BaseModel):
15
+ """A specific format/size of a media file.
16
+
17
+ Strapi generates multiple formats for images (thumbnail, small, medium, large).
18
+
19
+ Attributes:
20
+ name: Format name (e.g., "thumbnail", "small", "medium", "large")
21
+ hash: File hash
22
+ ext: File extension
23
+ mime: MIME type
24
+ width: Image width in pixels
25
+ height: Image height in pixels
26
+ size: File size in KB
27
+ path: File path (if applicable)
28
+ url: URL to access the file
29
+ """
30
+
31
+ model_config = ConfigDict(extra="allow")
32
+
33
+ name: str = Field(..., description="Format name")
34
+ hash: str = Field(..., description="File hash")
35
+ ext: str = Field(..., description="File extension")
36
+ mime: str = Field(..., description="MIME type")
37
+ width: int | None = Field(None, description="Image width")
38
+ height: int | None = Field(None, description="Image height")
39
+ size: float = Field(..., description="File size in KB")
40
+ path: str | None = Field(None, description="File path")
41
+ url: str = Field(..., description="File URL")
42
+
43
+
44
+ class MediaFile(BaseModel):
45
+ """Media file entity from Strapi.
46
+
47
+ Represents uploaded files with multiple format variations.
48
+
49
+ Attributes:
50
+ id: Numeric file ID
51
+ document_id: Document ID (v5 only)
52
+ name: Original filename
53
+ alternative_text: Alt text for accessibility
54
+ caption: Image caption
55
+ width: Original width
56
+ height: Original height
57
+ formats: Available formats (thumbnail, small, medium, large)
58
+ hash: File hash
59
+ ext: File extension
60
+ mime: MIME type
61
+ size: File size in KB
62
+ url: URL to original file
63
+ preview_url: Preview URL
64
+ provider: Storage provider (local, s3, etc.)
65
+ provider_metadata: Provider-specific metadata
66
+ created_at: Upload timestamp
67
+ updated_at: Last update timestamp
68
+ """
69
+
70
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
71
+
72
+ id: int = Field(..., description="Numeric file ID")
73
+ document_id: str | None = Field(None, alias="documentId", description="Document ID (v5)")
74
+ name: str = Field(..., description="Original filename")
75
+ alternative_text: str | None = Field(None, alias="alternativeText")
76
+ caption: str | None = Field(None, description="Image caption")
77
+ width: int | None = Field(None, description="Image width")
78
+ height: int | None = Field(None, description="Image height")
79
+ formats: dict[str, MediaFormat] | None = Field(None, description="Available formats")
80
+ hash: str = Field(..., description="File hash")
81
+ ext: str = Field(..., description="File extension")
82
+ mime: str = Field(..., description="MIME type")
83
+ size: float = Field(..., description="File size in KB")
84
+ url: str = Field(..., description="File URL")
85
+ preview_url: str | None = Field(None, alias="previewUrl")
86
+ provider: str = Field(..., description="Storage provider")
87
+ provider_metadata: dict[str, Any] | None = Field(
88
+ None, alias="providerMetadata", description="Provider metadata"
89
+ )
90
+ created_at: datetime | None = Field(None, alias="createdAt")
91
+ updated_at: datetime | None = Field(None, alias="updatedAt")
@@ -0,0 +1,44 @@
1
+ """Metadata models for Strapi API responses.
2
+
3
+ Contains pagination metadata and response metadata structures.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class PaginationMeta(BaseModel):
12
+ """Pagination metadata from Strapi responses.
13
+
14
+ Attributes:
15
+ page: Current page number (1-indexed)
16
+ page_size: Number of items per page
17
+ page_count: Total number of pages
18
+ total: Total number of items across all pages
19
+ """
20
+
21
+ model_config = ConfigDict(populate_by_name=True)
22
+
23
+ page: int | None = Field(None, description="Current page number")
24
+ page_size: int | None = Field(None, alias="pageSize", description="Items per page")
25
+ page_count: int | None = Field(None, alias="pageCount", description="Total pages")
26
+ total: int | None = Field(None, description="Total items")
27
+
28
+
29
+ class ResponseMeta(BaseModel):
30
+ """Response metadata from Strapi.
31
+
32
+ Contains pagination information and other metadata like available locales.
33
+
34
+ Attributes:
35
+ pagination: Pagination metadata (if pagination was used)
36
+ available_locales: Available locale codes (if i18n is enabled)
37
+ """
38
+
39
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
40
+
41
+ pagination: PaginationMeta | None = Field(None, description="Pagination metadata")
42
+ available_locales: list[str] | None = Field(
43
+ None, alias="availableLocales", description="Available locales"
44
+ )
@@ -0,0 +1,168 @@
1
+ """Normalized response models providing version-agnostic interface.
2
+
3
+ The normalization layer abstracts the differences between Strapi v4 and v5,
4
+ providing a consistent interface for working with entities regardless of version.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+ from strapi_kit.models.response.base import StrapiCollectionResponse, StrapiSingleResponse
15
+ from strapi_kit.models.response.v4 import V4Entity
16
+ from strapi_kit.models.response.v5 import V5Entity
17
+
18
+
19
+ class NormalizedEntity(BaseModel):
20
+ """Version-agnostic entity representation.
21
+
22
+ Provides a consistent interface abstracting v4/v5 differences:
23
+ - V4: nested attributes → flattened in NormalizedEntity
24
+ - V5: already flat → preserved in NormalizedEntity
25
+ - Document ID: v5 only, None for v4
26
+ - Timestamps: extracted to top level
27
+ - Custom fields: grouped in 'attributes' dict
28
+
29
+ Attributes:
30
+ id: Numeric entity ID (present in both v4 and v5)
31
+ document_id: String document ID (v5 only, None for v4)
32
+ created_at: Creation timestamp
33
+ updated_at: Last update timestamp
34
+ published_at: Publication timestamp
35
+ locale: Locale code
36
+ attributes: All custom fields (non-timestamp, non-system fields)
37
+
38
+ Examples:
39
+ >>> # From v4
40
+ >>> v4_entity = V4Entity(
41
+ ... id=1,
42
+ ... attributes=V4Attributes(
43
+ ... createdAt=datetime.now(),
44
+ ... title="Test",
45
+ ... content="Body"
46
+ ... )
47
+ ... )
48
+ >>> normalized = NormalizedEntity.from_v4(v4_entity)
49
+ >>> normalized.attributes["title"]
50
+ 'Test'
51
+ >>> normalized.document_id # None for v4
52
+ None
53
+
54
+ >>> # From v5
55
+ >>> v5_entity = V5Entity(
56
+ ... id=1,
57
+ ... documentId="abc123",
58
+ ... createdAt=datetime.now(),
59
+ ... title="Test",
60
+ ... content="Body"
61
+ ... )
62
+ >>> normalized = NormalizedEntity.from_v5(v5_entity)
63
+ >>> normalized.document_id
64
+ 'abc123'
65
+ >>> normalized.attributes["title"]
66
+ 'Test'
67
+ """
68
+
69
+ id: int = Field(..., description="Numeric entity ID")
70
+ document_id: str | None = Field(None, description="Document ID (v5 only)")
71
+ created_at: datetime | None = Field(None, description="Creation timestamp")
72
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
73
+ published_at: datetime | None = Field(None, description="Publication timestamp")
74
+ locale: str | None = Field(None, description="Locale code")
75
+ attributes: dict[str, Any] = Field(default_factory=dict, description="All custom entity fields")
76
+
77
+ @classmethod
78
+ def from_v4(cls, entity: V4Entity) -> NormalizedEntity:
79
+ """Create normalized entity from v4 entity.
80
+
81
+ Extracts attributes from the nested 'attributes' object and
82
+ promotes timestamps to top level.
83
+
84
+ Args:
85
+ entity: V4 entity to normalize
86
+
87
+ Returns:
88
+ Normalized entity with flattened structure
89
+
90
+ Examples:
91
+ >>> v4 = V4Entity(
92
+ ... id=1,
93
+ ... attributes=V4Attributes(title="Test", content="Body")
94
+ ... )
95
+ >>> normalized = NormalizedEntity.from_v4(v4)
96
+ >>> normalized.id
97
+ 1
98
+ """
99
+ # Get all attributes as dict
100
+ attrs_dict = entity.attributes.model_dump(by_alias=False, exclude_none=False)
101
+
102
+ # Extract system fields
103
+ created_at = attrs_dict.pop("created_at", None)
104
+ updated_at = attrs_dict.pop("updated_at", None)
105
+ published_at = attrs_dict.pop("published_at", None)
106
+ locale = attrs_dict.pop("locale", None)
107
+
108
+ # Remaining fields are custom attributes
109
+ return cls(
110
+ id=entity.id,
111
+ document_id=None, # v4 doesn't have document_id
112
+ created_at=created_at,
113
+ updated_at=updated_at,
114
+ published_at=published_at,
115
+ locale=locale,
116
+ attributes=attrs_dict,
117
+ )
118
+
119
+ @classmethod
120
+ def from_v5(cls, entity: V5Entity) -> NormalizedEntity:
121
+ """Create normalized entity from v5 entity.
122
+
123
+ Extracts timestamps and system fields, grouping remaining fields
124
+ as custom attributes.
125
+
126
+ Args:
127
+ entity: V5 entity to normalize
128
+
129
+ Returns:
130
+ Normalized entity
131
+
132
+ Examples:
133
+ >>> v5 = V5Entity(
134
+ ... id=1,
135
+ ... documentId="abc123",
136
+ ... title="Test",
137
+ ... content="Body"
138
+ ... )
139
+ >>> normalized = NormalizedEntity.from_v5(v5)
140
+ >>> normalized.document_id
141
+ 'abc123'
142
+ """
143
+ # Get all fields as dict
144
+ entity_dict = entity.model_dump(by_alias=False, exclude_none=False)
145
+
146
+ # Extract system fields
147
+ entity_id = entity_dict.pop("id")
148
+ document_id = entity_dict.pop("document_id")
149
+ created_at = entity_dict.pop("created_at", None)
150
+ updated_at = entity_dict.pop("updated_at", None)
151
+ published_at = entity_dict.pop("published_at", None)
152
+ locale = entity_dict.pop("locale", None)
153
+
154
+ # Remaining fields are custom attributes
155
+ return cls(
156
+ id=entity_id,
157
+ document_id=document_id,
158
+ created_at=created_at,
159
+ updated_at=updated_at,
160
+ published_at=published_at,
161
+ locale=locale,
162
+ attributes=entity_dict,
163
+ )
164
+
165
+
166
+ # Type aliases for normalized responses
167
+ NormalizedSingleResponse = StrapiSingleResponse[NormalizedEntity]
168
+ NormalizedCollectionResponse = StrapiCollectionResponse[NormalizedEntity]
@@ -0,0 +1,48 @@
1
+ """Relation data models for Strapi responses.
2
+
3
+ Models for handling relation fields which are wrapped in a 'data' key.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Generic, TypeVar
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+ # Generic type for relation data
13
+ T = TypeVar("T")
14
+
15
+
16
+ class RelationData(BaseModel, Generic[T]):
17
+ """Wrapper for relation fields.
18
+
19
+ In Strapi, populated relations are wrapped in a 'data' key:
20
+ - Single relation: {"data": {...}}
21
+ - Multiple relations: {"data": [{...}, {...}]}
22
+
23
+ Attributes:
24
+ data: The actual relation data (entity or list of entities)
25
+
26
+ Examples:
27
+ >>> # Single relation
28
+ >>> from strapi_kit.models.response.v5 import V5Entity
29
+ >>> relation = RelationData[V5Entity](
30
+ ... data=V5Entity(id=1, documentId="abc", title="Related")
31
+ ... )
32
+ >>> relation.data.title
33
+ 'Related'
34
+
35
+ >>> # Multiple relations
36
+ >>> relation = RelationData[list[V5Entity]](
37
+ ... data=[
38
+ ... V5Entity(id=1, documentId="abc", title="First"),
39
+ ... V5Entity(id=2, documentId="def", title="Second")
40
+ ... ]
41
+ ... )
42
+ >>> len(relation.data)
43
+ 2
44
+ """
45
+
46
+ model_config = ConfigDict(extra="allow")
47
+
48
+ data: T | None = Field(None, description="Relation data")
@@ -0,0 +1,70 @@
1
+ """Strapi v4 response models.
2
+
3
+ Models for parsing Strapi v4 API responses with nested attributes structure.
4
+
5
+ V4 Structure:
6
+ {
7
+ "data": {
8
+ "id": 1,
9
+ "attributes": {
10
+ "title": "Hello",
11
+ "createdAt": "2024-01-01T00:00:00.000Z",
12
+ "updatedAt": "2024-01-01T00:00:00.000Z",
13
+ "publishedAt": "2024-01-01T00:00:00.000Z",
14
+ "locale": "en",
15
+ ...custom fields
16
+ }
17
+ }
18
+ }
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from datetime import datetime
24
+
25
+ from pydantic import BaseModel, ConfigDict, Field
26
+
27
+ from strapi_kit.models.response.base import StrapiCollectionResponse, StrapiSingleResponse
28
+
29
+
30
+ class V4Attributes(BaseModel):
31
+ """Attributes container for v4 entities.
32
+
33
+ In v4, all entity data is nested under the 'attributes' key.
34
+ This includes timestamps, locale, and all custom fields.
35
+
36
+ Attributes:
37
+ created_at: Creation timestamp
38
+ updated_at: Last update timestamp
39
+ published_at: Publication timestamp (if draft & publish enabled)
40
+ locale: Locale code (if i18n enabled)
41
+ Additional fields are allowed and will be preserved.
42
+ """
43
+
44
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
45
+
46
+ created_at: datetime | None = Field(None, alias="createdAt")
47
+ updated_at: datetime | None = Field(None, alias="updatedAt")
48
+ published_at: datetime | None = Field(None, alias="publishedAt")
49
+ locale: str | None = Field(None, description="Locale code")
50
+
51
+
52
+ class V4Entity(BaseModel):
53
+ """Strapi v4 entity structure.
54
+
55
+ V4 entities have a numeric ID and nested attributes.
56
+
57
+ Attributes:
58
+ id: Numeric entity ID
59
+ attributes: All entity data (timestamps + custom fields)
60
+ """
61
+
62
+ model_config = ConfigDict(extra="allow")
63
+
64
+ id: int = Field(..., description="Numeric entity ID")
65
+ attributes: V4Attributes = Field(..., description="Entity attributes")
66
+
67
+
68
+ # Type aliases for v4 responses
69
+ V4SingleResponse = StrapiSingleResponse[V4Entity]
70
+ V4CollectionResponse = StrapiCollectionResponse[V4Entity]
@@ -0,0 +1,57 @@
1
+ """Strapi v5 response models.
2
+
3
+ Models for parsing Strapi v5 API responses with flattened structure.
4
+
5
+ V5 Structure:
6
+ {
7
+ "data": {
8
+ "id": 1,
9
+ "documentId": "abc123",
10
+ "title": "Hello",
11
+ "createdAt": "2024-01-01T00:00:00.000Z",
12
+ "updatedAt": "2024-01-01T00:00:00.000Z",
13
+ "publishedAt": "2024-01-01T00:00:00.000Z",
14
+ "locale": "en",
15
+ ...custom fields
16
+ }
17
+ }
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from datetime import datetime
23
+
24
+ from pydantic import BaseModel, ConfigDict, Field
25
+
26
+ from strapi_kit.models.response.base import StrapiCollectionResponse, StrapiSingleResponse
27
+
28
+
29
+ class V5Entity(BaseModel):
30
+ """Strapi v5 entity structure.
31
+
32
+ V5 entities have a flattened structure with both numeric ID and document ID.
33
+ All fields (timestamps, locale, custom) are at the same level.
34
+
35
+ Attributes:
36
+ id: Numeric entity ID (for backward compatibility)
37
+ document_id: String document ID (new in v5, unique identifier)
38
+ created_at: Creation timestamp
39
+ updated_at: Last update timestamp
40
+ published_at: Publication timestamp (if draft & publish enabled)
41
+ locale: Locale code (if i18n enabled)
42
+ Additional fields are allowed and will be preserved.
43
+ """
44
+
45
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
46
+
47
+ id: int = Field(..., description="Numeric entity ID")
48
+ document_id: str = Field(..., alias="documentId", description="Document ID (v5)")
49
+ created_at: datetime | None = Field(None, alias="createdAt")
50
+ updated_at: datetime | None = Field(None, alias="updatedAt")
51
+ published_at: datetime | None = Field(None, alias="publishedAt")
52
+ locale: str | None = Field(None, description="Locale code")
53
+
54
+
55
+ # Type aliases for v5 responses
56
+ V5SingleResponse = StrapiSingleResponse[V5Entity]
57
+ V5CollectionResponse = StrapiCollectionResponse[V5Entity]
@@ -0,0 +1,93 @@
1
+ """Content type schema models."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class FieldType(str, Enum):
9
+ """Field types in Strapi."""
10
+
11
+ STRING = "string"
12
+ TEXT = "text"
13
+ RICH_TEXT = "richtext"
14
+ EMAIL = "email"
15
+ PASSWORD = "password" # nosec B105 - This is a field type enum, not a hardcoded password
16
+ INTEGER = "integer"
17
+ BIG_INTEGER = "biginteger"
18
+ FLOAT = "float"
19
+ DECIMAL = "decimal"
20
+ DATE = "date"
21
+ TIME = "time"
22
+ DATETIME = "datetime"
23
+ TIMESTAMP = "timestamp"
24
+ BOOLEAN = "boolean"
25
+ ENUMERATION = "enumeration"
26
+ JSON = "json"
27
+ MEDIA = "media"
28
+ RELATION = "relation"
29
+ COMPONENT = "component"
30
+ DYNAMIC_ZONE = "dynamiczone"
31
+ UID = "uid"
32
+
33
+
34
+ class RelationType(str, Enum):
35
+ """Relation types in Strapi."""
36
+
37
+ ONE_TO_ONE = "oneToOne"
38
+ ONE_TO_MANY = "oneToMany"
39
+ MANY_TO_ONE = "manyToOne"
40
+ MANY_TO_MANY = "manyToMany"
41
+
42
+
43
+ class FieldSchema(BaseModel):
44
+ """Schema for a single field."""
45
+
46
+ type: FieldType
47
+ required: bool = False
48
+ unique: bool = False
49
+
50
+ # Relation-specific
51
+ relation: RelationType | None = None
52
+ target: str | None = None # Target content type UID
53
+ mapped_by: str | None = None
54
+ inversed_by: str | None = None
55
+
56
+
57
+ class ContentTypeSchema(BaseModel):
58
+ """Complete schema for a content type."""
59
+
60
+ uid: str
61
+ display_name: str
62
+ kind: str = "collectionType"
63
+ singular_name: str | None = Field(None, description="Singular name from Strapi schema")
64
+ plural_name: str | None = Field(
65
+ None, description="Plural name from Strapi schema (API endpoint)"
66
+ )
67
+ fields: dict[str, FieldSchema] = Field(default_factory=dict)
68
+
69
+ def get_field_target(self, field_name: str) -> str | None:
70
+ """Get target content type for a relation field.
71
+
72
+ Args:
73
+ field_name: Name of the field to check
74
+
75
+ Returns:
76
+ Target content type UID if field is a relation, None otherwise
77
+ """
78
+ field = self.fields.get(field_name)
79
+ if field and field.type == FieldType.RELATION:
80
+ return field.target
81
+ return None
82
+
83
+ def is_relation_field(self, field_name: str) -> bool:
84
+ """Check if a field is a relation.
85
+
86
+ Args:
87
+ field_name: Name of the field to check
88
+
89
+ Returns:
90
+ True if field is a relation, False otherwise
91
+ """
92
+ field = self.fields.get(field_name)
93
+ return field is not None and field.type == FieldType.RELATION
@@ -0,0 +1,16 @@
1
+ """Operations module for strapi-kit.
2
+
3
+ This module contains utility functions and helpers for various operations.
4
+ """
5
+
6
+ from strapi_kit.operations.media import (
7
+ build_media_download_url,
8
+ build_upload_payload,
9
+ normalize_media_response,
10
+ )
11
+
12
+ __all__ = [
13
+ "build_media_download_url",
14
+ "build_upload_payload",
15
+ "normalize_media_response",
16
+ ]