Skip to content

Commit 0d05a14

Browse files
committed
Upgrade to pydantic2
1 parent 41fab59 commit 0d05a14

9 files changed

+104
-73
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dependencies = [
3535
"qastle>=0.17",
3636
"func_adl>=3.2.6",
3737
"requests>=2.31",
38-
"pydantic>=1.10,<2.0", # FIXME: Adopt pydantic 2.0 API
38+
"pydantic>=2.0",
3939
"httpx>=0.24",
4040
"miniopy-async>=1.15",
4141
"tinydb>=4.7",

servicex/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from servicex import models
3030
from servicex import servicex_client
3131
from servicex import dataset_identifier
32-
from servicex.databinder_models import Sample, General, Definition, ServiceXSpec
32+
from servicex.databinder_models import Sample, General, DefinitionDict, ServiceXSpec
3333
from servicex.func_adl.func_adl_dataset import FuncADLQuery
3434
from servicex.servicex_client import ServiceXClient, deliver
3535
from .query import Query
@@ -53,7 +53,7 @@
5353
"FuncADLQuery",
5454
"Sample",
5555
"General",
56-
"Definition",
56+
"DefinitionDict",
5757
"ServiceXSpec",
5858
"deliver"
5959
]

servicex/configuration.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,39 @@
3030
from pathlib import Path, PurePath
3131
from typing import List, Optional, Dict
3232

33-
from pydantic import BaseModel, Field, validator
33+
from pydantic import BaseModel, Field, AliasChoices, model_validator
3434

3535
import yaml
3636

3737

3838
class Endpoint(BaseModel):
3939
endpoint: str
4040
name: str
41-
token: Optional[str]
41+
token: Optional[str] = ""
4242

4343

4444
class Configuration(BaseModel):
4545
api_endpoints: List[Endpoint]
4646
default_endpoint: Optional[str] = Field(alias="default-endpoint", default=None)
47-
cache_path: Optional[str] = Field(alias="cache-path", default=None)
47+
cache_path: Optional[str] = Field(
48+
validation_alias=AliasChoices("cache-path", "cache_path"), default=None
49+
)
50+
4851
shortened_downloaded_filename: Optional[bool] = False
4952

50-
@validator("cache_path", always=True)
51-
def expand_cache_path(cls, v):
53+
@model_validator(mode="after")
54+
def expand_cache_path(self):
5255
"""
5356
Expand the cache path to a full path, and create it if it doesn't exist.
5457
Expand ${USER} to be the user name on the system. Works for windows, too.
5558
:param v:
5659
:return:
5760
"""
5861
# create a folder inside the tmp directory if not specified in cache_path
59-
if not v:
60-
v = "/tmp/servicex_${USER}"
62+
if not self.cache_path:
63+
self.cache_path = "/tmp/servicex_${USER}"
6164

62-
s_path = os.path.expanduser(v)
65+
s_path = os.path.expanduser(self.cache_path)
6366

6467
# If they have tried to use the USER or UserName as an expansion, and it has failed, then
6568
# translate it to maintain harmony across platforms.
@@ -75,10 +78,11 @@ def expand_cache_path(cls, v):
7578
p = Path(p_p)
7679
p.mkdir(exist_ok=True, parents=True)
7780

78-
return p.as_posix()
81+
self.cache_path = p.as_posix()
82+
return self
7983

8084
class Config:
81-
allow_population_by_field_name = True
85+
populate_by_name = True
8286

8387
def endpoint_dict(self) -> Dict[str, Endpoint]:
8488
return {endpoint.name: endpoint for endpoint in self.api_endpoints}
@@ -98,7 +102,7 @@ def read(cls, config_path: Optional[str] = None):
98102
yaml_config = cls._add_from_path(walk_up_tree=True)
99103

