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,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