strapi-kit 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. strapi_kit/__init__.py +97 -0
  2. strapi_kit/__version__.py +15 -0
  3. strapi_kit/_version.py +34 -0
  4. strapi_kit/auth/__init__.py +7 -0
  5. strapi_kit/auth/api_token.py +48 -0
  6. strapi_kit/cache/__init__.py +5 -0
  7. strapi_kit/cache/schema_cache.py +211 -0
  8. strapi_kit/client/__init__.py +11 -0
  9. strapi_kit/client/async_client.py +1032 -0
  10. strapi_kit/client/base.py +460 -0
  11. strapi_kit/client/sync_client.py +980 -0
  12. strapi_kit/config_provider.py +368 -0
  13. strapi_kit/exceptions/__init__.py +37 -0
  14. strapi_kit/exceptions/errors.py +205 -0
  15. strapi_kit/export/__init__.py +10 -0
  16. strapi_kit/export/exporter.py +384 -0
  17. strapi_kit/export/importer.py +619 -0
  18. strapi_kit/export/media_handler.py +322 -0
  19. strapi_kit/export/relation_resolver.py +172 -0
  20. strapi_kit/models/__init__.py +104 -0
  21. strapi_kit/models/bulk.py +69 -0
  22. strapi_kit/models/config.py +174 -0
  23. strapi_kit/models/enums.py +97 -0
  24. strapi_kit/models/export_format.py +166 -0
  25. strapi_kit/models/import_options.py +142 -0
  26. strapi_kit/models/request/__init__.py +1 -0
  27. strapi_kit/models/request/fields.py +65 -0
  28. strapi_kit/models/request/filters.py +611 -0
  29. strapi_kit/models/request/pagination.py +168 -0
  30. strapi_kit/models/request/populate.py +281 -0
  31. strapi_kit/models/request/query.py +429 -0
  32. strapi_kit/models/request/sort.py +147 -0
  33. strapi_kit/models/response/__init__.py +1 -0
  34. strapi_kit/models/response/base.py +75 -0
  35. strapi_kit/models/response/component.py +67 -0
  36. strapi_kit/models/response/media.py +91 -0
  37. strapi_kit/models/response/meta.py +44 -0
  38. strapi_kit/models/response/normalized.py +168 -0
  39. strapi_kit/models/response/relation.py +48 -0
  40. strapi_kit/models/response/v4.py +70 -0
  41. strapi_kit/models/response/v5.py +57 -0
  42. strapi_kit/models/schema.py +93 -0
  43. strapi_kit/operations/__init__.py +16 -0
  44. strapi_kit/operations/media.py +226 -0
  45. strapi_kit/operations/streaming.py +144 -0
  46. strapi_kit/parsers/__init__.py +5 -0
  47. strapi_kit/parsers/version_detecting.py +171 -0
  48. strapi_kit/protocols.py +455 -0
  49. strapi_kit/utils/__init__.py +15 -0
  50. strapi_kit/utils/rate_limiter.py +201 -0
  51. strapi_kit/utils/uid.py +88 -0
  52. strapi_kit-0.0.1.dist-info/METADATA +1098 -0
  53. strapi_kit-0.0.1.dist-info/RECORD +55 -0
  54. strapi_kit-0.0.1.dist-info/WHEEL +4 -0
  55. strapi_kit-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,368 @@
