wherobots-python-dbapi 0.22.0__tar.gz → 0.23.0__tar.gz

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 (17) hide show
  1. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/PKG-INFO +42 -2
  2. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/README.md +40 -0
  3. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/pyproject.toml +2 -2
  4. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/__init__.py +5 -0
  5. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/connection.py +48 -13
  6. wherobots_python_dbapi-0.23.0/wherobots/db/constants.py +22 -0
  7. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/cursor.py +51 -18
  8. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/driver.py +7 -5
  9. wherobots_python_dbapi-0.23.0/wherobots/db/models.py +80 -0
  10. wherobots_python_dbapi-0.22.0/wherobots/db/constants.py → wherobots_python_dbapi-0.23.0/wherobots/db/types.py +6 -20
  11. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/.gitignore +0 -0
  12. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/LICENSE +0 -0
  13. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/__init__.py +0 -0
  14. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/errors.py +0 -0
  15. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/region.py +0 -0
  16. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/runtime.py +0 -0
  17. {wherobots_python_dbapi-0.22.0 → wherobots_python_dbapi-0.23.0}/wherobots/db/session_type.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wherobots-python-dbapi
3
- Version: 0.22.0
3
+ Version: 0.23.0
4
4
  Summary: Python DB-API driver for Wherobots DB
5
5
  Project-URL: Homepage, https://github.com/wherobots/wherobots-python-dbapi-driver
6
6
  Project-URL: Tracker, https://github.com/wherobots/wherobots-python-dbapi-driver/issues
7
7
  Author-email: Maxime Petazzoni <max@wherobots.com>
8
8
  License-Expression: Apache-2.0
9
9
  License-File: LICENSE
10
- Requires-Python: <4,>=3.8
10
+ Requires-Python: <4,>=3.10
11
11
  Requires-Dist: cbor2>=5.6.3
12
12
  Requires-Dist: packaging
13
13
  Requires-Dist: pandas
@@ -83,6 +83,46 @@ It also implements the `close()` method, as suggested by the PEP-2049
83
83
  specification, to support situations where the cursor is wrapped in a
84
84
  `contextmanager.closing()`.
85
85
 
86
+ ### Storing results in cloud storage
87
+
88
+ For large query results, you can store them directly in cloud storage
89
+ instead of retrieving them over the connection. This is useful when
90
+ results are too large to transfer efficiently, or when you want to
91
+ process them later with other tools.
92
+
93
+ ```python
94
+ from wherobots.db import connect, Store, StorageFormat
95
+ from wherobots.db.region import Region
96
+ from wherobots.db.runtime import Runtime
97
+
98
+ with connect(
99
+ api_key='...',
100
+ runtime=Runtime.TINY,
101
+ region=Region.AWS_US_WEST_2) as conn:
102
+ curr = conn.cursor()
103
+
104
+ # Store results with a presigned URL for easy download
105
+ curr.execute(
106
+ "SELECT * FROM wherobots_open_data.overture.places LIMIT 1000",
107
+ store=Store.for_download()
108
+ )
109
+ store_result = curr.get_store_result()
110
+ print(f"Results stored at: {store_result.result_uri}")
111
+ print(f"Size: {store_result.size} bytes")
112
+ ```
113
+
114
+ The `Store` class supports the following options:
115
+
116
+ * `format`: output format - `StorageFormat.PARQUET` (default),
117
+ `StorageFormat.CSV`, or `StorageFormat.GEOJSON`
118
+ * `single`: if `True`, write results to a single file instead of
119
+ multiple partitioned files (default: `True`)
120
+ * `generate_presigned_url`: if `True`, generate a presigned URL for
121
+ downloading results (default: `False`)
122
+
123
+ Use `Store.for_download()` as a convenient shorthand for storing results
124
+ as a single Parquet file with a presigned URL.
125
+
86
126
  ### Runtime and region selection
87
127
 
88
128
  You can chose the Wherobots runtime you want to use using the `runtime`
@@ -59,6 +59,46 @@ It also implements the `close()` method, as suggested by the PEP-2049
59
59
  specification, to support situations where the cursor is wrapped in a
