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 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.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 ValueError("API token is required and cannot be empty")
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)
@@ -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
 
@@ -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
- ValueError: If mixing page-based and offset-based parameters
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 ValueError("Cannot mix page-based and offset-based pagination")
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 ValueError("page must be >= 1")
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 ValueError("page_size must be >= 1")
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 ValueError("start must be >= 0")
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 ValueError("limit must be >= 1")
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,