strapi-kit 0.0.2__py3-none-any.whl → 0.0.3__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 +1 -1
- strapi_kit/_version.py +2 -2
- strapi_kit/client/async_client.py +87 -1
- strapi_kit/client/base.py +91 -2
- strapi_kit/client/sync_client.py +87 -1
- strapi_kit/config_provider.py +1 -16
- strapi_kit/exceptions/__init__.py +2 -0
- strapi_kit/exceptions/errors.py +13 -0
- strapi_kit/export/exporter.py +6 -5
- strapi_kit/models/__init__.py +8 -0
- strapi_kit/models/content_type.py +148 -0
- strapi_kit/models/export_format.py +5 -3
- strapi_kit/models/request/query.py +9 -6
- strapi_kit/operations/media.py +13 -6
- strapi_kit/operations/streaming.py +5 -4
- strapi_kit/utils/__init__.py +20 -1
- strapi_kit/utils/rate_limiter.py +4 -2
- strapi_kit/utils/seo.py +294 -0
- strapi_kit/utils/uid.py +118 -0
- {strapi_kit-0.0.2.dist-info → strapi_kit-0.0.3.dist-info}/METADATA +203 -10
- {strapi_kit-0.0.2.dist-info → strapi_kit-0.0.3.dist-info}/RECORD +23 -21
- {strapi_kit-0.0.2.dist-info → strapi_kit-0.0.3.dist-info}/WHEEL +0 -0
- {strapi_kit-0.0.2.dist-info → strapi_kit-0.0.3.dist-info}/licenses/LICENSE +0 -0
strapi_kit/__init__.py
CHANGED
|
@@ -13,13 +13,13 @@ from .__version__ import __version__
|
|
|
13
13
|
from .client import AsyncClient, SyncClient
|
|
14
14
|
from .config_provider import (
|
|
15
15
|
ConfigFactory,
|
|
16
|
-
ConfigurationError,
|
|
17
16
|
create_config,
|
|
18
17
|
load_config,
|
|
19
18
|
)
|
|
20
19
|
from .exceptions import (
|
|
21
20
|
AuthenticationError,
|
|
22
21
|
AuthorizationError,
|
|
22
|
+
ConfigurationError,
|
|
23
23
|
ConflictError,
|
|
24
24
|
FormatError,
|
|
25
25
|
ImportExportError,
|
strapi_kit/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 3)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -8,7 +8,11 @@ import asyncio
|
|
|
8
8
|
import logging
|
|
9
9
|
from collections.abc import Callable
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..models.content_type import ComponentListItem, ContentTypeListItem
|
|
15
|
+
from ..models.content_type import ContentTypeSchema as CTBContentTypeSchema
|
|
12
16
|
|
|
13
17
|
import httpx
|
|
14
18
|
|
|
@@ -1030,3 +1034,85 @@ class AsyncClient(BaseClient):
|
|
|
1030
1034
|
succeeded=success_count,
|
|
1031
1035
|
failed=len(failures),
|
|
1032
1036
|
)
|
|
1037
|
+
|
|
1038
|
+
# Content-Type Builder API
|
|
1039
|
+
|
|
1040
|
+
async def get_content_types(
|
|
1041
|
+
self,
|
|
1042
|
+
*,
|
|
1043
|
+
include_plugins: bool = False,
|
|
1044
|
+
) -> list["ContentTypeListItem"]:
|
|
1045
|
+
"""List all content types from Content-Type Builder API.
|
|
1046
|
+
|
|
1047
|
+
Retrieves schema information for all content types defined in Strapi.
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
include_plugins: Whether to include plugin content types
|
|
1051
|
+
(e.g., users-permissions). Defaults to False.
|
|
1052
|
+
|
|
1053
|
+
Returns:
|
|
1054
|
+
List of ContentTypeListItem with uid, kind, info, and attributes
|
|
1055
|
+
|
|
1056
|
+
Examples:
|
|
1057
|
+
>>> # Get only API content types
|
|
1058
|
+
>>> content_types = await client.get_content_types()
|
|
1059
|
+
>>> for ct in content_types:
|
|
1060
|
+
... print(f"{ct.uid}: {ct.info.display_name}")
|
|
1061
|
+
api::article.article: Article
|
|
1062
|
+
api::category.category: Category
|
|
1063
|
+
|
|
1064
|
+
>>> # Include plugin content types
|
|
1065
|
+
>>> all_types = await client.get_content_types(include_plugins=True)
|
|
1066
|
+
>>> plugin_types = [ct for ct in all_types if ct.uid.startswith("plugin::")]
|
|
1067
|
+
"""
|
|
1068
|
+
|
|
1069
|
+
raw_response = await self.get("content-type-builder/content-types")
|
|
1070
|
+
return self._parse_content_types_response(raw_response, include_plugins)
|
|
1071
|
+
|
|
1072
|
+
async def get_components(self) -> list["ComponentListItem"]:
|
|
1073
|
+
"""List all components from Content-Type Builder API.
|
|
1074
|
+
|
|
1075
|
+
Retrieves schema information for all components defined in Strapi.
|
|
1076
|
+
|
|
1077
|
+
Returns:
|
|
1078
|
+
List of ComponentListItem with uid, category, info, and attributes
|
|
1079
|
+
|
|
1080
|
+
Examples:
|
|
1081
|
+
>>> components = await client.get_components()
|
|
1082
|
+
>>> for comp in components:
|
|
1083
|
+
... print(f"{comp.category}/{comp.uid}: {comp.info.display_name}")
|
|
1084
|
+
shared/shared.seo: SEO
|
|
1085
|
+
blocks/blocks.hero: Hero Section
|
|
1086
|
+
"""
|
|
1087
|
+
|
|
1088
|
+
raw_response = await self.get("content-type-builder/components")
|
|
1089
|
+
return self._parse_components_response(raw_response)
|
|
1090
|
+
|
|
1091
|
+
async def get_content_type_schema(self, uid: str) -> "CTBContentTypeSchema":
|
|
1092
|
+
"""Get full schema for a specific content type.
|
|
1093
|
+
|
|
1094
|
+
Retrieves detailed schema information including all field configurations.
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
uid: Content type UID (e.g., "api::article.article")
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
CTBContentTypeSchema with complete field definitions
|
|
1101
|
+
|
|
1102
|
+
Raises:
|
|
1103
|
+
NotFoundError: If content type doesn't exist
|
|
1104
|
+
|
|
1105
|
+
Examples:
|
|
1106
|
+
>>> schema = await client.get_content_type_schema("api::article.article")
|
|
1107
|
+
>>> schema.info.display_name
|
|
1108
|
+
'Article'
|
|
1109
|
+
>>> schema.attributes["title"]["type"]
|
|
1110
|
+
'string'
|
|
1111
|
+
>>> schema.is_relation_field("author")
|
|
1112
|
+
True
|
|
1113
|
+
>>> schema.get_relation_target("author")
|
|
1114
|
+
'api::author.author'
|
|
1115
|
+
"""
|
|
1116
|
+
|
|
1117
|
+
raw_response = await self.get(f"content-type-builder/content-types/{uid}")
|
|
1118
|
+
return self._parse_content_type_schema_response(raw_response)
|
strapi_kit/client/base.py
CHANGED
|
@@ -5,7 +5,11 @@ automatic response format detection, error handling, and authentication.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any, Literal
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..models.content_type import ComponentListItem, ContentTypeListItem
|
|
12
|
+
from ..models.content_type import ContentTypeSchema as CTBContentTypeSchema
|
|
9
13
|
|
|
10
14
|
import httpx
|
|
11
15
|
from tenacity import (
|
|
@@ -20,6 +24,7 @@ from ..auth.api_token import APITokenAuth
|
|
|
20
24
|
from ..exceptions import (
|
|
21
25
|
AuthenticationError,
|
|
22
26
|
AuthorizationError,
|
|
27
|
+
ConfigurationError,
|
|
23
28
|
ConflictError,
|
|
24
29
|
NotFoundError,
|
|
25
30
|
RateLimitError,
|
|
@@ -83,7 +88,7 @@ class BaseClient:
|
|
|
83
88
|
|
|
84
89
|
# Validate authentication
|
|
85
90
|
if not self.auth.validate_token():
|
|
86
|
-
raise
|
|
91
|
+
raise ConfigurationError("API token is required and cannot be empty")
|
|
87
92
|
|
|
88
93
|
# API version detection (for backward compatibility)
|
|
89
94
|
self._api_version: Literal["v4", "v5"] | None = (
|
|
@@ -458,3 +463,87 @@ class BaseClient:
|
|
|
458
463
|
|
|
459
464
|
# Media list follows standard collection format
|
|
460
465
|
return self._parse_collection_response(response_data)
|
|
466
|
+
|
|
467
|
+
def _parse_content_types_response(
|
|
468
|
+
self,
|
|
469
|
+
response_data: dict[str, Any],
|
|
470
|
+
include_plugins: bool = False,
|
|
471
|
+
) -> list["ContentTypeListItem"]:
|
|
472
|
+
"""Parse content-type-builder content types response.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
response_data: Raw JSON response from content-type-builder
|
|
476
|
+
include_plugins: Whether to include plugin content types
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of ContentTypeListItem instances
|
|
480
|
+
"""
|
|
481
|
+
from ..models.content_type import ContentTypeListItem
|
|
482
|
+
|
|
483
|
+
data = response_data.get("data", [])
|
|
484
|
+
result = []
|
|
485
|
+
|
|
486
|
+
for item in data:
|
|
487
|
+
uid = item.get("uid", "")
|
|
488
|
+
# Filter out plugin content types if not requested
|
|
489
|
+
if not include_plugins and uid.startswith("plugin::"):
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
content_type = ContentTypeListItem.model_validate(item)
|
|
494
|
+
result.append(content_type)
|
|
495
|
+
except Exception:
|
|
496
|
+
# Skip malformed items
|
|
497
|
+
logger.warning(f"Failed to parse content type: {uid}")
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
return result
|
|
501
|
+
|
|
502
|
+
def _parse_components_response(
|
|
503
|
+
self,
|
|
504
|
+
response_data: dict[str, Any],
|
|
505
|
+
) -> list["ComponentListItem"]:
|
|
506
|
+
"""Parse content-type-builder components response.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
response_data: Raw JSON response from content-type-builder
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
List of ComponentListItem instances
|
|
513
|
+
"""
|
|
514
|
+
from ..models.content_type import ComponentListItem
|
|
515
|
+
|
|
516
|
+
data = response_data.get("data", [])
|
|
517
|
+
result = []
|
|
518
|
+
|
|
519
|
+
for item in data:
|
|
520
|
+
uid = item.get("uid", "")
|
|
521
|
+
try:
|
|
522
|
+
component = ComponentListItem.model_validate(item)
|
|
523
|
+
result.append(component)
|
|
524
|
+
except Exception:
|
|
525
|
+
# Skip malformed items
|
|
526
|
+
logger.warning(f"Failed to parse component: {uid}")
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
return result
|
|
530
|
+
|
|
531
|
+
def _parse_content_type_schema_response(
|
|
532
|
+
self,
|
|
533
|
+
response_data: dict[str, Any],
|
|
534
|
+
) -> "CTBContentTypeSchema":
|
|
535
|
+
"""Parse content-type-builder single content type schema response.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
response_data: Raw JSON response from content-type-builder
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
CTBContentTypeSchema instance
|
|
542
|
+
|
|
543
|
+
Raises:
|
|
544
|
+
ValidationError: If response cannot be parsed
|
|
545
|
+
"""
|
|
546
|
+
from ..models.content_type import ContentTypeSchema as CTBContentTypeSchema
|
|
547
|
+
|
|
548
|
+
data = response_data.get("data", response_data)
|
|
549
|
+
return CTBContentTypeSchema.model_validate(data)
|
strapi_kit/client/sync_client.py
CHANGED
|
@@ -7,7 +7,11 @@ and applications that don't require concurrency.
|
|
|
7
7
|
import logging
|
|
8
8
|
from collections.abc import Callable
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..models.content_type import ComponentListItem, ContentTypeListItem
|
|
14
|
+
from ..models.content_type import ContentTypeSchema as CTBContentTypeSchema
|
|
11
15
|
|
|
12
16
|
import httpx
|
|
13
17
|
|
|
@@ -978,3 +982,85 @@ class SyncClient(BaseClient):
|
|
|
978
982
|
succeeded=success_count,
|
|
979
983
|
failed=len(failures),
|
|
980
984
|
)
|
|
985
|
+
|
|
986
|
+
# Content-Type Builder API
|
|
987
|
+
|
|
988
|
+
def get_content_types(
|
|
989
|
+
self,
|
|
990
|
+
*,
|
|
991
|
+
include_plugins: bool = False,
|
|
992
|
+
) -> list["ContentTypeListItem"]:
|
|
993
|
+
"""List all content types from Content-Type Builder API.
|
|
994
|
+
|
|
995
|
+
Retrieves schema information for all content types defined in Strapi.
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
include_plugins: Whether to include plugin content types
|
|
999
|
+
(e.g., users-permissions). Defaults to False.
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
List of ContentTypeListItem with uid, kind, info, and attributes
|
|
1003
|
+
|
|
1004
|
+
Examples:
|
|
1005
|
+
>>> # Get only API content types
|
|
1006
|
+
>>> content_types = client.get_content_types()
|
|
1007
|
+
>>> for ct in content_types:
|
|
1008
|
+
... print(f"{ct.uid}: {ct.info.display_name}")
|
|
1009
|
+
api::article.article: Article
|
|
1010
|
+
api::category.category: Category
|
|
1011
|
+
|
|
1012
|
+
>>> # Include plugin content types
|
|
1013
|
+
>>> all_types = client.get_content_types(include_plugins=True)
|
|
1014
|
+
>>> plugin_types = [ct for ct in all_types if ct.uid.startswith("plugin::")]
|
|
1015
|
+
"""
|
|
1016
|
+
|
|
1017
|
+
raw_response = self.get("content-type-builder/content-types")
|
|
1018
|
+
return self._parse_content_types_response(raw_response, include_plugins)
|
|
1019
|
+
|
|
1020
|
+
def get_components(self) -> list["ComponentListItem"]:
|
|
1021
|
+
"""List all components from Content-Type Builder API.
|
|
1022
|
+
|
|
1023
|
+
Retrieves schema information for all components defined in Strapi.
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
List of ComponentListItem with uid, category, info, and attributes
|
|
1027
|
+
|
|
1028
|
+
Examples:
|
|
1029
|
+
>>> components = client.get_components()
|
|
1030
|
+
>>> for comp in components:
|
|
1031
|
+
... print(f"{comp.category}/{comp.uid}: {comp.info.display_name}")
|
|
1032
|
+
shared/shared.seo: SEO
|
|
1033
|
+
blocks/blocks.hero: Hero Section
|
|
1034
|
+
"""
|
|
1035
|
+
|
|
1036
|
+
raw_response = self.get("content-type-builder/components")
|
|
1037
|
+
return self._parse_components_response(raw_response)
|
|
1038
|
+
|
|
1039
|
+
def get_content_type_schema(self, uid: str) -> "CTBContentTypeSchema":
|
|
1040
|
+
"""Get full schema for a specific content type.
|
|
1041
|
+
|
|
1042
|
+
Retrieves detailed schema information including all field configurations.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
uid: Content type UID (e.g., "api::article.article")
|
|
1046
|
+
|
|
1047
|
+
Returns:
|
|
1048
|
+
CTBContentTypeSchema with complete field definitions
|
|
1049
|
+
|
|
1050
|
+
Raises:
|
|
1051
|
+
NotFoundError: If content type doesn't exist
|
|
1052
|
+
|
|
1053
|
+
Examples:
|
|
1054
|
+
>>> schema = client.get_content_type_schema("api::article.article")
|
|
1055
|
+
>>> schema.info.display_name
|
|
1056
|
+
'Article'
|
|
1057
|
+
>>> schema.attributes["title"]["type"]
|
|
1058
|
+
'string'
|
|
1059
|
+
>>> schema.is_relation_field("author")
|
|
1060
|
+
True
|
|
1061
|
+
>>> schema.get_relation_target("author")
|
|
1062
|
+
'api::author.author'
|
|
1063
|
+
"""
|
|
1064
|
+
|
|
1065
|
+
raw_response = self.get(f"content-type-builder/content-types/{uid}")
|
|
1066
|
+
return self._parse_content_type_schema_response(raw_response)
|
strapi_kit/config_provider.py
CHANGED
|
@@ -9,25 +9,10 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from pydantic import SecretStr, ValidationError
|
|
11
11
|
|
|
12
|
-
from .exceptions import
|
|
12
|
+
from .exceptions import ConfigurationError
|
|
13
13
|
from .models.config import RetryConfig, StrapiConfig
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class ConfigurationError(StrapiError):
|
|
17
|
-
"""Raised when configuration cannot be loaded or is invalid.
|
|
18
|
-
|
|
19
|
-
Inherits from StrapiError for consistent exception handling.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(self, message: str) -> None:
|
|
23
|
-
"""Initialize ConfigurationError.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
message: Human-readable error message
|
|
27
|
-
"""
|
|
28
|
-
super().__init__(message, details=None)
|
|
29
|
-
|
|
30
|
-
|
|
31
16
|
class ConfigFactory:
|
|
32
17
|
"""Factory for creating StrapiConfig instances from various sources.
|
|
33
18
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .errors import (
|
|
4
4
|
AuthenticationError,
|
|
5
5
|
AuthorizationError,
|
|
6
|
+
ConfigurationError,
|
|
6
7
|
ConflictError,
|
|
7
8
|
ConnectionError,
|
|
8
9
|
FormatError,
|
|
@@ -20,6 +21,7 @@ from .errors import (
|
|
|
20
21
|
|
|
21
22
|
__all__ = [
|
|
22
23
|
"StrapiError",
|
|
24
|
+
"ConfigurationError",
|
|
23
25
|
"AuthenticationError",
|
|
24
26
|
"AuthorizationError",
|
|
25
27
|
"NotFoundError",
|
strapi_kit/exceptions/errors.py
CHANGED
|
@@ -32,6 +32,19 @@ class StrapiError(Exception):
|
|
|
32
32
|
return self.message
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class ConfigurationError(StrapiError):
|
|
36
|
+
"""Raised when configuration is invalid or cannot be loaded.
|
|
37
|
+
|
|
38
|
+
This includes:
|
|
39
|
+
- Missing required configuration values
|
|
40
|
+
- Invalid configuration values (wrong types, out of range)
|
|
41
|
+
- Invalid URLs or authentication tokens
|
|
42
|
+
- Failed .env file loading
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
35
48
|
# HTTP Status Code Related Errors
|
|
36
49
|
|
|
37
50
|
|
strapi_kit/export/exporter.py
CHANGED
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
13
|
from strapi_kit.cache.schema_cache import InMemorySchemaCache
|
|
14
|
-
from strapi_kit.exceptions import ImportExportError
|
|
14
|
+
from strapi_kit.exceptions import ImportExportError, ValidationError
|
|
15
15
|
from strapi_kit.export.media_handler import MediaHandler
|
|
16
16
|
from strapi_kit.export.relation_resolver import RelationResolver
|
|
17
17
|
from strapi_kit.models.export_format import (
|
|
@@ -78,7 +78,7 @@ class StrapiExporter:
|
|
|
78
78
|
ExportData containing all exported content
|
|
79
79
|
|
|
80
80
|
Raises:
|
|
81
|
-
|
|
81
|
+
ValidationError: If include_media=True but media_dir is not provided
|
|
82
82
|
ImportExportError: If export fails
|
|
83
83
|
|
|
84
84
|
Example:
|
|
@@ -89,7 +89,7 @@ class StrapiExporter:
|
|
|
89
89
|
>>> print(f"Exported {export_data.get_entity_count()} entities")
|
|
90
90
|
"""
|
|
91
91
|
if include_media and media_dir is None:
|
|
92
|
-
raise
|
|
92
|
+
raise ValidationError("media_dir must be provided when include_media=True")
|
|
93
93
|
|
|
94
94
|
try:
|
|
95
95
|
# Create metadata
|
|
@@ -161,8 +161,9 @@ class StrapiExporter:
|
|
|
161
161
|
"Exporting media files",
|
|
162
162
|
)
|
|
163
163
|
|
|
164
|
-
#
|
|
165
|
-
|
|
164
|
+
# Type guard: media_dir validated at method start (line 91-92)
|
|
165
|
+
if media_dir is None:
|
|
166
|
+
raise ValidationError("media_dir must be provided when include_media=True")
|
|
166
167
|
self._export_media(
|
|
167
168
|
export_data, media_dir, progress_callback, media_ids=all_media_ids
|
|
168
169
|
)
|
strapi_kit/models/__init__.py
CHANGED
|
@@ -5,6 +5,9 @@ Includes configuration models and request/response models for Strapi API interac
|
|
|
5
5
|
|
|
6
6
|
from .bulk import BulkOperationFailure, BulkOperationResult
|
|
7
7
|
from .config import RetryConfig, StrapiConfig
|
|
8
|
+
from .content_type import ComponentListItem, ContentTypeListItem
|
|
9
|
+
from .content_type import ContentTypeInfo as CTBContentTypeInfo
|
|
10
|
+
from .content_type import ContentTypeSchema as CTBContentTypeSchema
|
|
8
11
|
from .enums import FilterOperator, PublicationState, SortDirection
|
|
9
12
|
from .export_format import ExportData, ExportedEntity, ExportedMediaFile, ExportMetadata
|
|
10
13
|
from .import_options import ConflictResolution, ImportOptions, ImportResult
|
|
@@ -101,4 +104,9 @@ __all__ = [
|
|
|
101
104
|
"FieldSchema",
|
|
102
105
|
"FieldType",
|
|
103
106
|
"RelationType",
|
|
107
|
+
# Content-Type Builder models
|
|
108
|
+
"CTBContentTypeInfo",
|
|
109
|
+
"CTBContentTypeSchema",
|
|
110
|
+
"ContentTypeListItem",
|
|
111
|
+
"ComponentListItem",
|
|
104
112
|
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Content-Type Builder API response models.
|
|
2
|
+
|
|
3
|
+
This module provides Pydantic models for parsing responses from
|
|
4
|
+
Strapi's Content-Type Builder API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContentTypeInfo(BaseModel):
|
|
13
|
+
"""Content type info metadata.
|
|
14
|
+
|
|
15
|
+
Contains display and naming information for a content type.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
display_name: str = Field(alias="displayName")
|
|
19
|
+
singular_name: str | None = Field(None, alias="singularName")
|
|
20
|
+
plural_name: str | None = Field(None, alias="pluralName")
|
|
21
|
+
description: str | None = None
|
|
22
|
+
|
|
23
|
+
model_config = {"populate_by_name": True}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContentTypeListItem(BaseModel):
|
|
27
|
+
"""Content type list item from Content-Type Builder API.
|
|
28
|
+
|
|
29
|
+
Represents a single content type in the list response.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
uid: str
|
|
33
|
+
kind: str = "collectionType"
|
|
34
|
+
info: ContentTypeInfo
|
|
35
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
36
|
+
plugin_options: dict[str, Any] | None = Field(None, alias="pluginOptions")
|
|
37
|
+
|
|
38
|
+
model_config = {"populate_by_name": True}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ComponentListItem(BaseModel):
|
|
42
|
+
"""Component list item from Content-Type Builder API.
|
|
43
|
+
|
|
44
|
+
Represents a single component in the list response.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
uid: str
|
|
48
|
+
category: str
|
|
49
|
+
info: ContentTypeInfo
|
|
50
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
model_config = {"populate_by_name": True}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ContentTypeSchema(BaseModel):
|
|
56
|
+
"""Full content type schema from Content-Type Builder API.
|
|
57
|
+
|
|
58
|
+
Contains complete schema information including all attributes
|
|
59
|
+
and their configurations.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
uid: str
|
|
63
|
+
kind: str = "collectionType"
|
|
64
|
+
info: ContentTypeInfo
|
|
65
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
66
|
+
plugin_options: dict[str, Any] | None = Field(None, alias="pluginOptions")
|
|
67
|
+
options: dict[str, Any] | None = None
|
|
68
|
+
|
|
69
|
+
model_config = {"populate_by_name": True}
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def display_name(self) -> str:
|
|
73
|
+
"""Get the display name from info."""
|
|
74
|
+
return self.info.display_name
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def singular_name(self) -> str | None:
|
|
78
|
+
"""Get the singular name from info."""
|
|
79
|
+
return self.info.singular_name
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def plural_name(self) -> str | None:
|
|
83
|
+
"""Get the plural name from info."""
|
|
84
|
+
return self.info.plural_name
|
|
85
|
+
|
|
86
|
+
def get_field_type(self, field_name: str) -> str | None:
|
|
87
|
+
"""Get the type of a specific field.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
field_name: Name of the field
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Field type string or None if not found
|
|
94
|
+
"""
|
|
95
|
+
field = self.attributes.get(field_name)
|
|
96
|
+
if isinstance(field, dict):
|
|
97
|
+
return field.get("type")
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def is_relation_field(self, field_name: str) -> bool:
|
|
101
|
+
"""Check if a field is a relation.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
field_name: Name of the field
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if field is a relation
|
|
108
|
+
"""
|
|
109
|
+
return self.get_field_type(field_name) == "relation"
|
|
110
|
+
|
|
111
|
+
def is_component_field(self, field_name: str) -> bool:
|
|
112
|
+
"""Check if a field is a component.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
field_name: Name of the field
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if field is a component
|
|
119
|
+
"""
|
|
120
|
+
return self.get_field_type(field_name) == "component"
|
|
121
|
+
|
|
122
|
+
def get_relation_target(self, field_name: str) -> str | None:
|
|
123
|
+
"""Get the target content type for a relation field.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
field_name: Name of the relation field
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Target content type UID or None
|
|
130
|
+
"""
|
|
131
|
+
field = self.attributes.get(field_name)
|
|
132
|
+
if isinstance(field, dict) and field.get("type") == "relation":
|
|
133
|
+
return field.get("target")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def get_component_uid(self, field_name: str) -> str | None:
|
|
137
|
+
"""Get the component UID for a component field.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
field_name: Name of the component field
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Component UID or None
|
|
144
|
+
"""
|
|
145
|
+
field = self.attributes.get(field_name)
|
|
146
|
+
if isinstance(field, dict) and field.get("type") == "component":
|
|
147
|
+
return field.get("component")
|
|
148
|
+
return None
|
|
@@ -10,6 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, Field, field_validator
|
|
12
12
|
|
|
13
|
+
from strapi_kit.exceptions import FormatError
|
|
14
|
+
|
|
13
15
|
from .schema import ContentTypeSchema
|
|
14
16
|
|
|
15
17
|
|
|
@@ -117,13 +119,13 @@ class ExportedMediaFile(BaseModel):
|
|
|
117
119
|
The validated path
|
|
118
120
|
|
|
119
121
|
Raises:
|
|
120
|
-
|
|
122
|
+
FormatError: If path contains traversal sequences or is absolute
|
|
121
123
|
"""
|
|
122
124
|
if ".." in v or v.startswith("/") or v.startswith("\\"):
|
|
123
|
-
raise
|
|
125
|
+
raise FormatError("local_path must be relative without path traversal")
|
|
124
126
|
# Block Windows drive-letter absolute paths (e.g., C:\, D:/)
|
|
125
127
|
if PureWindowsPath(v).is_absolute():
|
|
126
|
-
raise
|
|
128
|
+
raise FormatError("local_path must be relative without path traversal")
|
|
127
129
|
return v
|
|
128
130
|
|
|
129
131
|
|
|
@@ -36,6 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
import copy
|
|
37
37
|
from typing import Any
|
|
38
38
|
|
|
39
|
+
from strapi_kit.exceptions import ValidationError
|
|
39
40
|
from strapi_kit.models.enums import PublicationState, SortDirection
|
|
40
41
|
from strapi_kit.models.request.fields import FieldSelection
|
|
41
42
|
from strapi_kit.models.request.filters import FilterBuilder
|
|
@@ -220,7 +221,9 @@ class StrapiQuery:
|
|
|
220
221
|
Self for method chaining
|
|
221
222
|
|
|
222
223
|
Raises:
|
|
223
|
-
|
|
224
|
+
strapi_kit.exceptions.ValidationError: If mixing page-based and
|
|
225
|
+
offset-based parameters, or if pagination values are invalid
|
|
226
|
+
(page < 1, page_size < 1, start < 0, limit < 1)
|
|
224
227
|
|
|
225
228
|
Examples:
|
|
226
229
|
>>> # Page-based
|
|
@@ -234,15 +237,15 @@ class StrapiQuery:
|
|
|
234
237
|
offset_based = start is not None or limit is not None
|
|
235
238
|
|
|
236
239
|
if page_based and offset_based:
|
|
237
|
-
raise
|
|
240
|
+
raise ValidationError("Cannot mix page-based and offset-based pagination")
|
|
238
241
|
|
|
239
242
|
if page_based:
|
|
240
243
|
# Validate page value if explicitly provided
|
|
241
244
|
if page is not None and page < 1:
|
|
242
|
-
raise
|
|
245
|
+
raise ValidationError("page must be >= 1")
|
|
243
246
|
# Validate page_size value if explicitly provided
|
|
244
247
|
if page_size is not None and page_size < 1:
|
|
245
|
-
raise
|
|
248
|
+
raise ValidationError("page_size must be >= 1")
|
|
246
249
|
self._pagination = PagePagination(
|
|
247
250
|
page=1 if page is None else page,
|
|
248
251
|
page_size=25 if page_size is None else page_size,
|
|
@@ -251,10 +254,10 @@ class StrapiQuery:
|
|
|
251
254
|
elif offset_based:
|
|
252
255
|
# Validate start value if explicitly provided
|
|
253
256
|
if start is not None and start < 0:
|
|
254
|
-
raise
|
|
257
|
+
raise ValidationError("start must be >= 0")
|
|
255
258
|
# Validate limit value if explicitly provided
|
|
256
259
|
if limit is not None and limit < 1:
|
|
257
|
-
raise
|
|
260
|
+
raise ValidationError("limit must be >= 1")
|
|
258
261
|
self._pagination = OffsetPagination(
|
|
259
262
|
start=0 if start is None else start,
|
|
260
263
|
limit=25 if limit is None else limit,
|