strapi-kit 0.0.2__py3-none-any.whl → 0.0.4__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 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.2'
32
- __version_tuple__ = version_tuple = (0, 0, 2)
31
+ __version__ = version = '0.0.4'
32
+ __version_tuple__ = version_tuple = (0, 0, 4)
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,9 +5,14 @@ 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
15
+ from pydantic import ValidationError as PydanticValidationError
11
16
  from tenacity import (
12
17
  before_sleep_log,
13
18
  retry,
@@ -20,6 +25,7 @@ from ..auth.api_token import APITokenAuth
20
25
  from ..exceptions import (
21
26
  AuthenticationError,
22
27
  AuthorizationError,
28
+ ConfigurationError,
23
29
  ConflictError,
24
30
  NotFoundError,
25
31
  RateLimitError,
@@ -70,7 +76,7 @@ class BaseClient:
70
76
  parser: Response parser (defaults to VersionDetectingParser)
71
77
 
72
78
  Raises:
73
- ValueError: If authentication token is invalid
79
+ ConfigurationError: If authentication token is invalid
74
80
  """
75
81
  self.config: ConfigProvider = config
76
82
  self.base_url = config.get_base_url()
@@ -83,7 +89,7 @@ class BaseClient:
83
89
 
84
90
  # Validate authentication
85
91
  if not self.auth.validate_token():
86
- raise ValueError("API token is required and cannot be empty")
92
+ raise ConfigurationError("API token is required and cannot be empty")
87
93
 
88
94
  # API version detection (for backward compatibility)
89
95
  self._api_version: Literal["v4", "v5"] | None = (
@@ -458,3 +464,93 @@ class BaseClient:
458
464
 
459
465
  # Media list follows standard collection format
460
466
  return self._parse_collection_response(response_data)
467
+
468
+ def _parse_content_types_response(
469
+ self,
470
+ response_data: dict[str, Any],
471
+ include_plugins: bool = False,
472
+ ) -> list["ContentTypeListItem"]:
473
+ """Parse content-type-builder content types response.
474
+
475
+ Args:
476
+ response_data: Raw JSON response from content-type-builder
477
+ include_plugins: Whether to include plugin content types
478
+
479
+ Returns:
480
+ List of ContentTypeListItem instances
481
+ """
482
+ from ..models.content_type import ContentTypeListItem
483
+
484
+ data = response_data.get("data", [])
485
+ result = []
486
+
487
+ for item in data:
488
+ uid = item.get("uid", "")
489
+ # Filter out plugin content types if not requested
490
+ if not include_plugins and uid.startswith("plugin::"):
491
+ continue
492
+
493
+ try:
494
+ content_type = ContentTypeListItem.model_validate(item)
495
+ result.append(content_type)
496
+ except PydanticValidationError as e:
497
+ # Skip malformed items
498
+ logger.warning(f"Failed to parse content type: {uid}", exc_info=e)
499
+ continue
500
+
501
+ return result
502
+
503
+ def _parse_components_response(
504
+ self,
505
+ response_data: dict[str, Any],
506
+ ) -> list["ComponentListItem"]:
507
+ """Parse content-type-builder components response.
508
+
509
+ Args:
510
+ response_data: Raw JSON response from content-type-builder
511
+
512
+ Returns:
513
+ List of ComponentListItem instances
514
+ """
515
+ from ..models.content_type import ComponentListItem
516
+
517
+ data = response_data.get("data", [])
518
+ result = []
519
+
520
+ for item in data:
521
+ uid = item.get("uid", "")
522
+ try:
523
+ component = ComponentListItem.model_validate(item)
524
+ result.append(component)
525
+ except PydanticValidationError as e:
526
+ # Skip malformed items
527
+ logger.warning(f"Failed to parse component: {uid}", exc_info=e)
528
+ continue
529
+
530
+ return result
531
+
532
+ def _parse_content_type_schema_response(
533
+ self,
534
+ response_data: dict[str, Any],
535
+ ) -> "CTBContentTypeSchema":
536
+ """Parse content-type-builder single content type schema response.
537
+
538
+ Args:
539
+ response_data: Raw JSON response from content-type-builder
540
+
541
+ Returns:
542
+ CTBContentTypeSchema instance
543
+
544
+ Raises:
545
+ ValidationError: If response cannot be parsed
546
+ """
547
+ from ..models.content_type import ContentTypeSchema as CTBContentTypeSchema
548
+
549
+ data = response_data.get("data", response_data)
550
+ try:
551
+ return CTBContentTypeSchema.model_validate(data)
552
+ except PydanticValidationError as e:
553
+ raise ValidationError(
554
+ "Invalid content type schema response",
555
+ details={"errors": e.errors()},
556
+ ) from e
@@ -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)
@@ -9,25 +9,10 @@ from typing import Any
9
9
 
10
10
  from pydantic import SecretStr, ValidationError
11
11
 
12
- from .exceptions import StrapiError
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",
@@ -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
 
@@ -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
- ValueError: If include_media=True but media_dir is not provided
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 ValueError("media_dir must be provided when include_media=True")
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
- # media_dir is guaranteed non-None here (validated at method start)
165
- assert media_dir is not None
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
  )
@@ -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
- ValueError: If path contains traversal sequences or is absolute
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 ValueError("local_path must be relative without path traversal")
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 ValueError("local_path must be relative without path traversal")
128
+ raise FormatError("local_path must be relative without path traversal")
127
129
  return v
128
130
 
129
131