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.
- strapi_kit/__init__.py +97 -0
- strapi_kit/__version__.py +15 -0
- strapi_kit/_version.py +34 -0
- strapi_kit/auth/__init__.py +7 -0
- strapi_kit/auth/api_token.py +48 -0
- strapi_kit/cache/__init__.py +5 -0
- strapi_kit/cache/schema_cache.py +211 -0
- strapi_kit/client/__init__.py +11 -0
- strapi_kit/client/async_client.py +1032 -0
- strapi_kit/client/base.py +460 -0
- strapi_kit/client/sync_client.py +980 -0
- strapi_kit/config_provider.py +368 -0
- strapi_kit/exceptions/__init__.py +37 -0
- strapi_kit/exceptions/errors.py +205 -0
- strapi_kit/export/__init__.py +10 -0
- strapi_kit/export/exporter.py +384 -0
- strapi_kit/export/importer.py +619 -0
- strapi_kit/export/media_handler.py +322 -0
- strapi_kit/export/relation_resolver.py +172 -0
- strapi_kit/models/__init__.py +104 -0
- strapi_kit/models/bulk.py +69 -0
- strapi_kit/models/config.py +174 -0
- strapi_kit/models/enums.py +97 -0
- strapi_kit/models/export_format.py +166 -0
- strapi_kit/models/import_options.py +142 -0
- strapi_kit/models/request/__init__.py +1 -0
- strapi_kit/models/request/fields.py +65 -0
- strapi_kit/models/request/filters.py +611 -0
- strapi_kit/models/request/pagination.py +168 -0
- strapi_kit/models/request/populate.py +281 -0
- strapi_kit/models/request/query.py +429 -0
- strapi_kit/models/request/sort.py +147 -0
- strapi_kit/models/response/__init__.py +1 -0
- strapi_kit/models/response/base.py +75 -0
- strapi_kit/models/response/component.py +67 -0
- strapi_kit/models/response/media.py +91 -0
- strapi_kit/models/response/meta.py +44 -0
- strapi_kit/models/response/normalized.py +168 -0
- strapi_kit/models/response/relation.py +48 -0
- strapi_kit/models/response/v4.py +70 -0
- strapi_kit/models/response/v5.py +57 -0
- strapi_kit/models/schema.py +93 -0
- strapi_kit/operations/__init__.py +16 -0
- strapi_kit/operations/media.py +226 -0
- strapi_kit/operations/streaming.py +144 -0
- strapi_kit/parsers/__init__.py +5 -0
- strapi_kit/parsers/version_detecting.py +171 -0
- strapi_kit/protocols.py +455 -0
- strapi_kit/utils/__init__.py +15 -0
- strapi_kit/utils/rate_limiter.py +201 -0
- strapi_kit/utils/uid.py +88 -0
- strapi_kit-0.0.1.dist-info/METADATA +1098 -0
- strapi_kit-0.0.1.dist-info/RECORD +55 -0
- strapi_kit-0.0.1.dist-info/WHEEL +4 -0
- strapi_kit-0.0.1.dist-info/licenses/LICENSE +21 -0
strapi_kit/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""strapi-kit: A modern Python client for Strapi CMS.
|
|
2
|
+
|
|
3
|
+
This package provides a comprehensive interface for interacting with
|
|
4
|
+
Strapi v4 and v5 APIs, including:
|
|
5
|
+
- Synchronous and asynchronous clients
|
|
6
|
+
- Full CRUD operations
|
|
7
|
+
- Import/export functionality
|
|
8
|
+
- Type-safe data models with Pydantic
|
|
9
|
+
- Automatic retry and rate limiting
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .__version__ import __version__
|
|
13
|
+
from .client import AsyncClient, SyncClient
|
|
14
|
+
from .config_provider import (
|
|
15
|
+
ConfigFactory,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
create_config,
|
|
18
|
+
load_config,
|
|
19
|
+
)
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
AuthorizationError,
|
|
23
|
+
ConflictError,
|
|
24
|
+
FormatError,
|
|
25
|
+
ImportExportError,
|
|
26
|
+
MediaError,
|
|
27
|
+
NetworkError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
RelationError,
|
|
31
|
+
ServerError,
|
|
32
|
+
StrapiError,
|
|
33
|
+
ValidationError,
|
|
34
|
+
)
|
|
35
|
+
from .export import StrapiExporter, StrapiImporter
|
|
36
|
+
from .models import (
|
|
37
|
+
BulkOperationFailure,
|
|
38
|
+
BulkOperationResult,
|
|
39
|
+
RetryConfig,
|
|
40
|
+
StrapiConfig,
|
|
41
|
+
)
|
|
42
|
+
from .operations.streaming import stream_entities, stream_entities_async
|
|
43
|
+
from .parsers import VersionDetectingParser
|
|
44
|
+
from .protocols import (
|
|
45
|
+
AsyncHTTPClient,
|
|
46
|
+
AuthProvider,
|
|
47
|
+
ConfigProvider,
|
|
48
|
+
HTTPClient,
|
|
49
|
+
ResponseParser,
|
|
50
|
+
SchemaProvider,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"__version__",
|
|
55
|
+
# Clients
|
|
56
|
+
"SyncClient",
|
|
57
|
+
"AsyncClient",
|
|
58
|
+
# Configuration
|
|
59
|
+
"StrapiConfig",
|
|
60
|
+
"RetryConfig",
|
|
61
|
+
"ConfigFactory",
|
|
62
|
+
"load_config",
|
|
63
|
+
"create_config",
|
|
64
|
+
"ConfigurationError",
|
|
65
|
+
# Bulk Operations
|
|
66
|
+
"BulkOperationResult",
|
|
67
|
+
"BulkOperationFailure",
|
|
68
|
+
# Streaming
|
|
69
|
+
"stream_entities",
|
|
70
|
+
"stream_entities_async",
|
|
71
|
+
# Export/Import
|
|
72
|
+
"StrapiExporter",
|
|
73
|
+
"StrapiImporter",
|
|
74
|
+
# Protocols (for dependency injection)
|
|
75
|
+
"AuthProvider",
|
|
76
|
+
"ConfigProvider",
|
|
77
|
+
"HTTPClient",
|
|
78
|
+
"AsyncHTTPClient",
|
|
79
|
+
"ResponseParser",
|
|
80
|
+
"SchemaProvider",
|
|
81
|
+
# Parsers
|
|
82
|
+
"VersionDetectingParser",
|
|
83
|
+
# Exceptions
|
|
84
|
+
"StrapiError",
|
|
85
|
+
"AuthenticationError",
|
|
86
|
+
"AuthorizationError",
|
|
87
|
+
"NotFoundError",
|
|
88
|
+
"ValidationError",
|
|
89
|
+
"ConflictError",
|
|
90
|
+
"NetworkError",
|
|
91
|
+
"RateLimitError",
|
|
92
|
+
"ServerError",
|
|
93
|
+
"ImportExportError",
|
|
94
|
+
"FormatError",
|
|
95
|
+
"RelationError",
|
|
96
|
+
"MediaError",
|
|
97
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Version information for strapi-kit.
|
|
2
|
+
|
|
3
|
+
This file is automatically updated by hatch-vcs during build.
|
|
4
|
+
For development installs, it falls back to a placeholder version.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
# Try to import version from hatch-vcs generated file
|
|
9
|
+
from strapi_kit._version import __version__
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Development mode - not built with hatch-vcs yet
|
|
12
|
+
# This happens when installed with pip install -e .
|
|
13
|
+
__version__ = "0.0.0.dev0+local"
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|
strapi_kit/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.0.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""API Token authentication for Strapi.
|
|
2
|
+
|
|
3
|
+
This module provides bearer token authentication for Strapi API requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class APITokenAuth:
|
|
8
|
+
"""API Token authentication handler.
|
|
9
|
+
|
|
10
|
+
Manages bearer token authentication for Strapi API requests.
|
|
11
|
+
Tokens can be created in Strapi admin panel under Settings > API Tokens.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
```python
|
|
15
|
+
auth = APITokenAuth("your-api-token")
|
|
16
|
+
headers = auth.get_headers()
|
|
17
|
+
# {"Authorization": "Bearer your-api-token"}
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, token: str) -> None:
|
|
22
|
+
"""Initialize API token authentication.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
token: The API token from Strapi
|
|
26
|
+
"""
|
|
27
|
+
self.token = token
|
|
28
|
+
|
|
29
|
+
def get_headers(self) -> dict[str, str]:
|
|
30
|
+
"""Get authentication headers for requests.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dictionary with Authorization header
|
|
34
|
+
"""
|
|
35
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
36
|
+
|
|
37
|
+
def validate_token(self) -> bool:
|
|
38
|
+
"""Validate that the token is not empty.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if token is valid (non-empty), False otherwise
|
|
42
|
+
"""
|
|
43
|
+
return bool(self.token and self.token.strip())
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
"""Return string representation (token masked for security)."""
|
|
47
|
+
masked = f"{self.token[:4]}...{self.token[-4:]}" if len(self.token) > 8 else "****"
|
|
48
|
+
return f"APITokenAuth(token={masked})"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""In-memory schema cache implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from ..exceptions import StrapiError
|
|
7
|
+
from ..models.schema import ContentTypeSchema, FieldSchema, FieldType, RelationType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..client.sync_client import SyncClient
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemorySchemaCache:
|
|
16
|
+
"""In-memory cache for content type schemas.
|
|
17
|
+
|
|
18
|
+
Temporary cache for export/import operations only.
|
|
19
|
+
NOT persistent across process restarts.
|
|
20
|
+
|
|
21
|
+
This cache stores content type schemas to enable proper relation
|
|
22
|
+
resolution during import. Schemas are fetched lazily on first access
|
|
23
|
+
and cached for subsequent lookups.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> cache = InMemorySchemaCache(client)
|
|
27
|
+
>>> schema = cache.get_schema("api::article.article")
|
|
28
|
+
>>> target = schema.get_field_target("author") # Returns "api::author.author"
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, client: "SyncClient") -> None:
|
|
32
|
+
"""Initialize the schema cache.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
client: Strapi client for fetching schemas
|
|
36
|
+
"""
|
|
37
|
+
self.client = client
|
|
38
|
+
self._cache: dict[str, ContentTypeSchema] = {}
|
|
39
|
+
self._fetch_count = 0
|
|
40
|
+
|
|
41
|
+
def get_schema(self, content_type: str) -> ContentTypeSchema:
|
|
42
|
+
"""Get schema (cached or fetch from API).
|
|
43
|
+
|
|
44
|
+
Lazy loading: only fetches on first access.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
content_type: Content type UID (e.g., "api::article.article")
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Content type schema
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
StrapiError: If schema fetch fails
|
|
54
|
+
"""
|
|
55
|
+
# Check cache first (O(1) lookup)
|
|
56
|
+
if content_type in self._cache:
|
|
57
|
+
logger.debug(f"Schema cache hit: {content_type}")
|
|
58
|
+
return self._cache[content_type]
|
|
59
|
+
|
|
60
|
+
# Cache miss - fetch from API
|
|
61
|
+
logger.debug(f"Schema cache miss: {content_type}")
|
|
62
|
+
schema = self._fetch_schema(content_type)
|
|
63
|
+
self._cache[content_type] = schema
|
|
64
|
+
self._fetch_count += 1
|
|
65
|
+
return schema
|
|
66
|
+
|
|
67
|
+
def cache_schema(self, content_type: str, schema: ContentTypeSchema) -> None:
|
|
68
|
+
"""Manually cache a schema.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
content_type: Content type UID
|
|
72
|
+
schema: Schema to cache
|
|
73
|
+
"""
|
|
74
|
+
self._cache[content_type] = schema
|
|
75
|
+
logger.debug(f"Manually cached schema: {content_type}")
|
|
76
|
+
|
|
77
|
+
def has_schema(self, content_type: str) -> bool:
|
|
78
|
+
"""Check if schema is cached.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
content_type: Content type UID
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if schema is cached, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
return content_type in self._cache
|
|
87
|
+
|
|
88
|
+
def clear_cache(self) -> None:
|
|
89
|
+
"""Clear all cached schemas."""
|
|
90
|
+
self._cache.clear()
|
|
91
|
+
self._fetch_count = 0
|
|
92
|
+
logger.debug("Schema cache cleared")
|
|
93
|
+
|
|
94
|
+
def _fetch_schema(self, content_type: str) -> ContentTypeSchema:
|
|
95
|
+
"""Fetch schema from Strapi API.
|
|
96
|
+
|
|
97
|
+
Endpoint: GET /api/content-type-builder/content-types/{uid}
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
content_type: Content type UID
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Parsed content type schema
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
StrapiError: If fetch fails
|
|
107
|
+
"""
|
|
108
|
+
endpoint = f"content-type-builder/content-types/{content_type}"
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
response = self.client.get(endpoint)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise StrapiError(
|
|
114
|
+
f"Failed to fetch schema for {content_type}",
|
|
115
|
+
details={"content_type": content_type, "error": str(e)},
|
|
116
|
+
) from e
|
|
117
|
+
|
|
118
|
+
schema_data = response.get("data")
|
|
119
|
+
if not schema_data:
|
|
120
|
+
raise StrapiError(
|
|
121
|
+
f"Invalid schema response for {content_type}",
|
|
122
|
+
details={"response": response},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return self._parse_schema_response(content_type, schema_data)
|
|
126
|
+
|
|
127
|
+
def _parse_schema_response(self, uid: str, schema_data: dict[str, Any]) -> ContentTypeSchema:
|
|
128
|
+
"""Parse Strapi schema response.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
uid: Content type UID
|
|
132
|
+
schema_data: Schema data from API response
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Parsed content type schema
|
|
136
|
+
"""
|
|
137
|
+
info = schema_data.get("info", {})
|
|
138
|
+
attributes = schema_data.get("attributes", {})
|
|
139
|
+
|
|
140
|
+
fields: dict[str, FieldSchema] = {}
|
|
141
|
+
for field_name, field_data in attributes.items():
|
|
142
|
+
try:
|
|
143
|
+
fields[field_name] = self._parse_field_schema(field_data)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.warning(f"Failed to parse field {field_name}: {e}")
|
|
146
|
+
|
|
147
|
+
return ContentTypeSchema(
|
|
148
|
+
uid=uid,
|
|
149
|
+
display_name=info.get("displayName", uid),
|
|
150
|
+
kind=schema_data.get("kind", "collectionType"),
|
|
151
|
+
singular_name=info.get("singularName"),
|
|
152
|
+
plural_name=info.get("pluralName"),
|
|
153
|
+
fields=fields,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _parse_field_schema(self, field_data: dict[str, Any]) -> FieldSchema:
|
|
157
|
+
"""Parse a single field schema.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
field_data: Field data from API response
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Parsed field schema
|
|
164
|
+
"""
|
|
165
|
+
field_type_str = field_data.get("type", "string")
|
|
166
|
+
|
|
167
|
+
# Try to match to FieldType enum, fallback to STRING
|
|
168
|
+
try:
|
|
169
|
+
field_type = FieldType(field_type_str)
|
|
170
|
+
except ValueError:
|
|
171
|
+
logger.warning(f"Unknown field type: {field_type_str}, using STRING")
|
|
172
|
+
field_type = FieldType.STRING
|
|
173
|
+
|
|
174
|
+
schema = FieldSchema(
|
|
175
|
+
type=field_type,
|
|
176
|
+
required=field_data.get("required", False),
|
|
177
|
+
unique=field_data.get("unique", False),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Relation-specific
|
|
181
|
+
if field_type == FieldType.RELATION:
|
|
182
|
+
relation_str = field_data.get("relation")
|
|
183
|
+
if relation_str:
|
|
184
|
+
try:
|
|
185
|
+
schema.relation = RelationType(relation_str)
|
|
186
|
+
except ValueError:
|
|
187
|
+
logger.warning(f"Unknown relation type: {relation_str}")
|
|
188
|
+
|
|
189
|
+
schema.target = field_data.get("target")
|
|
190
|
+
schema.mapped_by = field_data.get("mappedBy")
|
|
191
|
+
schema.inversed_by = field_data.get("inversedBy")
|
|
192
|
+
|
|
193
|
+
return schema
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def cache_size(self) -> int:
|
|
197
|
+
"""Get number of cached schemas.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Number of schemas in cache
|
|
201
|
+
"""
|
|
202
|
+
return len(self._cache)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def fetch_count(self) -> int:
|
|
206
|
+
"""Get number of schemas fetched from API.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Number of API fetches performed
|
|
210
|
+
"""
|
|
211
|
+
return self._fetch_count
|