pygeobox 1.0.2__py3-none-any.whl → 1.0.4__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.
pygeobox/base.py CHANGED
@@ -1,364 +1,364 @@
1
- from typing import Any, List, Dict, Callable, TYPE_CHECKING, Union
2
- from urllib.parse import urljoin, urlencode
3
- from datetime import datetime
4
-
5
- from .utils import clean_data
6
-
7
- if TYPE_CHECKING:
8
- from .user import User
9
- from .task import Task
10
- from . import GeoboxClient
11
-
12
-
13
- class Base:
14
- BASE_ENDPOINT = ''
15
-
16
- def __init__(self, api, **kwargs):
17
- """
18
- Initialize the Base class.
19
-
20
- Args:
21
- api (GeoboxClient): The API client
22
- uuid (str, optional): The UUID of the resource
23
- data (dict, optional): The data of the resource
24
- """
25
- self.api = api
26
- self.uuid = kwargs.get('uuid')
27
- self.data = kwargs.get('data')
28
- self.endpoint = urljoin(self.BASE_ENDPOINT, f'{self.uuid}/') if self.uuid else None
29
-
30
-
31
- def __dir__(self) -> List[str]:
32
- """
33
- Return a list of available attributes for the Feature object.
34
-
35
- This method extends the default dir() behavior to include:
36
- - All keys from the data dictionary
37
-
38
- This allows for better IDE autocompletion and introspection of feature attributes.
39
-
40
- Returns:
41
- list: A list of attribute names available on this object.
42
- """
43
- return super().__dir__() + list(self.data.keys())
44
-
45
-
46
- def __getattr__(self, name: str) -> Any:
47
- """
48
- Get an attribute from the resource.
49
-
50
- Args:
51
- name (str): The name of the attribute
52
- """
53
- if name in self.data:
54
- value = self.data.get(name)
55
- if isinstance(value, str):
56
- parsed = self._parse_datetime(value)
57
- if isinstance(parsed, datetime):
58
- return parsed
59
- return value
60
- raise AttributeError(f"{self.__class__.__name__} has no attribute {name}")
61
-
62
-
63
- def __repr__(self) -> str:
64
- """
65
- Return a string representation of the resource.
66
- """
67
- return f"{self.__class__.__name__}(name={self.name})"
68
-
69
-
70
- def _update_properties(self, data: dict) -> None:
71
- """
72
- Update the properties of the resource.
73
-
74
- Args:
75
- data (dict): The data to update the properties with
76
- """
77
- self.data.update(data)
78
-
79
-
80
- @classmethod
81
- def _handle_geojson_response(cls, api: 'GeoboxClient', response: dict, factory_func: Callable) -> List['Base']:
82
- """Handle GeoJSON response format"""
83
- features_data = response.get('features', [])
84
- srid = cls._extract_srid(response)
85
- return [factory_func(api, feature, srid) for feature in features_data]
86
-
87
-
88
- @classmethod
89
- def _extract_srid(cls, response: dict) -> int:
90
- """Extract SRID from GeoJSON response"""
91
- if isinstance(response, dict) and 'crs' in response:
92
- return int(response['crs']['properties']['name'].split(':')[-1])
93
-
94
-
95
- @classmethod
96
- def _get_count(cls, response: dict) -> int:
97
- """Get the count of resources"""
98
- if isinstance(response, dict) and 'count' in response:
99
- return response.get('count', 0)
100
- elif isinstance(response, int):
101
- return response
102
- else:
103
- raise ValueError('Invalid response format')
104
-
105
-
106
- @classmethod
107
- def _get_list(cls, api: 'GeoboxClient', endpoint: str, params: dict = {},
108
- factory_func: Callable = None, geojson: bool = False) -> Union[List['Base'], int]:
109
- """Get a list of resources with optional filtering and pagination"""
110
- query_string = urlencode(clean_data(params)) if params else ''
111
- endpoint = urljoin(endpoint, f'?{query_string}')
112
- response = api.get(endpoint)
113
-
114
- if params.get('return_count'):
115
- return cls._get_count(response)
116
-
117
- if not response:
118
- return []
119
-
120
- if geojson:
121
- return cls._handle_geojson_response(api, response, factory_func)
122
-
123
- return [factory_func(api, item) for item in response]
124
-
125
-
126
- @classmethod
127
- def _get_list_by_ids(cls, api: 'GeoboxClient', endpoint: str, params: dict = None, factory_func: Callable = None) -> List['Base']:
128
- """
129
- Internal method to get a list of resources by their IDs.
130
-
131
- Args:
132
- api (GeoboxClient): The API client
133
- endpoint (str): The endpoint of the resource
134
- params (dict): Additional parameters for filtering and pagination
135
- factory_func (Callable): A function to create the resource object
136
-
137
- Returns:
138
- List[Base]: The list of resource objects
139
- """
140
- params = clean_data(params)
141
- query_string = urlencode(params)
142
- endpoint = urljoin(endpoint, f'?{query_string}')
143
- response = api.get(endpoint)
144
- return [factory_func(api, item) for item in response]
145
-
146
-
147
- @classmethod
148
- def _get_detail(cls, api: 'GeoboxClient', endpoint: str, uuid: str, params: dict = {}, factory_func: Callable = None) -> 'Base':
149
- """
150
- Internal method to get a single resource by UUID.
151
-
152
- Args:
153
- api (GeoboxClient): The API client
154
- uuid (str): The UUID of the resource
155
- params (dict): Additional parameters for filtering and pagination
156
- factory_func (Callable): A function to create the resource object
157
-
158
- Returns:
159
- Base: The resource object
160
- """
161
- query_strings = urlencode(clean_data(params))
162
- endpoint = urljoin(endpoint, f'{uuid}/?{query_strings}')
163
- response = api.get(endpoint)
164
- return factory_func(api, response)
165
-
166
-
167
- @classmethod
168
- def _create(cls, api: 'GeoboxClient', endpoint: str, data: dict, factory_func: Callable = None) -> 'Base':
169
- """
170
- Internal method to create a resource.
171
-
172
- Args:
173
- api (GeoboxClient): The API client
174
- data (dict): The data to create the resource with
175
- factory_func (Callable): A function to create the resource object
176
-
177
- Returns:
178
- Base: The created resource object
179
- """
180
- data = clean_data(data)
181
- response = api.post(endpoint, data)
182
- return factory_func(api, response)
183
-
184
-
185
- def _update(self, endpoint: str, data: dict, clean: bool = True) -> Dict:
186
- """
187
- Update the resource.
188
-
189
- Args:
190
- data (dict): The data to update the resource with
191
- """
192
- if clean:
193
- data = clean_data(data)
194
-
195
- response = self.api.put(endpoint, data)
196
- self._update_properties(response)
197
- return response
198
-
199
-
200
- def delete(self, endpoint: str) -> None:
201
- """
202
- Delete the resource.
203
- """
204
- self.api.delete(endpoint)
205
- self.uuid = None
206
- self.endpoint = None
207
-
208
-
209
- def _share(self, endpoint: str, users: List['User']) -> None:
210
- """
211
- Internal method to share the resource with the given user IDs.
212
-
213
- Args:
214
- users (List[User]): The user objects to share the resource with
215
- """
216
- data = {"user_ids": [user.user_id for user in users]}
217
- endpoint = urljoin(endpoint, f'share/')
218
- self.api.post(endpoint, data, is_json=False)
219
-
220
-
221
- def _unshare(self, endpoint: str, users: List['User']) -> None:
222
- """
223
- Internal method to unshare the resource with the given user IDs.
224
-
225
- Args:
226
- users (List[User]): The user objects to unshare the resource with
227
- """
228
- data = {"user_ids": [user.user_id for user in users]}
229
- endpoint = urljoin(endpoint, f'unshare/')
230
- self.api.post(endpoint, data, is_json=False)
231
-
232
-
233
- def _get_shared_users(self, endpoint: str, params: dict = None) -> List['User']:
234
- """
235
- Internal method to get the users that the resource is shared with.
236
-
237
- Args:
238
- endpoint (str): resource endpoint
239
- params (dict): Additional parameters for filtering and pagination
240
-
241
- Returns:
242
- List[User]: The users that the resource is shared with
243
- """
244
- from .user import User
245
-
246
- params = clean_data(params)
247
- query_strings = urlencode(params)
248
- endpoint = urljoin(endpoint, f'shared-with-users/?{query_strings}')
249
- response = self.api.get(endpoint)
250
- return [User(self.api, item['id'], item) for item in response]
251
-
252
-
253
- def _get_settings(self, endpoint: str) -> Dict:
254
- """
255
- Internal method to get the settings of the resource.
256
-
257
- Args:
258
- endpoint (str): The endpoint of the resource
259
- """
260
- endpoint = urljoin(endpoint, f'settings/?f=json')
261
- return self.api.get(endpoint)
262
-
263
-
264
- def _set_settings(self, endpoint: str, data: dict) -> None:
265
- """
266
- Internal method to set the settings of the resource.
267
-
268
- Args:
269
- endpoint (str): The endpoint of the resource
270
- data (dict): The data to set the settings with
271
- """
272
- endpoint = urljoin(endpoint, f'settings/')
273
- return self.api.put(endpoint, data)
274
-
275
-
276
- def _get_task(self, response, error_message: str) -> List['Task']:
277
- from .task import Task # avoid circular dependency
278
-
279
- if len(response) == 1 and isinstance(response, list) and response[0].get('task_id'):
280
- result = [self.api.get_task(response[0].get('task_id'))]
281
- elif len(response) == 2 and isinstance(response, list) and (response[0].get('task_id') and response[1].get('task_id')):
282
- result = [self.api.get_task(item.get('task_id')) for item in response]
283
- elif len(response) == 1 and isinstance(response, dict) and response.get('task_id'):
284
- result = [self.api.get_task(response.get('task_id'))]
285
- else:
286
- raise ValueError(error_message)
287
-
288
- return result
289
-
290
-
291
- def _seed_cache(self, endpoint: str, data: dict) -> List['Task']:
292
- """
293
- Internal method to cache seed the resource.
294
-
295
- Args:
296
- endpoint (str): The endpoint of the resource
297
- data (dict): The data to cache seed with
298
- """
299
- if data['workers'] not in [1, 2, 4, 8, 12, 16, 20, 24]:
300
- raise ValueError("workers must be in [1, 2, 4, 8, 12, 16, 20, 24]")
301
-
302
- data = clean_data(data)
303
- endpoint = urljoin(endpoint, f'cache/seed/')
304
- response = self.api.post(endpoint, data)
305
- return self._get_task(response, 'Failed to seed cache')
306
-
307
- def _clear_cache(self, endpoint: str) -> None:
308
- """
309
- Internal method to clear the cache of the resource.
310
-
311
- Args:
312
- endpoint (str): The endpoint of the resource
313
- """
314
- endpoint = urljoin(endpoint, f'cache/clear/')
315
- self.api.post(endpoint)
316
-
317
-
318
- def _cache_size(self, endpoint: str) -> int:
319
- """
320
- Internal method to get the size of the cache of the resource.
321
-
322
- Args:
323
- endpoint (str): The endpoint of the resource
324
- """
325
- endpoint = urljoin(endpoint, f'cache/size/')
326
- return self.api.post(endpoint)
327
-
328
-
329
- def _update_cache(self, endpoint: str, data: Dict = {}) -> List['Task']:
330
- """
331
- Internal method to update the cache of the resource.
332
-
333
- Args:
334
- endpoint (str): The endpoint of the resource
335
- """
336
- data = clean_data(data)
337
- endpoint = urljoin(endpoint, 'cache/update/')
338
- response = self.api.post(endpoint, data)
339
- return self._get_task(response, 'Failed to update cache')
340
-
341
- def _parse_datetime(self, date_string: str) -> Union[datetime, str]:
342
- """
343
- Parse a datetime string with multiple format support.
344
-
345
- Args:
346
- date_string (str): The datetime string to parse
347
-
348
- Returns:
349
- Union[datetime, str]: Parsed datetime object or original string if parsing fails
350
- """
351
- formats = [
352
- "%Y-%m-%dT%H:%M:%S.%f", # With microseconds
353
- "%Y-%m-%dT%H:%M:%SZ", # Without microseconds, with timezone
354
- "%Y-%m-%dT%H:%M:%S" # Without microseconds, without timezone
355
- ]
356
-
357
- for fmt in formats:
358
- try:
359
- return datetime.strptime(date_string, fmt)
360
- except ValueError:
361
- continue
362
-
363
- # If all parsing fails, return the original string
364
- return date_string
1
+ from typing import Any, List, Dict, Callable, TYPE_CHECKING, Union
2
+ from urllib.parse import urljoin, urlencode
3
+ from datetime import datetime
4
+
5
+ from .utils import clean_data
6
+
7
+ if TYPE_CHECKING:
8
+ from .user import User
9
+ from .task import Task
10
+ from . import GeoboxClient
11
+
12
+
13
+ class Base:
14
+ BASE_ENDPOINT = ''
15
+
16
+ def __init__(self, api, **kwargs):
17
+ """
18
+ Initialize the Base class.
19
+
20
+ Args:
21
+ api (GeoboxClient): The API client
22
+ uuid (str, optional): The UUID of the resource
23
+ data (dict, optional): The data of the resource
24
+ """
25
+ self.api = api
26
+ self.uuid = kwargs.get('uuid')
27
+ self.data = kwargs.get('data')
28
+ self.endpoint = urljoin(self.BASE_ENDPOINT, f'{self.uuid}/') if self.uuid else None
29
+
30
+
31
+ def __dir__(self) -> List[str]:
32
+ """
33
+ Return a list of available attributes for the Feature object.
34
+
35
+ This method extends the default dir() behavior to include:
36
+ - All keys from the data dictionary
37
+
38
+ This allows for better IDE autocompletion and introspection of feature attributes.
39
+
40
+ Returns:
41
+ list: A list of attribute names available on this object.
42
+ """
43
+ return super().__dir__() + list(self.data.keys())
44
+
45
+
46
+ def __getattr__(self, name: str) -> Any:
47
+ """
48
+ Get an attribute from the resource.
49
+
50
+ Args:
51
+ name (str): The name of the attribute
52
+ """
53
+ if name in self.data:
54
+ value = self.data.get(name)
55
+ if isinstance(value, str):
56
+ parsed = self._parse_datetime(value)
57
+ if isinstance(parsed, datetime):
58
+ return parsed
59
+ return value
60
+ raise AttributeError(f"{self.__class__.__name__} has no attribute {name}")
61
+
62
+
63
+ def __repr__(self) -> str:
64
+ """
65
+ Return a string representation of the resource.
66
+ """
67
+ return f"{self.__class__.__name__}(name={self.name})"
68
+
69
+
70
+ def _update_properties(self, data: dict) -> None:
71
+ """
72
+ Update the properties of the resource.
73
+
74
+ Args:
75
+ data (dict): The data to update the properties with
76
+ """
77
+ self.data.update(data)
78
+
79
+
80
+ @classmethod
81
+ def _handle_geojson_response(cls, api: 'GeoboxClient', response: dict, factory_func: Callable) -> List['Base']:
82
+ """Handle GeoJSON response format"""
83
+ features_data = response.get('features', [])
84
+ srid = cls._extract_srid(response)
85
+ return [factory_func(api, feature, srid) for feature in features_data]
86
+
87
+
88
+ @classmethod
89
+ def _extract_srid(cls, response: dict) -> int:
90
+ """Extract SRID from GeoJSON response"""
91
+ if isinstance(response, dict) and 'crs' in response:
92
+ return int(response['crs']['properties']['name'].split(':')[-1])
93
+
94
+
95
+ @classmethod
96
+ def _get_count(cls, response: dict) -> int:
97
+ """Get the count of resources"""
98
+ if isinstance(response, dict) and 'count' in response:
99
+ return response.get('count', 0)
100
+ elif isinstance(response, int):
101
+ return response
102
+ else:
103
+ raise ValueError('Invalid response format')
104
+
105
+
106
+ @classmethod
107
+ def _get_list(cls, api: 'GeoboxClient', endpoint: str, params: dict = {},
108
+ factory_func: Callable = None, geojson: bool = False) -> Union[List['Base'], int]:
109
+ """Get a list of resources with optional filtering and pagination"""
110
+ query_string = urlencode(clean_data(params)) if params else ''
111
+ endpoint = urljoin(endpoint, f'?{query_string}')
112
+ response = api.get(endpoint)
113
+
114
+ if params.get('return_count'):
115
+ return cls._get_count(response)
116
+
117
+ if not response:
118
+ return []
119
+
120
+ if geojson:
121
+ return cls._handle_geojson_response(api, response, factory_func)
122
+
123
+ return [factory_func(api, item) for item in response]
124
+
125
+
126
+ @classmethod
127
+ def _get_list_by_ids(cls, api: 'GeoboxClient', endpoint: str, params: dict = None, factory_func: Callable = None) -> List['Base']:
128
+ """
129
+ Internal method to get a list of resources by their IDs.
130
+
131
+ Args:
132
+ api (GeoboxClient): The API client
133
+ endpoint (str): The endpoint of the resource
134
+ params (dict): Additional parameters for filtering and pagination
135
+ factory_func (Callable): A function to create the resource object
136
+
137
+ Returns:
138
+ List[Base]: The list of resource objects
139
+ """
140
+ params = clean_data(params)
141
+ query_string = urlencode(params)
142
+ endpoint = urljoin(endpoint, f'?{query_string}')
143
+ response = api.get(endpoint)
144
+ return [factory_func(api, item) for item in response]
145
+
146
+
147
+ @classmethod
148
+ def _get_detail(cls, api: 'GeoboxClient', endpoint: str, uuid: str, params: dict = {}, factory_func: Callable = None) -> 'Base':
149
+ """
150
+ Internal method to get a single resource by UUID.
151
+
152
+ Args:
153
+ api (GeoboxClient): The API client
154
+ uuid (str): The UUID of the resource
155
+ params (dict): Additional parameters for filtering and pagination
156
+ factory_func (Callable): A function to create the resource object
157
+
158
+ Returns:
159
+ Base: The resource object
160
+ """
161
+ query_strings = urlencode(clean_data(params))
162
+ endpoint = urljoin(endpoint, f'{uuid}/?{query_strings}')
163
+ response = api.get(endpoint)
164
+ return factory_func(api, response)
165
+
166
+
167
+ @classmethod
168
+ def _create(cls, api: 'GeoboxClient', endpoint: str, data: dict, factory_func: Callable = None) -> 'Base':
169
+ """
170
+ Internal method to create a resource.
171
+
172
+ Args:
173
+ api (GeoboxClient): The API client
174
+ data (dict): The data to create the resource with
175
+ factory_func (Callable): A function to create the resource object
176
+
177
+ Returns:
178
+ Base: The created resource object
179
+ """
180
+ data = clean_data(data)
181
+ response = api.post(endpoint, data)
182
+ return factory_func(api, response)
183
+
184
+
185
+ def _update(self, endpoint: str, data: dict, clean: bool = True) -> Dict:
186
+ """
187
+ Update the resource.
188
+
189
+ Args:
190
+ data (dict): The data to update the resource with
191
+ """
192
+ if clean:
193
+ data = clean_data(data)
194
+
195
+ response = self.api.put(endpoint, data)
196
+ self._update_properties(response)
197
+ return response
198
+
199
+
200
+ def delete(self, endpoint: str) -> None:
201
+ """
202
+ Delete the resource.
203
+ """
204
+ self.api.delete(endpoint)
205
+ self.uuid = None
206
+ self.endpoint = None
207
+
208
+
209
+ def _share(self, endpoint: str, users: List['User']) -> None:
210
+ """
211
+ Internal method to share the resource with the given user IDs.
212
+
213
+ Args:
214
+ users (List[User]): The user objects to share the resource with
215
+ """
216
+ data = {"user_ids": [user.user_id for user in users]}
217
+ endpoint = urljoin(endpoint, f'share/')
218
+ self.api.post(endpoint, data, is_json=False)
219
+
220
+
221
+ def _unshare(self, endpoint: str, users: List['User']) -> None:
222
+ """
223
+ Internal method to unshare the resource with the given user IDs.
224
+
225
+ Args:
226
+ users (List[User]): The user objects to unshare the resource with
227
+ """
228
+ data = {"user_ids": [user.user_id for user in users]}
229
+ endpoint = urljoin(endpoint, f'unshare/')
230
+ self.api.post(endpoint, data, is_json=False)
231
+
232
+
233
+ def _get_shared_users(self, endpoint: str, params: dict = None) -> List['User']:
234
+ """
235
+ Internal method to get the users that the resource is shared with.
236
+
237
+ Args:
238
+ endpoint (str): resource endpoint
239
+ params (dict): Additional parameters for filtering and pagination
240
+
241
+ Returns:
242
+ List[User]: The users that the resource is shared with
243
+ """
244
+ from .user import User
245
+
246
+ params = clean_data(params)
247
+ query_strings = urlencode(params)
248
+ endpoint = urljoin(endpoint, f'shared-with-users/?{query_strings}')
249
+ response = self.api.get(endpoint)
250
+ return [User(self.api, item['id'], item) for item in response]
251
+
252
+
253
+ def _get_settings(self, endpoint: str) -> Dict:
254
+ """
255
+ Internal method to get the settings of the resource.
256
+
257
+ Args:
258
+ endpoint (str): The endpoint of the resource
259
+ """
260
+ endpoint = urljoin(endpoint, f'settings/?f=json')
261
+ return self.api.get(endpoint)
262
+
263
+
264
+ def _set_settings(self, endpoint: str, data: dict) -> None:
265
+ """
266
+ Internal method to set the settings of the resource.
267
+
268
+ Args:
269
+ endpoint (str): The endpoint of the resource
270
+ data (dict): The data to set the settings with
271
+ """
272
+ endpoint = urljoin(endpoint, f'settings/')
273
+ return self.api.put(endpoint, data)
274
+
275
+
276
+ def _get_task(self, response, error_message: str) -> List['Task']:
277
+ from .task import Task # avoid circular dependency
278
+
279
+ if len(response) == 1 and isinstance(response, list) and response[0].get('task_id'):
280
+ result = [self.api.get_task(response[0].get('task_id'))]
281
+ elif len(response) == 2 and isinstance(response, list) and (response[0].get('task_id') and response[1].get('task_id')):
282
+ result = [self.api.get_task(item.get('task_id')) for item in response]
283
+ elif len(response) == 1 and isinstance(response, dict) and response.get('task_id'):
284
+ result = [self.api.get_task(response.get('task_id'))]
285
+ else:
286
+ raise ValueError(error_message)
287
+
288
+ return result
289
+
290
+
291
+ def _seed_cache(self, endpoint: str, data: dict) -> List['Task']:
292
+ """
293
+ Internal method to cache seed the resource.
294
+
295
+ Args:
296
+ endpoint (str): The endpoint of the resource
297
+ data (dict): The data to cache seed with
298
+ """
299
+ if data['workers'] not in [1, 2, 4, 8, 12, 16, 20, 24]:
300
+ raise ValueError("workers must be in [1, 2, 4, 8, 12, 16, 20, 24]")
301
+
302
+ data = clean_data(data)
303
+ endpoint = urljoin(endpoint, f'cache/seed/')
304
+ response = self.api.post(endpoint, data)
305
+ return self._get_task(response, 'Failed to seed cache')
306
+
307
+ def _clear_cache(self, endpoint: str) -> None:
308
+ """
309
+ Internal method to clear the cache of the resource.
310
+
311
+ Args:
312
+ endpoint (str): The endpoint of the resource
313
+ """
314
+ endpoint = urljoin(endpoint, f'cache/clear/')
315
+ self.api.post(endpoint)
316
+
317
+
318
+ def _cache_size(self, endpoint: str) -> int:
319
+ """
320
+ Internal method to get the size of the cache of the resource.
321
+
322
+ Args:
323
+ endpoint (str): The endpoint of the resource
324
+ """
325
+ endpoint = urljoin(endpoint, f'cache/size/')
326
+ return self.api.post(endpoint)
327
+
328
+
329
+ def _update_cache(self, endpoint: str, data: Dict = {}) -> List['Task']:
330
+ """
331
+ Internal method to update the cache of the resource.
332
+
333
+ Args:
334
+ endpoint (str): The endpoint of the resource
335
+ """
336
+ data = clean_data(data)
337
+ endpoint = urljoin(endpoint, 'cache/update/')
338
+ response = self.api.post(endpoint, data)
339
+ return self._get_task(response, 'Failed to update cache')
340
+
341
+ def _parse_datetime(self, date_string: str) -> Union[datetime, str]:
342
+ """
343
+ Parse a datetime string with multiple format support.
344
+
345
+ Args:
346
+ date_string (str): The datetime string to parse
347
+
348
+ Returns:
349
+ Union[datetime, str]: Parsed datetime object or original string if parsing fails
350
+ """
351
+ formats = [
352
+ "%Y-%m-%dT%H:%M:%S.%f", # With microseconds
353
+ "%Y-%m-%dT%H:%M:%SZ", # Without microseconds, with timezone
354
+ "%Y-%m-%dT%H:%M:%S" # Without microseconds, without timezone
355
+ ]
356
+
357
+ for fmt in formats:
358
+ try:
359
+ return datetime.strptime(date_string, fmt)
360
+ except ValueError:
361
+ continue
362
+
363
+ # If all parsing fails, return the original string
364
+ return date_string