eodag 3.0.1__py3-none-any.whl → 3.1.0b2__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 (87) hide show
  1. eodag/api/core.py +164 -127
  2. eodag/api/product/_assets.py +11 -11
  3. eodag/api/product/_product.py +45 -30
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +101 -85
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +78 -81
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +46 -22
  15. eodag/plugins/apis/usgs.py +16 -15
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +16 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +58 -78
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +87 -44
  40. eodag/plugins/search/build_search_result.py +1067 -329
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +16 -15
  45. eodag/plugins/search/qssearch.py +103 -187
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +3 -3
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +663 -304
  50. eodag/resources/providers.yml +823 -1749
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +11 -0
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +40 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +15 -16
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +75 -28
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +152 -50
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +208 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/METADATA +77 -76
  81. eodag-3.1.0b2.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/top_level.txt +0 -0
@@ -23,17 +23,7 @@ import logging
23
23
  import os
24
24
  from io import BufferedReader
25
25
  from shutil import make_archive, rmtree
26
- from typing import (
27
- TYPE_CHECKING,
28
- Any,
29
- Callable,
30
- Dict,
31
- Iterator,
32
- List,
33
- NamedTuple,
34
- Optional,
35
- Union,
36
- )
26
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, NamedTuple, Optional, Union
37
27
  from urllib.parse import unquote_plus, urlencode
38
28
 
39
29
  import orjson
@@ -55,12 +45,15 @@ __all__ = ["get_date", "get_datetime"]
55
45
 
56
46
  logger = logging.getLogger("eodag.rest.utils")
57
47
 
48
+ # Path of the liveness endpoint
49
+ LIVENESS_PROBE_PATH = "/_mgmt/ping"
50
+
58
51
 
59
52
  class Cruncher(NamedTuple):
60
53
  """Type hinted Cruncher namedTuple"""
61
54
 
62
55
  clazz: Callable[..., Any]
63
- config_params: List[str]
56
+ config_params: list[str]
64
57
 
65
58
 