60
60
  `contextmanager.closing()`.
61
61
 
62
+ ### Storing results in cloud storage
63
+
64
+ For large query results, you can store them directly in cloud storage
65
+ instead of retrieving them over the connection. This is useful when
66
+ results are too large to transfer efficiently, or when you want to
67
+ process them later with other tools.
68
+
69
+ ```python
70
+ from wherobots.db import connect, Store, StorageFormat
71
+ from wherobots.db.region import Region
72
+ from wherobots.db.runtime import Runtime
73
+
74
+ with connect(
75
+ api_key='...',
76
+ runtime=Runtime.TINY,
77
+ region=Region.AWS_US_WEST_2) as conn:
78
+ curr = conn.cursor()
79
+
80
+ # Store results with a presigned URL for easy download
81
+ curr.execute(
82
+ "SELECT * FROM wherobots_open_data.overture.places LIMIT 1000",
83
+ store=Store.for_download()
84
+ )
85
+ store_result = curr.get_store_result()
86
+ print(f"Results stored at: {store_result.result_uri}")
87
+ print(f"Size: {store_result.size} bytes")
88
+ ```
89
+
90
+ The `Store` class supports the following options:
91
+
92
+ * `format`: output format - `StorageFormat.PARQUET` (default),
93
+ `StorageFormat.CSV`, or `StorageFormat.GEOJSON`
94
+ * `single`: if `True`, write results to a single file instead of
95
+ multiple partitioned files (default: `True`)
96
+ * `generate_presigned_url`: if `True`, generate a presigned URL for
97
+ downloading results (default: `False`)
98
+
99
+ Use `Store.for_download()` as a convenient shorthand for storing results
100
+ as a single Parquet file with a presigned URL.
101
+
62
102
  ### Runtime and region selection
63
103
 
64
104
  You can chose the Wherobots runtime you want to use using the `runtime`
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "wherobots-python-dbapi"
3
- version = "0.22.0"
3
+ version = "0.23.0"
4
4
  description = "Python DB-API driver for Wherobots DB"
5
5
  authors = [{ name = "Maxime Petazzoni", email = "max@wherobots.com" }]
6
- requires-python = ">=3.8, <4"
6
+ requires-python = ">=3.10, <4"
7
7
  readme = "README.md"
8
8
  license = "Apache-2.0"