100104
if yaml_config:
101-
return Configuration(**yaml_config)
105+
return Configuration.model_validate(yaml_config)
102106
else:
103107
path_extra = f"in {config_path}" if config_path else ""
104108
raise NameError(

servicex/databinder_models.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,21 @@
2727
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
from enum import Enum
2929
from typing import Union, Optional, Callable, List
30-
from pydantic import BaseModel, Field, root_validator, constr, validator
30+
from pydantic import (
31+
BaseModel,
32+
Field,
33+
model_validator,
34+
)
3135

3236
from servicex.dataset_identifier import RucioDatasetIdentifier, FileListDataset
3337
from servicex.func_adl import func_adl_dataset
3438

3539

3640
class Sample(BaseModel):
3741
Name: str
38-
Codegen: Optional[str]
39-
RucioDID: Optional[str]
40-
XRootDFiles: Optional[Union[str, List[str]]]
42+
Codegen: Optional[str] = None
43+
RucioDID: Optional[str] = None
44+
XRootDFiles: Optional[Union[str, List[str]]] = None
4145
NFiles: Optional[int] = Field(default=None)
4246
Function: Optional[Union[str, Callable]] = Field(default=None)
4347
Query: Optional[Union[str, func_adl_dataset.Query]] = Field(default=None)
@@ -54,29 +58,29 @@ def dataset_identifier(self):
5458
elif self.XRootDFiles:
5559
return FileListDataset(self.XRootDFiles)
5660

57-
@root_validator
61+
@model_validator(mode="before")
5862
def validate_did_xor_file(cls, values):
5963
"""
6064
Ensure that only one of RootFile or RucioDID is specified.
6165
:param values:
6266
:return:
6367
"""
64-
if values['XRootDFiles'] and values['RucioDID']:
68+
if "XRootDFiles" in values and "RucioDID" in values:
6569
raise ValueError("Only specify one of XRootDFiles or RucioDID, not both.")
66-
if not values['XRootDFiles'] and not values['RucioDID']:
70+
if "XRootDFiles" not in values and "RucioDID" not in values:
6771
raise ValueError("Must specify one of XRootDFiles or RucioDID.")
6872
return values
6973

70-
@root_validator
74+
@model_validator(mode="before")
7175
def validate_function_xor_query(cls, values):
7276
"""
7377
Ensure that only one of Function or Query is specified.
7478
:param values:
7579
:return:
7680
"""
77-
if values['Function'] and values['Query']:
81+
if "Function" in values and "Query" in values:
7882
raise ValueError("Only specify one of Function or Query, not both.")
79-
if not values['Function'] and not values['Query']:
83+
if "Function" not in values and "Query" not in values:
8084
raise ValueError("Must specify one of Function or Query.")
8185
return values
8286

@@ -92,17 +96,21 @@ class DeliveryEnum(str, Enum):
9296

9397
ServiceX: str = Field(..., alias="ServiceX")
9498
Codegen: str
95-
OutputFormat: Union[OutputFormatEnum,
96-
constr(regex='^(parquet|root-file)$')] = Field(default=OutputFormatEnum.root) # NOQA F722
99+
OutputFormat: OutputFormatEnum = (
100+
Field(default=OutputFormatEnum.root, pattern="^(parquet|root-file)$")
101+
) # NOQA F722
97102

98-
Delivery: Union[DeliveryEnum, constr(regex='^(LocalCache|SignedURLs)$')] = Field(default=DeliveryEnum.LocalCache) # NOQA F722
103+
Delivery: DeliveryEnum = Field(
104+
default=DeliveryEnum.LocalCache, pattern="^(LocalCache|SignedURLs)$"
105+
) # NOQA F722
99106

100107

101-
class Definition(BaseModel):
108+
class DefinitionDict(BaseModel):
102109
class Config:
103110
extra = "allow" # Allow additional fields not defined in the model
104111

105-
@root_validator
112+
@model_validator(mode="before")
113+
@classmethod
106114
def check_def_name(cls, values):
107115
"""
108116
Ensure that the definition name is DEF_XXX format
@@ -111,31 +119,36 @@ def check_def_name(cls, values):
111119
"""
112120
for field in values:
113121
if not field.startswith("DEF_"):
114-
raise ValueError(f"Definition key {field} does not meet the convention of DEF_XXX format") # NOQA E501
122+
raise ValueError(
123+
f"Definition key {field} does not meet the convention of DEF_XXX format"
124+
) # NOQA E501
115125
return values
116126

117127

118128
class ServiceXSpec(BaseModel):
119129
General: General
120130
Sample: List[Sample]
121-
Definition: Optional[Definition]
122-
123-
@validator('Sample')
124-
def check_tree_property(cls, v, values):
125-
if 'General' in values and values['General'].Codegen != 'uproot':
126-
for sample in v:
127-
if 'Tree' in sample:
128-
raise ValueError('"Tree" property is not allowed when codegen is not "uproot"')
129-
return v
130-
131-
@root_validator(pre=False, skip_on_failure=True)
132-
def replace_definition(cls, values):
131+
Definition: Optional[DefinitionDict] = None
132+
133+
@model_validator(mode="after")
134+
def check_tree_property(self):
135+
if self.General and self.General.Codegen != "uproot":
136+
for sample in self.Sample:
137+
if "Tree" in sample:
138+
raise ValueError(
139+
'"Tree" property is not allowed when codegen is not "uproot"'
140+
)
141+
return self
142+
143+
@model_validator(mode="after")
144+
def replace_definition(self):
133145
"""
134146
Replace the definition name with the actual definition value looking
135147
through the Samples and the General sections
136148
:param values:
137149
:return:
138150
"""
151+
139152
def replace_value_from_def(value, defs):
140153
"""
141154
Replace the value with the actual definition value
@@ -154,14 +167,14 @@ def replace_value_from_def(value, defs):
154167
raise ValueError(f"Definition {value} not found")
155168
return value
156169

157-
if 'Definition' in values and values['Definition'] is not None:
158-
defs = values['Definition'].dict()
170+
if self.Definition and self.Definition:
171+
defs = self.Definition.dict()
159172
else:
160173
defs = {}
161174

162-
for sample_field in values['Sample']:
175+
for sample_field in self.Sample:
163176
replace_value_from_def(sample_field.__dict__, defs)
164177

165-
replace_value_from_def(values['General'].__dict__, defs)
178+
replace_value_from_def(self.General.__dict__, defs)
166179

167-
return values
180+
return self

servicex/models.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
from datetime import datetime
3030
from enum import Enum
3131

32-
from pydantic import BaseModel, Field, validator
33-
from typing import List, Optional
32+
from pydantic import BaseModel, Field, field_validator
33+
from typing import List, Optional, Union
3434

3535

3636
class ResultDestination(str, Enum):
3737
r"""
3838
Direct the output to object store or posix volume
3939
"""
40+
4041
object_store = "object-store"
4142
volume = "volume"
4243

@@ -45,6 +46,7 @@ class ResultFormat(str, Enum):
4546
r"""
4647
Specify the file format for the generated output
4748
"""
49+
4850
parquet = "parquet"
4951
root = "root-file"
5052

@@ -53,6 +55,7 @@ class Status(str, Enum):
5355
r"""
5456
Status of a submitted transform
5557
"""
58+
5659
complete = ("Complete",)
5760
fatal = ("Fatal",)
5861
canceled = ("Canceled",)
@@ -65,18 +68,21 @@ class TransformRequest(BaseModel):
6568
r"""
6669
Transform request sent to ServiceX
6770
"""
71+
6872
title: Optional[str] = None
6973
did: Optional[str] = None
7074
file_list: Optional[List[str]] = Field(default=None, alias="file-list")
7175
selection: str
7276
image: Optional[str] = None
7377
codegen: str
7478
tree_name: Optional[str] = Field(default=None, alias="tree-name")
75-
result_destination: ResultDestination = Field(alias="result-destination")
76-
result_format: ResultFormat = Field(alias="result-format")
79+
result_destination: Union[str, ResultDestination] = Field(
80+
serialization_alias="result-destination"
81+
)
82+
result_format: Union[str, ResultFormat] = Field(serialization_alias="result-format")
7783

7884
class Config:
79-
allow_population_by_field_name = True
85+
populate_by_name = True
8086

8187
def compute_hash(self):
8288
r"""
@@ -105,28 +111,29 @@ class TransformStatus(BaseModel):
105111
r"""
106112
Status object returned by servicex
107113
"""
114+
108115
request_id: str
109116
did: str
110117
selection: str
111-
tree_name: Optional[str] = Field(alias="tree-name")
118+
tree_name: Optional[str] = Field(validation_alias="tree-name")
112119
image: str
113-
result_destination: ResultDestination = Field(alias="result-destination")
114-
result_format: ResultFormat = Field(alias="result-format")
115-
generated_code_cm: str = Field(alias="generated-code-cm")
120+
result_destination: ResultDestination = Field(validation_alias="result-destination")
121+
result_format: ResultFormat = Field(validation_alias="result-format")
122+
generated_code_cm: str = Field(validation_alias="generated-code-cm")
116123
status: Status
117-
app_version: str = Field(alias="app-version")
124+
app_version: str = Field(validation_alias="app-version")
118125
files: int
119-
files_completed: int = Field(alias="files-completed")
120-
files_failed: int = Field(alias="files-failed")
121-
files_remaining: Optional[int] = Field(alias="files-remaining")
122-
submit_time: datetime = Field(alias="submit-time")
123-
finish_time: Optional[datetime] = Field(alias="finish-time")
124-
minio_endpoint: Optional[str] = Field(alias="minio-endpoint")
125-
minio_secured: Optional[bool] = Field(alias="minio-secured")
126-
minio_access_key: Optional[str] = Field(alias="minio-access-key")
127-
minio_secret_key: Optional[str] = Field(alias="minio-secret-key")
128-
129-
@validator("finish_time", pre=True)
126+
files_completed: int = Field(validation_alias="files-completed")
127+
files_failed: int = Field(validation_alias="files-failed")
128+
files_remaining: Optional[int] = Field(validation_alias="files-remaining", default=0)
129+
submit_time: datetime = Field(validation_alias="submit-time", default=None)
130+
finish_time: Optional[datetime] = Field(validation_alias="finish-time", default=None)
131+
minio_endpoint: Optional[str] = Field(validation_alias="minio-endpoint", default=None)
132+
minio_secured: Optional[bool] = Field(validation_alias="minio-secured", default=None)
133+
minio_access_key: Optional[str] = Field(validation_alias="minio-access-key", default=None)
134+
minio_secret_key: Optional[str] = Field(validation_alias="minio-secret-key", default=None)
135+
136+
@field_validator("finish_time")
130137
def parse_finish_time(cls, v):
131138
if isinstance(v, str) and v == "None":
132139
return None
@@ -137,6 +144,7 @@ class ResultFile(BaseModel):
137144
r"""
138145
Record reporting the properties of a transformed file result
139146
"""
147+
140148
filename: str
141149
size: int
142150
extension: str
@@ -147,6 +155,7 @@ class TransformedResults(BaseModel):
147155
Returned for a submission. Gives you everything you need to know about a completed
148156
transform.
149157
"""
158+
150159
hash: str
151160
title: str
152161
codegen: str

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def transform_status_response() -> dict:
119119
"files-failed": 0,
120120
"files-remaining": 1,
121121
"submit-time": "2023-05-25T20:05:05.564137Z",
122-
"finish-time": "None",
122+
"finish-time": None,
123123
}
124124
]
125125
}
@@ -149,7 +149,7 @@ def completed_status() -> TransformStatus:
149149
"files-failed": 0,
150150
"files-remaining": 1,
151151
"submit-time": "2023-05-25T20:05:05.564137Z",
152-
"finish-time": "None",
152+
"finish-time": None,
153153
"minio-endpoint": "minio.org:9000",
154154
"minio-secured": False,
155155
"minio-access-key": "miniouser",

0 commit comments

Comments
 (0)