1
+ """Configuration provider and factory for dependency injection.
2
+
3
+ This module provides a flexible way to create StrapiConfig instances from
4
+ various sources (environment variables, .env files, dictionaries, etc.).
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from pydantic import SecretStr, ValidationError
11
+
12
+ from .exceptions import StrapiError
13
+ from .models.config import RetryConfig, StrapiConfig
14
+
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
+ class ConfigFactory:
32
+ """Factory for creating StrapiConfig instances from various sources.
33
+
34
+ This class provides a flexible dependency injection pattern for configuration,
35
+ allowing configs to be loaded from:
36
+ - Environment variables
37
+ - .env files (with custom paths)
38
+ - Dictionaries
39
+ - Default values
40
+
41
+ Examples:
42
+ >>> # Load from .env file in current directory
43
+ >>> config = ConfigFactory.from_env()
44
+
45
+ >>> # Load from custom .env file
46
+ >>> config = ConfigFactory.from_env_file("/path/to/.env")
47
+
48
+ >>> # Load from dictionary
49
+ >>> config = ConfigFactory.from_dict({
50
+ ... "base_url": "http://localhost:1337",
51
+ ... "api_token": "secret-token"
52
+ ... })
53
+
54
+ >>> # Load with custom search paths
55
+ >>> config = ConfigFactory.from_env(
56
+ ... search_paths=[".env", ".env.local", "~/.strapi/.env"]
57
+ ... )
58
+
59
+ >>> # Create with explicit values (no .env loading)
60
+ >>> config = ConfigFactory.create(
61
+ ... base_url="http://localhost:1337",
62
+ ... api_token="secret-token"
63
+ ... )
64
+ """
65
+
66
+ @staticmethod
67
+ def from_env(
68
+ *,
69
+ search_paths: list[str | Path] | None = None,
70
+ required: bool = False,
71
+ ) -> StrapiConfig:
72
+ """Load configuration from environment variables and .env files.
73
+
74
+ Searches for .env files in the specified paths and loads the first one found.
75
+ Environment variables always take precedence over .env file values.
76
+
77
+ Args:
78
+ search_paths: List of paths to search for .env files
79
+ (default: [".env", ".env.local", "~/.config/strapi/.env"])
80
+ required: If True, raises ConfigurationError if no .env file is found
81
+
82
+ Returns:
83
+ Configured StrapiConfig instance
84
+
85
+ Raises:
86
+ ConfigurationError: If required=True and no .env file found,
87
+ or if configuration values are invalid (wraps ValidationError)
88
+
89
+ Example:
90
+ >>> config = ConfigFactory.from_env(
91
+ ... search_paths=[".env", ".env.production"],
92
+ ... required=True
93
+ ... )
94
+ """
95
+ if search_paths is None:
96
+ search_paths = [
97
+ ".env",
98
+ ".env.local",
99
+ Path.home() / ".config" / "strapi" / ".env",
100
+ ]
101
+
102
+ # Find first existing .env file
103
+ env_file = None
104
+ for path in search_paths:
105
+ resolved_path = Path(path).expanduser().resolve()
106
+ if resolved_path.exists():
107
+ env_file = resolved_path
108
+ break
109
+
110
+ if required and env_file is None:
111
+ searched = [str(Path(p).expanduser().resolve()) for p in search_paths]
112
+ raise ConfigurationError(f"No .env file found. Searched: {', '.join(searched)}")
113
+
114
+ try:
115
+ # Load with custom env_file path
116
+ if env_file:
117
+ return StrapiConfig(_env_file=str(env_file)) # type: ignore[call-arg]
118
+ else:
119
+ # Load from environment variables only
120
+ return StrapiConfig(_env_file=None) # type: ignore[call-arg]
121
+
122
+ except ValidationError as e:
123
+ raise ConfigurationError(f"Invalid configuration: {e.error_count()} errors\n{e}") from e
124
+
125
+ @staticmethod
126
+ def from_env_file(
127
+ env_file: str | Path,
128
+ *,
129
+ required: bool = True,
130
+ ) -> StrapiConfig:
131
+ """Load configuration from a specific .env file.
132
+
133
+ Args:
134
+ env_file: Path to the .env file
135
+ required: If True, raises error if file doesn't exist
136
+
137
+ Returns:
138
+ Configured StrapiConfig instance
139
+
140
+ Raises:
141
+ ConfigurationError: If file doesn't exist (when required=True),
142
+ or if configuration values are invalid (wraps ValidationError)
143
+
144
+ Example:
145
+ >>> config = ConfigFactory.from_env_file("/etc/strapi/.env")
146
+ """
147
+ resolved_path = Path(env_file).expanduser().resolve()
148
+
149
+ if required and not resolved_path.exists():
150
+ raise ConfigurationError(f".env file not found: {resolved_path}")
151
+
152
+ try:
153
+ return StrapiConfig( # type: ignore[call-arg]
154
+ _env_file=str(resolved_path) if resolved_path.exists() else None
155
+ )
156
+ except ValidationError as e:
157
+ raise ConfigurationError(
158
+ f"Invalid configuration in {resolved_path}: {e.error_count()} errors\n{e}"
159
+ ) from e
160
+
161
+ @staticmethod
162
+ def from_dict(config_dict: dict[str, Any]) -> StrapiConfig:
163
+ """Create configuration from a dictionary.
164
+
165
+ Args:
166
+ config_dict: Dictionary with configuration values
167
+
168
+ Returns:
169
+ Configured StrapiConfig instance
170
+
171
+ Raises:
172
+ ConfigurationError: If configuration values are invalid (wraps ValidationError)
173
+
174
+ Example:
175
+ >>> config = ConfigFactory.from_dict({
176
+ ... "base_url": "http://localhost:1337",
177
+ ... "api_token": "secret-token",
178
+ ... "timeout": 60.0,
179
+ ... "retry": {
180
+ ... "max_attempts": 5,
181
+ ... "initial_wait": 2.0
182
+ ... }
183
+ ... })
184
+ """
185
+ try:
186
+ # Disable .env loading when creating from dict
187
+ return StrapiConfig(_env_file=None, **config_dict) # type: ignore[call-arg]
188
+ except ValidationError as e:
189
+ raise ConfigurationError(f"Invalid configuration: {e.error_count()} errors\n{e}") from e
190
+
191
+ @staticmethod
192
+ def create(
193
+ *,
194
+ base_url: str,
195
+ api_token: str,
196
+ api_version: str = "auto",
197
+ timeout: float = 30.0,
198
+ max_connections: int = 10,
199
+ retry: RetryConfig | dict[str, Any] | None = None,
200
+ rate_limit_per_second: float | None = None,
201
+ verify_ssl: bool = True,
202
+ ) -> StrapiConfig:
203
+ """Create configuration with explicit values (no .env loading).
204
+
205
+ Args:
206
+ base_url: Base URL of Strapi instance
207
+ api_token: API authentication token
208
+ api_version: API version (v4, v5, or auto)
209
+ timeout: Request timeout in seconds
210
+ max_connections: Maximum concurrent connections
211
+ retry: Retry configuration (RetryConfig instance or dict)
212
+ rate_limit_per_second: Maximum requests per second
213
+ verify_ssl: Whether to verify SSL certificates
214
+
215
+ Returns:
216
+ Configured StrapiConfig instance
217
+
218
+ Raises:
219
+ ConfigurationError: If validation fails
220
+
221
+ Example:
222
+ >>> config = ConfigFactory.create(
223
+ ... base_url="http://localhost:1337",
224
+ ... api_token="secret-token",
225
+ ... timeout=60.0
226
+ ... )
227
+ """
228
+ try:
229
+ # Convert retry dict to RetryConfig if needed
230
+ retry_config: RetryConfig
231
+ if retry is None:
232
+ retry_config = RetryConfig()
233
+ elif isinstance(retry, dict):
234
+ retry_config = RetryConfig(**retry)
235
+ else:
236
+ retry_config = retry
237
+
238
+ return StrapiConfig( # type: ignore[call-arg]
239
+ _env_file=None, # Disable .env loading
240
+ base_url=base_url,
241
+ api_token=SecretStr(api_token),
242
+ api_version=api_version, # type: ignore[arg-type]
243
+ timeout=timeout,
244
+ max_connections=max_connections,
245
+ retry=retry_config,
246
+ rate_limit_per_second=rate_limit_per_second,
247
+ verify_ssl=verify_ssl,
248
+ )
249
+ except ValidationError as e:
250
+ raise ConfigurationError(f"Invalid configuration: {e.error_count()} errors\n{e}") from e
251
+
252
+ @staticmethod
253
+ def from_environment_only() -> StrapiConfig:
254
+ """Load configuration from environment variables only (no .env files).
255
+
256
+ This is useful in containerized environments where configuration
257
+ is injected via environment variables.
258
+
259
+ Returns:
260
+ Configured StrapiConfig instance
261
+
262
+ Raises:
263
+ ConfigurationError: If validation fails
264
+
265
+ Example:
266
+ >>> # Set env vars first:
267
+ >>> # export STRAPI_BASE_URL=http://localhost:1337
268
+ >>> # export STRAPI_API_TOKEN=secret-token
269
+ >>> config = ConfigFactory.from_environment_only()
270
+ """
271
+ try:
272
+ return StrapiConfig(_env_file=None) # type: ignore[call-arg]
273
+ except ValidationError as e:
274
+ raise ConfigurationError(
275
+ f"Invalid configuration from environment: {e.error_count()} errors\n{e}"
276
+ ) from e
277
+
278
+ @staticmethod
279
+ def merge(
280
+ *configs: StrapiConfig,
281
+ base: StrapiConfig | None = None,
282
+ ) -> StrapiConfig:
283
+ """Merge multiple configurations with later configs overriding earlier ones.
284
+
285
+ Args:
286
+ *configs: Configuration instances to merge
287
+ base: Optional base configuration (merged first)
288
+
289
+ Returns:
290
+ Merged StrapiConfig instance
291
+
292
+ Example:
293
+ >>> base_config = ConfigFactory.from_env_file("base.env")
294
+ >>> override_config = ConfigFactory.from_dict({"timeout": 60.0})
295
+ >>> final_config = ConfigFactory.merge(base_config, override_config)
296
+ """
297
+ if not configs and base is None:
298
+ raise ValueError("At least one config must be provided")
299
+
300
+ all_configs = [base] if base else []
301
+ all_configs.extend(configs)
302
+
303
+ # Start with first config's dict
304
+ merged_dict: dict[str, Any] = all_configs[0].model_dump(exclude_unset=False)
305
+
306
+ # Merge each subsequent config
307
+ for config in all_configs[1:]:
308
+ config_dict = config.model_dump(exclude_unset=True)
309
+ merged_dict.update(config_dict)
310
+
311
+ return ConfigFactory.from_dict(merged_dict)
312
+
313
+
314
+ # Convenience functions for common patterns
315
+
316
+
317
+ def load_config(
318
+ env_file: str | Path | None = None,
319
+ *,
320
+ required: bool = False,
321
+ ) -> StrapiConfig:
322
+ """Convenience function to load configuration.
323
+
324
+ Args:
325
+ env_file: Optional path to .env file (searches defaults if None)
326
+ required: If True, raises error if no .env file found
327
+
328
+ Returns:
329
+ Configured StrapiConfig instance
330
+
331
+ Example:
332
+ >>> # Load from default locations
333
+ >>> config = load_config()
334
+
335
+ >>> # Load from specific file
336
+ >>> config = load_config("/path/to/.env")
337
+
338
+ >>> # Require .env file
339
+ >>> config = load_config(required=True)
340
+ """
341
+ if env_file:
342
+ return ConfigFactory.from_env_file(env_file, required=required)
343
+ return ConfigFactory.from_env(required=required)
344
+
345
+
346
+ def create_config(
347
+ base_url: str,
348
+ api_token: str,
349
+ **kwargs: Any,
350
+ ) -> StrapiConfig:
351
+ """Convenience function to create configuration with explicit values.
352
+
353
+ Args:
354
+ base_url: Base URL of Strapi instance
355
+ api_token: API authentication token
356
+ **kwargs: Additional configuration options
357
+
358
+ Returns:
359
+ Configured StrapiConfig instance
360
+
361
+ Example:
362
+ >>> config = create_config(
363
+ ... base_url="http://localhost:1337",
364
+ ... api_token="secret-token",
365
+ ... timeout=60.0
366
+ ... )
367
+ """
368
+ return ConfigFactory.create(base_url=base_url, api_token=api_token, **kwargs)
@@ -0,0 +1,37 @@
1
+ """Exception classes for strapi-kit."""
2
+
3
+ from .errors import (
4
+ AuthenticationError,
5
+ AuthorizationError,
6
+ ConflictError,
7
+ ConnectionError,
8
+ FormatError,
9
+ ImportExportError,
10
+ MediaError,
11
+ NetworkError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ RelationError,
15
+ ServerError,
16
+ StrapiError,
17
+ TimeoutError,
18
+ ValidationError,
19
+ )
20
+
21
+ __all__ = [
22
+ "StrapiError",
23
+ "AuthenticationError",
24
+ "AuthorizationError",
25
+ "NotFoundError",
26
+ "ValidationError",
27
+ "ConflictError",
28
+ "NetworkError",
29
+ "ConnectionError",
30
+ "TimeoutError",
31
+ "RateLimitError",
32
+ "ServerError",
33
+ "ImportExportError",
34
+ "FormatError",
35
+ "RelationError",
36
+ "MediaError",
37
+ ]
@@ -0,0 +1,205 @@
1
+ """Exception hierarchy for strapi-kit.
2
+
3
+ This module defines all custom exceptions used throughout the package,
4
+ organized in a clear hierarchy for better error handling.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+
10
+ class StrapiError(Exception):
11
+ """Base exception for all strapi-kit errors.
12
+
13
+ All custom exceptions in this package inherit from this class,
14
+ making it easy to catch all package-specific errors.
15
+ """
16
+
17
+ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
18
+ """Initialize the exception.
19
+
20
+ Args:
21
+ message: Human-readable error message
22
+ details: Optional dictionary with additional error context
23
+ """
24
+ super().__init__(message)
25
+ self.message = message
26
+ self.details = details or {}
27
+
28
+ def __str__(self) -> str:
29
+ """Return string representation of the error."""
30
+ if self.details:
31
+ return f"{self.message} (details: {self.details})"
32
+ return self.message
33
+
34
+
35
+ # HTTP Status Code Related Errors
36
+
37
+
38
+ class AuthenticationError(StrapiError):
39
+ """Raised when authentication fails (HTTP 401).
40
+
41
+ This typically means the API token is invalid, expired, or missing.
42
+ """
43
+
44
+ pass
45
+
46
+
47
+ class AuthorizationError(StrapiError):
48
+ """Raised when authorization fails (HTTP 403).
49
+
50
+ The authentication was successful, but the user doesn't have
51
+ permission to access the requested resource.
52
+ """
53
+
54
+ pass
55
+
56
+
57
+ class NotFoundError(StrapiError):
58
+ """Raised when a resource is not found (HTTP 404).
59
+
60
+ This can mean the content type, document ID, or endpoint doesn't exist.
61
+ """
62
+
63
+ pass
64
+
65
+
66
+ class ValidationError(StrapiError):
67
+ """Raised when request validation fails (HTTP 400).
68
+
69
+ This typically means the request data doesn't match the expected schema
70
+ or contains invalid values.
71
+ """
72
+
73
+ pass
74
+
75
+
76
+ class ConflictError(StrapiError):
77
+ """Raised when a conflict occurs (HTTP 409).
78
+
79
+ This typically happens when trying to create a resource that already exists
80
+ or when there's a version conflict during updates.
81
+ """
82
+
83
+ pass
84
+
85
+
86
+ class ServerError(StrapiError):
87
+ """Raised when the server returns a 5xx error.
88
+
89
+ This indicates an internal server error that is typically temporary
90
+ and may succeed if retried.
91
+ """
92
+
93
+ def __init__(
94
+ self, message: str, status_code: int, details: dict[str, Any] | None = None
95
+ ) -> None:
96
+ """Initialize server error with status code.
97
+
98
+ Args:
99
+ message: Human-readable error message
100
+ status_code: HTTP status code (5xx)
101
+ details: Optional dictionary with additional error context
102
+ """
103
+ super().__init__(message, details)
104
+ self.status_code = status_code
105
+
106
+
107
+ # Network Related Errors
108
+
109
+
110
+ class NetworkError(StrapiError):
111
+ """Base class for network-related errors.
112
+
113
+ This is raised when there's a problem with the network connection
114
+ rather than an HTTP error response.
115
+ """
116
+
117
+ pass
118
+
119
+
120
+ class ConnectionError(NetworkError):
121
+ """Raised when a connection to the server cannot be established.
122
+
123
+ This typically means the server is unreachable or the URL is incorrect.
124
+ """
125
+
126
+ pass
127
+
128
+
129
+ class TimeoutError(NetworkError):
130
+ """Raised when a request times out.
131
+
132
+ The server didn't respond within the configured timeout period.
133
+ """
134
+
135
+ pass
136
+
137
+
138
+ class RateLimitError(NetworkError):
139
+ """Raised when rate limit is exceeded (HTTP 429).
140
+
141
+ The client has sent too many requests in a given time period.
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ message: str,
147
+ retry_after: int | None = None,
148
+ details: dict[str, Any] | None = None,
149
+ ) -> None:
150
+ """Initialize rate limit error.
151
+
152
+ Args:
153
+ message: Human-readable error message
154
+ retry_after: Seconds to wait before retrying (from Retry-After header)
155
+ details: Optional dictionary with additional error context
156
+ """
157
+ super().__init__(message, details)
158
+ self.retry_after = retry_after
159
+
160
+
161
+ # Import/Export Related Errors
162
+
163
+
164
+ class ImportExportError(StrapiError):
165
+ """Base class for import/export related errors.
166
+
167
+ Raised during data export or import operations when something goes wrong.
168
+ """
169
+
170
+ pass
171
+
172
+
173
+ class FormatError(ImportExportError):
174
+ """Raised when data format is invalid or unsupported.
175
+
176
+ This happens when the import data doesn't match the expected format
177
+ or contains malformed JSON/data structures.
178
+ """
179
+
180
+ pass
181
+
182
+
183
+ class RelationError(ImportExportError):
184
+ """Raised when there's an error resolving or mapping relations.
185
+
186
+ This can happen when:
187
+ - A referenced document doesn't exist
188
+ - Circular relations are detected
189
+ - Relation format is invalid
190
+ """
191
+
192
+ pass
193
+
194
+
195
+ class MediaError(ImportExportError):
196
+ """Raised when there's an error handling media files.
197
+
198
+ This can happen during:
199
+ - Media file download (export)
200
+ - Media file upload (import)
201
+ - Invalid media references
202
+ - File system errors
203
+ """
204
+
205
+ pass
@@ -0,0 +1,10 @@
1
+ """Export and import functionality for Strapi data.
2
+
3
+ This package provides tools for exporting and importing Strapi content types,
4
+ entities, and media files in a portable format.
5
+ """
6
+
7
+ from strapi_kit.export.exporter import StrapiExporter
8
+ from strapi_kit.export.importer import StrapiImporter
9
+
10
+ __all__ = ["StrapiExporter", "StrapiImporter"]