9
9
  dependencies = [
@@ -10,8 +10,10 @@ from .errors import (
10
10
  ProgrammingError,
11
11
  NotSupportedError,
12
12
  )
13
+ from .models import Store, StoreResult
13
14
  from .region import Region
14
15
  from .runtime import Runtime
16
+ from .types import StorageFormat
15
17
 
16
18
  __all__ = [
17
19
  "Connection",
@@ -27,4 +29,7 @@ __all__ = [
27
29
  "NotSupportedError",
28
30
  "Region",
29
31
  "Runtime",
32
+ "Store",
33
+ "StorageFormat",
34
+ "StoreResult",
30
35
  ]
@@ -4,7 +4,7 @@ import textwrap
4
4
  import threading
5
5
  import uuid
6
6
  from dataclasses import dataclass
7
- from typing import Any, Callable, Union, Dict
7
+ from typing import Any, Callable, Dict
8
8
 
9
9
  import pandas
10
10
  import pyarrow
@@ -13,8 +13,11 @@ import websockets.exceptions
13
13
  import websockets.protocol
14
14
  import websockets.sync.client
15
15
 
16
- from wherobots.db.constants import (
17
- DEFAULT_READ_TIMEOUT_SECONDS,
16
+ from .constants import DEFAULT_READ_TIMEOUT_SECONDS
17
+ from .cursor import Cursor
18
+ from .errors import NotSupportedError, OperationalError
19
+ from .models import ExecutionResult, Store, StoreResult
20
+ from .types import (
18
21
  RequestKind,
19
22
  EventKind,
20
23
  ExecutionState,
@@ -22,8 +25,6 @@ from wherobots.db.constants import (
22
25
  DataCompression,
23
26
  GeometryRepresentation,
24
27
  )
25
- from wherobots.db.cursor import Cursor
26
- from wherobots.db.errors import NotSupportedError, OperationalError
27
28
 
28
29
 
29
30
  @dataclass
@@ -32,6 +33,7 @@ class Query:
32
33
  execution_id: str
33
34
  state: ExecutionState
34
35
  handler: Callable[[Any], None]
36
+ store: Store | None = None
35
37
 
36
38
 
37
39
  class Connection:
@@ -53,9 +55,9 @@ class Connection:
53
55
  self,
54
56
  ws: websockets.sync.client.ClientConnection,
55
57
  read_timeout: float = DEFAULT_READ_TIMEOUT_SECONDS,
56
- results_format: Union[ResultsFormat, None] = None,
57
- data_compression: Union[DataCompression, None] = None,
58
- geometry_representation: Union[GeometryRepresentation, None] = None,
58
+ results_format: ResultsFormat | None = None,
59
+ data_compression: DataCompression | None = None,
60
+ geometry_representation: GeometryRepresentation | None = None,
59
61
  ):
60
62
  self.__ws = ws
61
63
  self.__read_timeout = read_timeout
@@ -132,8 +134,26 @@ class Connection:
132
134
 
133
135
  if query.state == ExecutionState.SUCCEEDED:
134
136
  # On a state_updated event telling us the query succeeded,
135
- # ask for results.
137
+ # check if results are stored in cloud storage or need to be fetched.
136
138
  if kind == EventKind.STATE_UPDATED:
139
+ result_uri = message.get("result_uri")
140
+ if result_uri:
141
+ # Results are stored in cloud storage
142
+ store_result = StoreResult(
143
+ result_uri=result_uri,
144
+ size=message.get("size"),
145
+ )
146
+ logging.info(
147
+ "Query %s results stored at: %s (size: %s)",
148
+ execution_id,
149
+ result_uri,
150
+ store_result.size,
151
+ )
152
+ query.state = ExecutionState.COMPLETED
153
+ query.handler(ExecutionResult(store_result=store_result))
154
+ return
155
+
156
+ # No store configured, request results normally
137
157
  self.__request_results(execution_id)
138
158
  return
139
159
 
@@ -144,13 +164,15 @@ class Connection:
144
164
  return
145
165
 
146
166
  query.state = ExecutionState.COMPLETED
147
- query.handler(self._handle_results(execution_id, results))
167
+ query.handler(
168
+ ExecutionResult(results=self._handle_results(execution_id, results))
169
+ )
148
170
  elif query.state == ExecutionState.CANCELLED:
149
171
  logging.info(
150
172
  "Query %s has been cancelled; returning empty results.",
151
173
  execution_id,
152
174
  )
153
- query.handler(pandas.DataFrame())
175
+ query.handler(ExecutionResult(results=pandas.DataFrame()))
154
176
  self.__queries.pop(execution_id)
155
177
  elif query.state == ExecutionState.FAILED:
156
178
  # Don't do anything here; the ERROR event is coming with more
@@ -159,7 +181,7 @@ class Connection:
159
181
  elif kind == EventKind.ERROR:
160
182
  query.state = ExecutionState.FAILED
161
183
  error = message.get("message")
162
- query.handler(OperationalError(error))
184
+ query.handler(ExecutionResult(error=OperationalError(error)))
163
185
  else:
164
186
  logging.warning("Received unknown %s event!", kind)
165
187
 
@@ -200,7 +222,12 @@ class Connection:
200
222
  raise ValueError("Unexpected frame type received")
201
223
  return message
202
224
 
203
- def __execute_sql(self, sql: str, handler: Callable[[Any], None]) -> str:
225
+ def __execute_sql(
226
+ self,
227
+ sql: str,
228
+ handler: Callable[[Any], None],
229
+ store: Store | None = None,
230
+ ) -> str:
204
231
  """Triggers the execution of the given SQL query."""
205
232
  execution_id = str(uuid.uuid4())
206
233
  request = {
@@ -209,11 +236,19 @@ class Connection:
209
236
  "statement": sql,
210
237
  }
211
238
 
239
+ if store:
240
+ request["store"] = {
241
+ "format": store.format.value,
242
+ "single": str(store.single).lower(),
243
+ "generate_presigned_url": str(store.generate_presigned_url).lower(),
244
+ }
245
+
212
246
  self.__queries[execution_id] = Query(
213
247
  sql=sql,
214
248
  execution_id=execution_id,
215
249
  state=ExecutionState.EXECUTION_REQUESTED,
216
250
  handler=handler,
251
+ store=store,
217
252
  )
218
253
 
219
254
  logging.info(
@@ -0,0 +1,22 @@
1
+ from packaging.version import Version
2
+
3
+ from .region import Region
4
+ from .runtime import Runtime
5
+ from .session_type import SessionType
6
+ from .types import StorageFormat
7
+
8
+
9
+ DEFAULT_ENDPOINT: str = "api.cloud.wherobots.com" # "api.cloud.wherobots.com"
10
+ STAGING_ENDPOINT: str = "api.staging.wherobots.com" # "api.staging.wherobots.com"
11
+
12
+ DEFAULT_RUNTIME: Runtime = Runtime.TINY
13
+ DEFAULT_REGION: Region = Region.AWS_US_WEST_2
14
+ DEFAULT_SESSION_TYPE: SessionType = SessionType.MULTI
15
+ DEFAULT_STORAGE_FORMAT: StorageFormat = StorageFormat.PARQUET
16
+ DEFAULT_READ_TIMEOUT_SECONDS: float = 0.25
17
+ DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS: float = 900
18
+
19
+ MAX_MESSAGE_SIZE: int = 100 * 2**20 # 100MiB
20
+ PROTOCOL_VERSION: Version = Version("1.0.0")
21
+
22
+ PARAM_STYLE = "pyformat"
@@ -1,7 +1,8 @@
1
1
  import queue
2
- from typing import Any, Optional, List, Tuple, Dict
2
+ from typing import Any, List, Tuple, Dict
3
3
 
4
- from .errors import DatabaseError, ProgrammingError
4
+ from .errors import ProgrammingError
5
+ from .models import ExecutionResult, Store, StoreResult
5
6
 
6
7
  _TYPE_MAP = {
7
8
  "object": "STRING",
@@ -20,20 +21,21 @@ class Cursor:
20
21
  self.__cancel_fn = cancel_fn
21
22
 
22
23
  self.__queue: queue.Queue = queue.Queue()
23
- self.__results: Optional[list[Any]] = None
24
- self.__current_execution_id: Optional[str] = None
24
+ self.__results: list[Any] | None = None
25
+ self.__store_result: StoreResult | None = None
26
+ self.__current_execution_id: str | None = None
25
27
  self.__current_row: int = 0
26
28
 
27
29
  # Description and row count are set by the last executed operation.
28
30
  # Their default values are defined by PEP-0249.
29
- self.__description: Optional[List[Tuple]] = None
31
+ self.__description: List[Tuple] | None = None
30
32
  self.__rowcount: int = -1
31
33
 
32
34
  # Array-size is also defined by PEP-0249 and is expected to be read/writable.
33
35
  self.arraysize: int = 1
34
36
 
35
37
  @property
36
- def description(self) -> Optional[List[Tuple]]:
38
+ def description(self) -> List[Tuple] | None:
37
39
  return self.__description
38
40
 
39
41
  @property
@@ -43,47 +45,78 @@ class Cursor:
43
45
  def __on_execution_result(self, result) -> None:
44
46
  self.__queue.put(result)
45
47
 
46
- def __get_results(self) -> Optional[List[Tuple[Any, ...]]]:
48
+ def __get_results(self) -> List[Tuple[Any, ...]] | None:
47
49
  if not self.__current_execution_id:
48
50
  raise ProgrammingError("No query has been executed yet")
49
51
  if self.__results is not None:
50
52
  return self.__results
51
53
 
52
- result = self.__queue.get()
53
- if isinstance(result, DatabaseError):
54
- raise result
54
+ execution_result = self.__queue.get()
55
+ if not isinstance(execution_result, ExecutionResult):
56
+ raise ProgrammingError("Unexpected result type")
55
57
 
56
- self.__rowcount = len(result)
57
- self.__results = result
58
- if not result.empty:
58
+ if execution_result.error:
59
+ raise execution_result.error
60
+
61
+ self.__store_result = execution_result.store_result
62
+ results = execution_result.results
63
+
64
+ # Results is None when results are stored in cloud storage
65
+ if results is None:
66
+ return None
67
+
68
+ self.__rowcount = len(results)
69
+ self.__results = results
70
+ if not results.empty:
59
71
  self.__description = [
60
72
  (
61
73
  col_name, # name
62
- _TYPE_MAP.get(str(result[col_name].dtype), "STRING"), # type_code
74
+ _TYPE_MAP.get(str(results[col_name].dtype), "STRING"), # type_code
63
75
  None, # display_size
64
- result[col_name].memory_usage(), # internal_size
76
+ results[col_name].memory_usage(), # internal_size
65
77
  None, # precision
66
78
  None, # scale
67
79
  True, # null_ok; Assuming all columns can accept NULL values
68
80
  )
69
- for col_name in result.columns
81
+ for col_name in results.columns
70
82
  ]
71
83
 
72
84
  return self.__results
73
85
 
74
- def execute(self, operation: str, parameters: Dict[str, Any] = None) -> None:
86
+ def execute(
87
+ self,
88
+ operation: str,
89
+ parameters: Dict[str, Any] | None = None,
90
+ store: Store | None = None,
91
+ ) -> None:
75
92
  if self.__current_execution_id:
76
93
  self.__cancel_fn(self.__current_execution_id)
77
94
 
78
95
  self.__results = None
96
+ self.__store_result = None
79
97
  self.__current_row = 0
80
98
  self.__rowcount = -1
81
99
  self.__description = None
82
100
 
83
101
  self.__current_execution_id = self.__exec_fn(
84
- operation % (parameters or {}), self.__on_execution_result
102
+ operation % (parameters or {}), self.__on_execution_result, store
85
103
  )
86
104
 
105
+ def get_store_result(self) -> StoreResult | None:
106
+ """Get the store result for the last executed query.
107
+
108
+ Returns the StoreResult containing the URI and size of the stored
109
+ results, or None if the query was not configured to store results.
110
+
111
+ This method blocks until the query completes.
112
+ """
113
+ if not self.__current_execution_id:
114
+ raise ProgrammingError("No query has been executed yet")
115
+
116
+ # Ensure we've waited for the result
117
+ self.__get_results()
118
+ return self.__store_result
119
+
87
120
  def executemany(
88
121
  self, operation: str, seq_of_parameters: List[Dict[str, Any]]
89
122
  ) -> None:
@@ -27,11 +27,6 @@ from .constants import (
27
27
  MAX_MESSAGE_SIZE,
28
28
  PARAM_STYLE,
29
29
  PROTOCOL_VERSION,
30
- AppStatus,
31
- DataCompression,
32
- GeometryRepresentation,
33
- ResultsFormat,
34
- SessionType,
35
30
  )
36
31
  from .errors import (
37
32
  InterfaceError,
@@ -39,6 +34,13 @@ from .errors import (
39
34
  )
40
35
  from .region import Region
41
36
  from .runtime import Runtime
37
+ from .session_type import SessionType
38
+ from .types import (
39
+ AppStatus,
40
+ DataCompression,
41
+ GeometryRepresentation,
42
+ ResultsFormat,
43
+ )
42
44
 
43
45
  apilevel = "2.0"
44
46
  threadsafety = 1
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+
3
+ import pandas
4
+
5
+ from .constants import DEFAULT_STORAGE_FORMAT
6
+ from .types import StorageFormat
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class StoreResult:
11
+ """Result information when a query's results are stored to cloud storage.
12
+
13
+ Attributes:
14
+ result_uri: The URI or presigned URL of the stored result.
15
+ size: The size of the stored result in bytes, or None if not available.
16
+ """
17
+
18
+ result_uri: str
19
+ size: int | None = None
20
+
21
+
22
+ @dataclass
23
+ class Store:
24
+ """Configuration for storing query results to cloud storage.
25
+
26
+ When passed to cursor.execute(), query results will be written to cloud
27
+ storage instead of being returned directly over the WebSocket connection.
28
+
29
+ Attributes:
30
+ format: The storage format (parquet, csv, or geojson). Defaults to parquet.
31
+ single: If True, store as a single file. If False, store as multiple files.
32
+ generate_presigned_url: If True, generate a presigned URL for the result.
33
+ Requires single=True.
34
+ """
35
+
36
+ format: StorageFormat
37
+ single: bool = False
38
+ generate_presigned_url: bool = False
39
+
40
+ def __post_init__(self) -> None:
41
+ if self.generate_presigned_url and not self.single:
42
+ raise ValueError("Presigned URL can only be generated when single=True")
43
+
44
+ @classmethod
45
+ def for_download(cls, format: StorageFormat | None = None) -> "Store":
46
+ """Create a configuration for downloading results via a presigned URL.
47
+
48
+ This is a convenience method that creates a configuration with
49
+ single file mode and presigned URL generation enabled.
50
+
51
+ Args:
52
+ format: The storage format.
53
+
54
+ Returns:
55
+ A Store configured for single-file download with presigned URL.
56
+ """
57
+ return cls(
58
+ format=format or DEFAULT_STORAGE_FORMAT,
59
+ single=True,
60
+ generate_presigned_url=True,
61
+ )
62
+
63
+
64
+ @dataclass
65
+ class ExecutionResult:
66
+ """Result of a query execution.
67
+
68
+ This class encapsulates all possible outcomes of a query execution:
69
+ a DataFrame result, an error, or a store result (when results are
70
+ written to cloud storage).
71
+
72
+ Attributes:
73
+ results: The query results as a pandas DataFrame, or None if an error occurred.
74
+ error: The error that occurred during execution, or None if successful.
75
+ store_result: The store result if results were written to cloud storage.
76
+ """
77
+
78
+ results: pandas.DataFrame | None = None
79
+ error: Exception | None = None
80
+ store_result: StoreResult | None = None
@@ -1,26 +1,6 @@
1
1
  from enum import auto
2
- from packaging.version import Version
3
2
  from strenum import LowercaseStrEnum, StrEnum
4
3
 
5
- from .region import Region
6
- from .runtime import Runtime
7
- from .session_type import SessionType
8
-
9
-
10
- DEFAULT_ENDPOINT: str = "api.cloud.wherobots.com" # "api.cloud.wherobots.com"
11
- STAGING_ENDPOINT: str = "api.staging.wherobots.com" # "api.staging.wherobots.com"
12
-
13
- DEFAULT_RUNTIME: Runtime = Runtime.TINY
14
- DEFAULT_REGION: Region = Region.AWS_US_WEST_2
15
- DEFAULT_SESSION_TYPE: SessionType = SessionType.MULTI
16
- DEFAULT_READ_TIMEOUT_SECONDS: float = 0.25
17
- DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS: float = 900
18
-
19
- MAX_MESSAGE_SIZE: int = 100 * 2**20 # 100MiB
20
- PROTOCOL_VERSION: Version = Version("1.0.0")
21
-
22
- PARAM_STYLE = "pyformat"
23
-
24
4
 
25
5
  class ExecutionState(LowercaseStrEnum):
26
6
  IDLE = auto()
@@ -84,6 +64,12 @@ class GeometryRepresentation(LowercaseStrEnum):
84
64
  GEOJSON = auto()
85
65
 
86
66
 
67
+ class StorageFormat(LowercaseStrEnum):
68
+ PARQUET = auto()
69
+ CSV = auto()
70
+ GEOJSON = auto()
71
+
72
+
87
73
  class AppStatus(StrEnum):
88
74
  PENDING = auto()
89
75
  PREPARING = auto()