66
59
  crunchers = {
@@ -87,19 +80,19 @@ def format_pydantic_error(e: pydanticValidationError) -> str:
87
80
 
88
81
  def is_dict_str_any(var: Any) -> bool:
89
82
  """Verify whether the variable is of type dict[str, Any]"""
90
- if isinstance(var, Dict):
83
+ if isinstance(var, dict):
91
84
  return all(isinstance(k, str) for k in var.keys()) # type: ignore
92
85
  return False
93
86
 
94
87
 
95
- def str2list(v: Optional[str]) -> Optional[List[str]]:
88
+ def str2list(v: Optional[str]) -> Optional[list[str]]:
96
89
  """Convert string to list base on , delimiter."""
97
90
  if v:
98
91
  return v.split(",")
99
92
  return None
100
93
 
101
94
 
102
- def str2json(k: str, v: Optional[str] = None) -> Optional[Dict[str, Any]]:
95
+ def str2json(k: str, v: Optional[str] = None) -> Optional[dict[str, Any]]:
103
96
  """decoding a URL parameter and then parsing it as JSON."""
104
97
  if not v:
105
98
  return None
@@ -109,25 +102,25 @@ def str2json(k: str, v: Optional[str] = None) -> Optional[Dict[str, Any]]:
109
102
  raise ValidationError(f"{k}: Incorrect JSON object") from e
110
103
 
111
104
 
112
- def flatten_list(nested_list: Union[Any, List[Any]]) -> List[Any]:
105
+ def flatten_list(nested_list: Union[Any, list[Any]]) -> list[Any]:
113
106
  """Flatten a nested list structure into a single list."""
114
107
  if not isinstance(nested_list, list):
115
108
  return [nested_list]
116
109
  else:
117
- flattened: List[Any] = []
110
+ flattened: list[Any] = []
118
111
  for element in nested_list:
119
112
  flattened.extend(flatten_list(element))
120
113
  return flattened
121
114
 
122
115
 
123
- def list_to_str_list(input_list: List[Any]) -> List[str]:
116
+ def list_to_str_list(input_list: list[Any]) -> list[str]:
124
117
  """Attempt to convert a list of any type to a list of strings."""
125
118
  try:
126
119
  # Try to convert each element to a string
127
120
  return [str(element) for element in input_list]
128
121
  except Exception as e:
129
122
  # Raise an exception if any element cannot be converted
130
- raise TypeError(f"Failed to convert to List[str]: {e}") from e
123
+ raise TypeError(f"Failed to convert to list[str]: {e}") from e
131
124
 
132
125
 
133
126
  def get_next_link(
@@ -135,7 +128,7 @@ def get_next_link(
135
128
  search_request: SearchPostRequest,
136
129
  total_results: Optional[int],
137
130
  items_per_page: int,
138
- ) -> Optional[Dict[str, Any]]:
131
+ ) -> Optional[dict[str, Any]]:
139
132
  """Generate next link URL and body"""
140
133
  body = search_request.model_dump(exclude_none=True)
141
134
  if "bbox" in body:
@@ -156,7 +149,7 @@ def get_next_link(
156
149
  params["page"] = str(page + 1)
157
150
  url += f"?{urlencode(params)}"
158
151
 
159
- next: Dict[str, Any] = {
152
+ next: dict[str, Any] = {
160
153
  "rel": "next",
161
154
  "href": url,
162
155
  "title": "Next page",
@@ -16,13 +16,13 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  from datetime import datetime as dt
19
- from typing import Any, Dict, List, Optional, Tuple, Union
19
+ from typing import Any, Optional, Union
20
20
 
21
21
  from pygeofilter import ast
22
22
  from pygeofilter.backends.evaluator import Evaluator, handle
23
23
  from pygeofilter.values import Geometry, Interval
24
24
 
25
- simpleNode = Union[ast.Attribute, str, int, complex, float, List[Any], Tuple[Any, ...]]
25
+ simpleNode = Union[ast.Attribute, str, int, complex, float, list[Any], tuple[Any, ...]]
26
26
 
27
27
 
28
28
  class EodagEvaluator(Evaluator):
@@ -36,7 +36,7 @@ class EodagEvaluator(Evaluator):
36
36
  return node
37
37
 
38
38
  @handle(Geometry)
39
- def spatial(self, node: Geometry) -> Dict[str, Any]:
39
+ def spatial(self, node: Geometry) -> dict[str, Any]:
40
40
  """handle geometry"""
41
41
  return node.geometry
42
42
 
@@ -46,7 +46,7 @@ class EodagEvaluator(Evaluator):
46
46
  return node.strftime("%Y-%m-%dT%H:%M:%SZ")
47
47
 
48
48
  @handle(Interval)
49
- def interval(self, _, *interval: Any) -> List[Any]:
49
+ def interval(self, _, *interval: Any) -> list[Any]:
50
50
  """handle datetime interval"""
51
51
  return list(interval)
52
52
 
@@ -60,7 +60,7 @@ class EodagEvaluator(Evaluator):
60
60
  )
61
61
  def predicate(
62
62
  self, node: ast.Predicate, lhs: Any, rhs: Any
63
- ) -> Optional[Dict[str, Any]]:
63
+ ) -> Optional[dict[str, Any]]:
64
64
  """
65
65
  Handle predicates
66
66
  Verify the property is first attribute in each predicate
@@ -114,6 +114,6 @@ class EodagEvaluator(Evaluator):
114
114
  return {lhs.name: list(rhs)}
115
115
 
116
116
  @handle(ast.And)
117
- def combination(self, _, lhs: Dict[str, str], rhs: Dict[str, str]):
117
+ def combination(self, _, lhs: dict[str, str], rhs: dict[str, str]):
118
118
  """handle combinations"""
119
119
  return {**lhs, **rhs}
@@ -16,14 +16,14 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  import datetime
19
- from typing import Optional, Tuple
19
+ from typing import Optional
20
20
 
21
21
  from eodag.utils.rest import rfc3339_str_to_datetime
22
22
 
23
23
 
24
24
  def str_to_interval(
25
25
  interval: Optional[str],
26
- ) -> Tuple[Optional[datetime.datetime], Optional[datetime.datetime]]:
26
+ ) -> tuple[Optional[datetime.datetime], Optional[datetime.datetime]]:
27
27
  """Extract a tuple of datetimes from an interval string.
28
28
 
29
29
  Interval strings are defined by
eodag/types/__init__.py CHANGED
@@ -16,16 +16,14 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  """EODAG types"""
19
+
19
20
  from __future__ import annotations
20
21
 
21
22
  from typing import (
22
23
  Annotated,
23
24
  Any,
24
- Dict,
25
- List,
26
25
  Literal,
27
26
  Optional,
28
- Tuple,
29
27
  TypedDict,
30
28
  Union,
31
29
  get_args,
@@ -33,7 +31,7 @@ from typing import (
33
31
  )
34
32
 
35
33
  from annotated_types import Gt, Lt
36
- from pydantic import Field
34
+ from pydantic import BaseModel, Field, create_model
37
35
  from pydantic.fields import FieldInfo
38
36
 
39
37
  from eodag.utils import copy_deepcopy
@@ -41,7 +39,7 @@ from eodag.utils.exceptions import ValidationError
41
39
 
42
40
  # Types mapping from JSON Schema and OpenAPI 3.1.0 specifications to Python
43
41
  # See https://spec.openapis.org/oas/v3.1.0#data-types
44
- JSON_TYPES_MAPPING: Dict[str, type] = {
42
+ JSON_TYPES_MAPPING: dict[str, type] = {
45
43
  "boolean": bool,
46
44
  "integer": int,
47
45
  "number": float,
@@ -52,7 +50,7 @@ JSON_TYPES_MAPPING: Dict[str, type] = {
52
50
  }
53
51
 
54
52
 
55
- def json_type_to_python(json_type: Union[str, List[str]]) -> type:
53
+ def json_type_to_python(json_type: Union[str, list[str]]) -> type:
56
54
  """Get python type from json type https://spec.openapis.org/oas/v3.1.0#data-types
57
55
 
58
56
  >>> json_type_to_python("number")
@@ -69,9 +67,9 @@ def json_type_to_python(json_type: Union[str, List[str]]) -> type:
69
67
  return type(None)
70
68
 
71
69
 
72
- def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
73
- """
74
- checks if the value from an Annotated object is a minimum or maximum
70
+ def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> tuple[str, Any]:
71
+ """Checks if the value from an Annotated object is a minimum or maximum
72
+
75
73
  :param type_info: info from Annotated
76
74
  :return: "min" or "max"
77
75
  """
@@ -83,10 +81,10 @@ def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
83
81
 
84
82
 
85
83
  def _get_type_info_from_annotated(
86
- annotated_type: Annotated[type, Any]
87
- ) -> Dict[str, Any]:
88
- """
89
- retrieves type information from an annotated object
84
+ annotated_type: Annotated[type, Any],
85
+ ) -> dict[str, Any]:
86
+ """Retrieves type information from an annotated object
87
+
90
88
  :param annotated_type: annotated object
91
89
  :return: dict containing type and min/max if available
92
90
  """
@@ -107,7 +105,7 @@ def _get_type_info_from_annotated(
107
105
 
108
106
  def python_type_to_json(
109
107
  python_type: type,
110
- ) -> Optional[Union[str, List[Dict[str, Any]]]]:
108
+ ) -> Optional[Union[str, list[dict[str, Any]]]]:
111
109
  """Get json type from python https://spec.openapis.org/oas/v3.1.0#data-types
112
110
 
113
111
  >>> python_type_to_json(int)
@@ -118,7 +116,8 @@ def python_type_to_json(
118
116
  :param python_type: the python type
119
117
  :returns: the json type
120
118
  """
121
- if get_origin(python_type) is Union:
119
+ origin = get_origin(python_type)
120
+ if origin is Union:
122
121
  json_type = list()
123
122
  for single_python_type in get_args(python_type):
124
123
  type_data = {}
@@ -138,14 +137,16 @@ def python_type_to_json(
138
137
  return list(JSON_TYPES_MAPPING.keys())[
139
138
  list(JSON_TYPES_MAPPING.values()).index(python_type)
140
139
  ]
141
- elif get_origin(python_type) == Annotated:
140
+ elif origin is Annotated:
142
141
  return [_get_type_info_from_annotated(python_type)]
142
+ elif origin is list:
143
+ raise NotImplementedError("Never completed")
143
144
  else:
144
145
  return None
145
146
 
146
147
 
147
148
  def json_field_definition_to_python(
148
- json_field_definition: Dict[str, Any],
149
+ json_field_definition: dict[str, Any],
149
150
  default_value: Optional[Any] = None,
150
151
  required: Optional[bool] = False,
151
152
  ) -> Annotated[Any, FieldInfo]:
@@ -173,12 +174,27 @@ def json_field_definition_to_python(
173
174
  title=json_field_definition.get("title", None),
174
175
  description=json_field_definition.get("description", None),
175
176
  pattern=json_field_definition.get("pattern", None),
177
+ le=json_field_definition.get("maximum"),
178
+ ge=json_field_definition.get("minimum"),
176
179
  )
177
180
 
178
- if "enum" in json_field_definition and (
179
- isinstance(json_field_definition["enum"], (list, set))
180
- ):
181
- python_type = Literal[tuple(sorted(json_field_definition["enum"]))] # type: ignore
181
+ enum = json_field_definition.get("enum")
182
+
183
+ if python_type in (list, set):
184
+ items = json_field_definition.get("items", None)
185
+ if isinstance(items, list):
186
+ python_type = tuple[ # type: ignore
187
+ tuple(
188
+ json_field_definition_to_python(item, required=required)
189
+ for item in items
190
+ )
191
+ ]
192
+ elif isinstance(items, dict):
193
+ enum = items.get("enum")
194
+
195
+ if enum:
196
+ literal = Literal[tuple(sorted(enum))] # type: ignore
197
+ python_type = list[literal] if python_type in (list, set) else literal # type: ignore
182
198
 
183
199
  if "$ref" in json_field_definition:
184
200
  field_type_kwargs["json_schema_extra"] = {"$ref": json_field_definition["$ref"]}
@@ -190,8 +206,8 @@ def json_field_definition_to_python(
190
206
 
191
207
 
192
208
  def python_field_definition_to_json(
193
- python_field_definition: Annotated[Any, FieldInfo]
194
- ) -> Dict[str, Any]:
209
+ python_field_definition: Annotated[Any, FieldInfo],
210
+ ) -> dict[str, Any]:
195
211
  """Get json field definition from python `typing.Annotated`
196
212
 
197
213
  >>> from pydantic import Field
@@ -212,7 +228,7 @@ def python_field_definition_to_json(
212
228
  "%s must be an instance of Annotated" % python_field_definition
213
229
  )
214
230
 
215
- json_field_definition: Dict[str, Any] = dict()
231
+ json_field_definition: dict[str, Any] = dict()
216
232
 
217
233
  python_field_args = get_args(python_field_definition)
218
234
 
@@ -252,6 +268,7 @@ def python_field_definition_to_json(
252
268
  json_field_definition["max"] = [
253
269
  row["max"] if "max" in row else None for row in field_type
254
270
  ]
271
+
255
272
  if "min" in json_field_definition and json_field_definition["min"].count(
256
273
  None
257
274
  ) == len(json_field_definition["min"]):
@@ -291,8 +308,8 @@ def python_field_definition_to_json(
291
308
 
292
309
 
293
310
  def model_fields_to_annotated(
294
- model_fields: Dict[str, FieldInfo]
295
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
311
+ model_fields: dict[str, FieldInfo],
312
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
296
313
  """Convert BaseModel.model_fields from FieldInfo to Annotated
297
314
 
298
315
  >>> from pydantic import create_model
@@ -306,7 +323,7 @@ def model_fields_to_annotated(
306
323
  :param model_fields: BaseModel.model_fields to convert
307
324
  :returns: Annotated tuple usable as create_model argument
308
325
  """
309
- annotated_model_fields = dict()
326
+ annotated_model_fields: dict[str, Annotated[Any, FieldInfo]] = dict()
310
327
  for param, field_info in model_fields.items():
311
328
  field_type = field_info.annotation or type(None)
312
329
  new_field_info = copy_deepcopy(field_info)
@@ -315,6 +332,27 @@ def model_fields_to_annotated(
315
332
  return annotated_model_fields
316
333
 
317
334
 
335
+ def annotated_dict_to_model(
336
+ model_name: str, annotated_fields: dict[str, Annotated[Any, FieldInfo]]
337
+ ) -> BaseModel:
338
+ """Convert a dictionary of Annotated values to a Pydantic BaseModel.
339
+
340
+ :param model_name: name of the model to be created
341
+ :param annotated_fields: dict containing the parameters and annotated values that should become
342
+ the properties of the model
343
+ :returns: pydantic model
344
+ """
345
+ fields = {
346
+ name: (field.__args__[0], field.__metadata__[0])
347
+ for name, field in annotated_fields.items()
348
+ }
349
+ return create_model(
350
+ model_name,
351
+ **fields, # type: ignore
352
+ __config__={"arbitrary_types_allowed": True},
353
+ )
354
+
355
+
318
356
  class ProviderSortables(TypedDict):
319
357
  """A class representing sortable parameter(s) of a provider and the allowed
320
358
  maximum number of used sortable(s) in a search request with the provider
@@ -323,5 +361,14 @@ class ProviderSortables(TypedDict):
323
361
  :param max_sort_params: (optional) The allowed maximum number of sortable(s) in a search request with the provider
324
362
  """
325
363
 
326
- sortables: List[str]
364
+ sortables: list[str]
327
365
  max_sort_params: Annotated[Optional[int], Gt(0)]
366
+
367
+
368
+ class S3SessionKwargs(TypedDict, total=False):
369
+ """A class representing available keyword arguments to pass to :class:`boto3.session.Session` for authentication"""
370
+
371
+ aws_access_key_id: Optional[str]
372
+ aws_secret_access_key: Optional[str]
373
+ aws_session_token: Optional[str]
374
+ profile_name: Optional[str]
eodag/types/bbox.py CHANGED
@@ -15,14 +15,14 @@
15
15
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
- from typing import Dict, List, Tuple, Union
18
+ from typing import Union
19
19
 
20
20
  from pydantic import BaseModel, ValidationInfo, field_validator
21
21
  from shapely.geometry.polygon import Polygon
22
22
 
23
23
  NumType = Union[float, int]
24
24
  BBoxArgs = Union[
25
- List[NumType], Tuple[NumType, NumType, NumType, NumType], Dict[str, NumType]
25
+ list[NumType], tuple[NumType, NumType, NumType, NumType], dict[str, NumType]
26
26
  ]
27
27
 
28
28
 
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Dict, Optional, TypedDict
20
+ from typing import Optional, TypedDict, Union
21
21
 
22
22
 
23
23
  class DownloadConf(TypedDict, total=False):
@@ -33,8 +33,8 @@ class DownloadConf(TypedDict, total=False):
33
33
  """
34
34
 
35
35
  output_dir: str
36
- output_extension: str
36
+ output_extension: Union[str, None]
37
37
  extract: bool
38
- dl_url_params: Dict[str, str]
38
+ dl_url_params: dict[str, str]
39
39
  delete_archive: bool
40
40
  asset: Optional[str]