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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Configuration models for strapi-kit.
|
|
2
|
+
|
|
3
|
+
This module defines the configuration structure using Pydantic for
|
|
4
|
+
type safety and validation with support for environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
from pydantic import Field, SecretStr, field_validator
|
|
11
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RetryConfig(BaseSettings):
|
|
15
|
+
"""Configuration for retry behavior.
|
|
16
|
+
|
|
17
|
+
Controls how the client handles failed requests with exponential backoff.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
model_config = SettingsConfigDict(env_prefix="STRAPI_RETRY_")
|
|
21
|
+
|
|
22
|
+
max_attempts: int = Field(
|
|
23
|
+
default=3,
|
|
24
|
+
ge=1,
|
|
25
|
+
le=10,
|
|
26
|
+
description="Maximum number of retry attempts for failed requests",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
initial_wait: float = Field(
|
|
30
|
+
default=1.0,
|
|
31
|
+
ge=0.1,
|
|
32
|
+
le=60.0,
|
|
33
|
+
description="Initial wait time in seconds before first retry",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
max_wait: float = Field(
|
|
37
|
+
default=60.0,
|
|
38
|
+
ge=1.0,
|
|
39
|
+
le=300.0,
|
|
40
|
+
description="Maximum wait time in seconds between retries",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
exponential_base: float = Field(
|
|
44
|
+
default=2.0,
|
|
45
|
+
ge=1.1,
|
|
46
|
+
le=10.0,
|
|
47
|
+
description="Exponential backoff multiplier",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
retry_on_status: set[int] = Field(
|
|
51
|
+
default_factory=lambda: {500, 502, 503, 504},
|
|
52
|
+
description="HTTP status codes that should trigger a retry",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StrapiConfig(BaseSettings):
|
|
57
|
+
"""Main configuration for the Strapi client.
|
|
58
|
+
|
|
59
|
+
This configuration can be loaded from environment variables with
|
|
60
|
+
the STRAPI_ prefix or passed directly as arguments.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
```python
|
|
64
|
+
# From environment variables
|
|
65
|
+
config = StrapiConfig()
|
|
66
|
+
|
|
67
|
+
# From arguments
|
|
68
|
+
config = StrapiConfig(
|
|
69
|
+
base_url="http://localhost:1337",
|
|
70
|
+
api_token="your-token-here"
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
model_config = SettingsConfigDict(
|
|
76
|
+
env_prefix="STRAPI_",
|
|
77
|
+
env_file=".env",
|
|
78
|
+
env_file_encoding="utf-8",
|
|
79
|
+
case_sensitive=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
base_url: str = Field(
|
|
83
|
+
...,
|
|
84
|
+
description="Base URL of the Strapi instance (e.g., http://localhost:1337)",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
api_token: SecretStr = Field(
|
|
88
|
+
...,
|
|
89
|
+
description="API token for authentication",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
api_version: Literal["v4", "v5", "auto"] = Field(
|
|
93
|
+
default="auto",
|
|
94
|
+
description="Strapi API version to use (v4, v5, or auto-detect)",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
timeout: float = Field(
|
|
98
|
+
default=30.0,
|
|
99
|
+
ge=1.0,
|
|
100
|
+
le=300.0,
|
|
101
|
+
description="Request timeout in seconds",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
max_connections: int = Field(
|
|
105
|
+
default=10,
|
|
106
|
+
ge=1,
|
|
107
|
+
le=100,
|
|
108
|
+
description="Maximum number of concurrent connections",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
retry: RetryConfig = Field(
|
|
112
|
+
default_factory=RetryConfig,
|
|
113
|
+
description="Retry configuration",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
rate_limit_per_second: float | None = Field(
|
|
117
|
+
default=None,
|
|
118
|
+
ge=0.1,
|
|
119
|
+
description="Maximum requests per second (None for unlimited)",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
verify_ssl: bool = Field(
|
|
123
|
+
default=True,
|
|
124
|
+
description="Whether to verify SSL certificates",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@field_validator("base_url", mode="before")
|
|
128
|
+
@classmethod
|
|
129
|
+
def validate_base_url(cls, v: str) -> str:
|
|
130
|
+
"""Validate URL format and ensure no trailing slash.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
v: URL string to validate
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Validated URL without trailing slash
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If URL is not a valid HTTP(S) URL
|
|
140
|
+
"""
|
|
141
|
+
if not isinstance(v, str):
|
|
142
|
+
raise ValueError("base_url must be a string")
|
|
143
|
+
|
|
144
|
+
url_str = v.strip().rstrip("/")
|
|
145
|
+
|
|
146
|
+
# Validate URL format
|
|
147
|
+
if not url_str:
|
|
148
|
+
raise ValueError("base_url cannot be empty")
|
|
149
|
+
|
|
150
|
+
if not url_str.startswith(("http://", "https://")):
|
|
151
|
+
raise ValueError(f"base_url must start with http:// or https://, got: {url_str[:50]}")
|
|
152
|
+
|
|
153
|
+
# Use urlsplit for robust URL validation
|
|
154
|
+
parsed = urlsplit(url_str)
|
|
155
|
+
if not parsed.scheme or not parsed.netloc:
|
|
156
|
+
raise ValueError(f"Invalid URL format (missing host): {url_str[:50]}")
|
|
157
|
+
|
|
158
|
+
return url_str
|
|
159
|
+
|
|
160
|
+
def get_api_token(self) -> str:
|
|
161
|
+
"""Get the API token as a plain string.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The API token value
|
|
165
|
+
"""
|
|
166
|
+
return self.api_token.get_secret_value()
|
|
167
|
+
|
|
168
|
+
def get_base_url(self) -> str:
|
|
169
|
+
"""Get the base URL as a plain string.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The base URL value
|
|
173
|
+
"""
|
|
174
|
+
return self.base_url
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Enumerations for Strapi API parameters and types.
|
|
2
|
+
|
|
3
|
+
This module defines core enums used throughout the models package:
|
|
4
|
+
- FilterOperator: 24 operators for query filtering
|
|
5
|
+
- SortDirection: Ascending/descending sort
|
|
6
|
+
- PublicationState: Draft, published, preview content states
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
# Type aliases for common Strapi types
|
|
13
|
+
StrapiVersion = Literal["v4", "v5", "auto"]
|
|
14
|
+
LocaleCode = str # ISO 639-1 language codes (e.g., "en", "fr", "de")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FilterOperator(str, Enum):
|
|
18
|
+
"""Filter operators supported by Strapi REST API.
|
|
19
|
+
|
|
20
|
+
Strapi supports 24 filter operators for querying content.
|
|
21
|
+
All operators work with both v4 and v5 APIs.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> FilterOperator.EQ.value
|
|
25
|
+
'$eq'
|
|
26
|
+
>>> FilterOperator.CONTAINS.value
|
|
27
|
+
'$contains'
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Equality operators
|
|
31
|
+
EQ = "$eq" # Equal
|
|
32
|
+
EQI = "$eqi" # Equal (case-insensitive)
|
|
33
|
+
NE = "$ne" # Not equal
|
|
34
|
+
NEI = "$nei" # Not equal (case-insensitive)
|
|
35
|
+
|
|
36
|
+
# Comparison operators
|
|
37
|
+
LT = "$lt" # Less than
|
|
38
|
+
LTE = "$lte" # Less than or equal
|
|
39
|
+
GT = "$gt" # Greater than
|
|
40
|
+
GTE = "$gte" # Greater than or equal
|
|
41
|
+
|
|
42
|
+
# String matching operators
|
|
43
|
+
CONTAINS = "$contains" # Contains substring
|
|
44
|
+
NOT_CONTAINS = "$notContains" # Does not contain substring
|
|
45
|
+
CONTAINSI = "$containsi" # Contains substring (case-insensitive)
|
|
46
|
+
NOT_CONTAINSI = "$notContainsi" # Does not contain substring (case-insensitive)
|
|
47
|
+
STARTS_WITH = "$startsWith" # Starts with string
|
|
48
|
+
STARTS_WITHI = "$startsWithi" # Starts with string (case-insensitive)
|
|
49
|
+
ENDS_WITH = "$endsWith" # Ends with string
|
|
50
|
+
ENDS_WITHI = "$endsWithi" # Ends with string (case-insensitive)
|
|
51
|
+
|
|
52
|
+
# Array operators
|
|
53
|
+
IN = "$in" # Value is in array
|
|
54
|
+
NOT_IN = "$notIn" # Value is not in array
|
|
55
|
+
|
|
56
|
+
# Null operators
|
|
57
|
+
NULL = "$null" # Value is null
|
|
58
|
+
NOT_NULL = "$notNull" # Value is not null
|
|
59
|
+
|
|
60
|
+
# Date/time range operators
|
|
61
|
+
BETWEEN = "$between" # Value is between two values (inclusive)
|
|
62
|
+
|
|
63
|
+
# Logical operators (used at filter group level)
|
|
64
|
+
AND = "$and" # Logical AND
|
|
65
|
+
OR = "$or" # Logical OR
|
|
66
|
+
NOT = "$not" # Logical NOT
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SortDirection(str, Enum):
|
|
70
|
+
"""Sort direction for query results.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
>>> SortDirection.ASC.value
|
|
74
|
+
'asc'
|
|
75
|
+
>>> SortDirection.DESC.value
|
|
76
|
+
'desc'
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
ASC = "asc" # Ascending order (A-Z, 0-9, oldest-newest)
|
|
80
|
+
DESC = "desc" # Descending order (Z-A, 9-0, newest-oldest)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PublicationState(str, Enum):
|
|
84
|
+
"""Content publication state filter.
|
|
85
|
+
|
|
86
|
+
Only applicable to content types with draft & publish enabled.
|
|
87
|
+
Used to filter between draft and published versions.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
>>> PublicationState.LIVE.value
|
|
91
|
+
'live'
|
|
92
|
+
>>> PublicationState.PREVIEW.value
|
|
93
|
+
'preview'
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
LIVE = "live" # Only published content
|
|
97
|
+
PREVIEW = "preview" # Both draft and published content
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Models for export file format.
|
|
2
|
+
|
|
3
|
+
Defines the structure of exported Strapi data for portability
|
|
4
|
+
and version compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import PureWindowsPath
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
|
|
13
|
+
from .schema import ContentTypeSchema
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExportMetadata(BaseModel):
|
|
17
|
+
"""Metadata about the export.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
version: Export format version (semver)
|
|
21
|
+
strapi_version: Strapi API version (v4 or v5)
|
|
22
|
+
exported_at: ISO timestamp of export
|
|
23
|
+
source_url: Base URL of source Strapi instance
|
|
24
|
+
content_types: List of exported content type UIDs
|
|
25
|
+
total_entities: Total number of entities exported
|
|
26
|
+
total_media: Total number of media files exported
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
version: str = Field(
|
|
30
|
+
default="1.0.0",
|
|
31
|
+
description="Export format version (semver)",
|
|
32
|
+
)
|
|
33
|
+
strapi_version: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="Strapi API version (v4 or v5)",
|
|
36
|
+
)
|
|
37
|
+
exported_at: datetime = Field(
|
|
38
|
+
default_factory=lambda: datetime.now(UTC),
|
|
39
|
+
description="ISO timestamp of export",
|
|
40
|
+
)
|
|
41
|
+
source_url: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="Base URL of source Strapi instance",
|
|
44
|
+
)
|
|
45
|
+
content_types: list[str] = Field(
|
|
46
|
+
default_factory=list,
|
|
47
|
+
description="List of exported content type UIDs",
|
|
48
|
+
)
|
|
49
|
+
total_entities: int = Field(
|
|
50
|
+
default=0,
|
|
51
|
+
description="Total number of entities exported",
|
|
52
|
+
)
|
|
53
|
+
total_media: int = Field(
|
|
54
|
+
default=0,
|
|
55
|
+
description="Total number of media files exported",
|
|
56
|
+
)
|
|
57
|
+
schemas: dict[str, ContentTypeSchema] = Field(
|
|
58
|
+
default_factory=dict,
|
|
59
|
+
description="Content type schemas (for relation resolution)",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ExportedEntity(BaseModel):
|
|
64
|
+
"""A single exported entity with metadata.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
id: Original entity ID
|
|
68
|
+
document_id: Document ID (v5 only)
|
|
69
|
+
content_type: Content type UID
|
|
70
|
+
data: Entity data (attributes)
|
|
71
|
+
relations: Relation field mapping
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
id: int = Field(..., description="Original entity ID")
|
|
75
|
+
document_id: str | None = Field(None, description="Document ID (v5 only)")
|
|
76
|
+
content_type: str = Field(..., description="Content type UID")
|
|
77
|
+
data: dict[str, Any] = Field(..., description="Entity data (attributes)")
|
|
78
|
+
relations: dict[str, list[int | str]] = Field(
|
|
79
|
+
default_factory=dict,
|
|
80
|
+
description="Relation field mapping (field -> [ids])",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ExportedMediaFile(BaseModel):
|
|
85
|
+
"""A media file reference in the export.
|
|
86
|
+
|
|
87
|
+
Attributes:
|
|
88
|
+
id: Original media file ID
|
|
89
|
+
url: Original URL (may be relative or absolute)
|
|
90
|
+
name: File name
|
|
91
|
+
mime: MIME type
|
|
92
|
+
size: File size in bytes
|
|
93
|
+
hash: File hash (for deduplication)
|
|
94
|
+
local_path: Path in export archive (relative)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
id: int = Field(..., description="Original media file ID")
|
|
98
|
+
url: str = Field(..., description="Original URL")
|
|
99
|
+
name: str = Field(..., description="File name")
|
|
100
|
+
mime: str = Field(..., description="MIME type")
|
|
101
|
+
size: int = Field(..., description="File size in bytes")
|
|
102
|
+
hash: str = Field(..., description="File hash")
|
|
103
|
+
local_path: str = Field(..., description="Path in export archive (relative)")
|
|
104
|
+
|
|
105
|
+
@field_validator("local_path")
|
|
106
|
+
@classmethod
|
|
107
|
+
def validate_local_path(cls, v: str) -> str:
|
|
108
|
+
"""Validate local_path doesn't contain path traversal sequences.
|
|
109
|
+
|
|
110
|
+
Prevents malicious exports from reading arbitrary files via
|
|
111
|
+
path traversal attacks (e.g., "../../../etc/passwd").
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
v: The local_path value to validate
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The validated path
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If path contains traversal sequences or is absolute
|
|
121
|
+
"""
|
|
122
|
+
if ".." in v or v.startswith("/") or v.startswith("\\"):
|
|
123
|
+
raise ValueError("local_path must be relative without path traversal")
|
|
124
|
+
# Block Windows drive-letter absolute paths (e.g., C:\, D:/)
|
|
125
|
+
if PureWindowsPath(v).is_absolute():
|
|
126
|
+
raise ValueError("local_path must be relative without path traversal")
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ExportData(BaseModel):
|
|
131
|
+
"""Complete export data structure.
|
|
132
|
+
|
|
133
|
+
This is the root model for exported data, containing metadata,
|
|
134
|
+
entities, and media references.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
metadata: Export metadata
|
|
138
|
+
entities: Exported entities grouped by content type
|
|
139
|
+
media: Media file references
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
metadata: ExportMetadata = Field(..., description="Export metadata")
|
|
143
|
+
entities: dict[str, list[ExportedEntity]] = Field(
|
|
144
|
+
default_factory=dict,
|
|
145
|
+
description="Entities grouped by content type UID",
|
|
146
|
+
)
|
|
147
|
+
media: list[ExportedMediaFile] = Field(
|
|
148
|
+
default_factory=list,
|
|
149
|
+
description="Media file references",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_entity_count(self) -> int:
|
|
153
|
+
"""Get total number of exported entities.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Total entity count across all content types
|
|
157
|
+
"""
|
|
158
|
+
return sum(len(entities) for entities in self.entities.values())
|
|
159
|
+
|
|
160
|
+
def get_media_count(self) -> int:
|
|
161
|
+
"""Get total number of exported media files.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Total media file count
|
|
165
|
+
"""
|
|
166
|
+
return len(self.media)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Models for import configuration and options.
|
|
2
|
+
|
|
3
|
+
Defines how imported data should be processed and validated.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConflictResolution(str, Enum):
|
|
13
|
+
"""Strategy for handling conflicts during import.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
SKIP: Skip entities that already exist
|
|
17
|
+
UPDATE: Update existing entities with imported data
|
|
18
|
+
FAIL: Fail import if conflicts are detected
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
SKIP = "skip"
|
|
22
|
+
UPDATE = "update"
|
|
23
|
+
FAIL = "fail"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ImportOptions(BaseModel):
|
|
27
|
+
"""Configuration for import operations.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
dry_run: Validate without actually importing
|
|
31
|
+
conflict_resolution: How to handle existing entities
|
|
32
|
+
import_media: Whether to import media files
|
|
33
|
+
overwrite_media: Overwrite existing media files
|
|
34
|
+
content_types: Specific content types to import (None = all)
|
|
35
|
+
skip_relations: Skip importing relations (for initial pass)
|
|
36
|
+
validate_relations: Validate relation targets exist
|
|
37
|
+
batch_size: Batch size for bulk operations
|
|
38
|
+
progress_callback: Optional progress callback(current, total, message)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
dry_run: bool = Field(
|
|
42
|
+
default=False,
|
|
43
|
+
description="Validate without actually importing",
|
|
44
|
+
)
|
|
45
|
+
conflict_resolution: ConflictResolution = Field(
|
|
46
|
+
default=ConflictResolution.SKIP,
|
|
47
|
+
description="How to handle existing entities",
|
|
48
|
+
)
|
|
49
|
+
import_media: bool = Field(
|
|
50
|
+
default=True,
|
|
51
|
+
description="Whether to import media files",
|
|
52
|
+
)
|
|
53
|
+
overwrite_media: bool = Field(
|
|
54
|
+
default=False,
|
|
55
|
+
description="Overwrite existing media files",
|
|
56
|
+
)
|
|
57
|
+
content_types: list[str] | None = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Specific content types to import (None = all)",
|
|
60
|
+
)
|
|
61
|
+
skip_relations: bool = Field(
|
|
62
|
+
default=False,
|
|
63
|
+
description="Skip importing relations (for initial pass)",
|
|
64
|
+
)
|
|
65
|
+
validate_relations: bool = Field(
|
|
66
|
+
default=True,
|
|
67
|
+
description="Validate relation targets exist",
|
|
68
|
+
)
|
|
69
|
+
batch_size: int = Field(
|
|
70
|
+
default=10,
|
|
71
|
+
ge=1,
|
|
72
|
+
le=100,
|
|
73
|
+
description="Batch size for bulk operations",
|
|
74
|
+
)
|
|
75
|
+
progress_callback: Callable[[int, int, str], None] | None = Field(
|
|
76
|
+
default=None,
|
|
77
|
+
description="Optional progress callback(current, total, message)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ImportResult(BaseModel):
|
|
84
|
+
"""Result of an import operation.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
success: Whether import succeeded
|
|
88
|
+
dry_run: Whether this was a dry run
|
|
89
|
+
entities_imported: Number of entities imported
|
|
90
|
+
entities_skipped: Number of entities skipped
|
|
91
|
+
entities_updated: Number of entities updated
|
|
92
|
+
entities_failed: Number of entities that failed
|
|
93
|
+
media_imported: Number of media files imported
|
|
94
|
+
media_skipped: Number of media files skipped
|
|
95
|
+
errors: List of error messages
|
|
96
|
+
warnings: List of warning messages
|
|
97
|
+
id_mapping: Mapping of old IDs to new IDs (content_type -> {old_id: new_id})
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
success: bool = Field(..., description="Whether import succeeded")
|
|
101
|
+
dry_run: bool = Field(..., description="Whether this was a dry run")
|
|
102
|
+
entities_imported: int = Field(default=0, description="Entities imported")
|
|
103
|
+
entities_skipped: int = Field(default=0, description="Entities skipped")
|
|
104
|
+
entities_updated: int = Field(default=0, description="Entities updated")
|
|
105
|
+
entities_failed: int = Field(default=0, description="Entities failed")
|
|
106
|
+
media_imported: int = Field(default=0, description="Media files imported")
|
|
107
|
+
media_skipped: int = Field(default=0, description="Media files skipped")
|
|
108
|
+
errors: list[str] = Field(default_factory=list, description="Error messages")
|
|
109
|
+
warnings: list[str] = Field(default_factory=list, description="Warning messages")
|
|
110
|
+
id_mapping: dict[str, dict[int, int]] = Field(
|
|
111
|
+
default_factory=dict,
|
|
112
|
+
description="Mapping of old IDs to new IDs per content type",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def add_error(self, error: str) -> None:
|
|
116
|
+
"""Add an error message.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
error: Error message to add
|
|
120
|
+
"""
|
|
121
|
+
self.errors.append(error)
|
|
122
|
+
|
|
123
|
+
def add_warning(self, warning: str) -> None:
|
|
124
|
+
"""Add a warning message.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
warning: Warning message to add
|
|
128
|
+
"""
|
|
129
|
+
self.warnings.append(warning)
|
|
130
|
+
|
|
131
|
+
def get_total_processed(self) -> int:
|
|
132
|
+
"""Get total number of entities processed.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Sum of imported, skipped, updated, and failed
|
|
136
|
+
"""
|
|
137
|
+
return (
|
|
138
|
+
self.entities_imported
|
|
139
|
+
+ self.entities_skipped
|
|
140
|
+
+ self.entities_updated
|
|
141
|
+
+ self.entities_failed
|
|
142
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Request models for building Strapi API queries."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Field selection for Strapi API queries.
|
|
2
|
+
|
|
3
|
+
Allows selecting specific fields to return in the response, reducing
|
|
4
|
+
payload size and improving performance.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Select specific fields:
|
|
8
|
+
>>> fields = FieldSelection(fields=["title", "description", "publishedAt"])
|
|
9
|
+
>>> fields.to_query_dict()
|
|
10
|
+
{'fields': ['title', 'description', 'publishedAt']}
|
|
11
|
+
|
|
12
|
+
Select all fields (default):
|
|
13
|
+
>>> fields = FieldSelection() # Returns all fields
|
|
14
|
+
>>> fields.to_query_dict()
|
|
15
|
+
{}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FieldSelection(BaseModel):
|
|
24
|
+
"""Field selection configuration.
|
|
25
|
+
|
|
26
|
+
Specifies which fields to include in the response. If no fields are
|
|
27
|
+
specified, all fields are returned by default.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
fields: List of field names to return (empty = all fields)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
fields: list[str] = Field(
|
|
34
|
+
default_factory=list,
|
|
35
|
+
description="List of field names to include in response (empty = all)",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def to_query_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""Convert to query parameters dictionary.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary with fields parameter, or empty if no fields specified
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> FieldSelection(fields=["title", "description"]).to_query_dict()
|
|
46
|
+
{'fields': ['title', 'description']}
|
|
47
|
+
|
|
48
|
+
>>> FieldSelection().to_query_dict()
|
|
49
|
+
{}
|
|
50
|
+
"""
|
|
51
|
+
if not self.fields:
|
|
52
|
+
return {}
|
|
53
|
+
return {"fields": self.fields}
|
|
54
|
+
|
|
55
|
+
def to_query_list(self) -> list[str]:
|
|
56
|
+
"""Get list of field names.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of field names
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
>>> FieldSelection(fields=["title", "description"]).to_query_list()
|
|
63
|
+
['title', 'description']
|
|
64
|
+
"""
|
|
65
|
+
return self.fields
|