Skip to content

Commit a83f492

Browse files
committed
fix(deps): Make library compatible with Pydantic V2
1 parent 6954d4e commit a83f492

38 files changed

Lines changed: 956 additions & 793 deletions

poetry.lock

Lines changed: 186 additions & 68 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ queenbee = 'queenbee.cli:main'
2323

2424
[tool.poetry.dependencies]
2525
python = "^3.10"
26-
pydantic = "<2.0"
26+
pydantic = "^2.0"
2727
pyyaml = ">=6.0"
2828
jsonschema = ">=4.17.3"
2929
click = "^8.1.7"
@@ -33,7 +33,7 @@ click-plugins = "^1.1.1"
3333
cli = ["click", "click_plugins"]
3434

3535
[tool.poetry.group.dev.dependencies]
36-
pydantic-openapi-helper = "^0.2.11"
36+
pydantic-openapi-helper = "^1.0.2"
3737
coverage = ">=6"
3838
coveralls = ">=3"
3939
pytest = ">=6.2.4"

queenbee/base/basemodel.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Queenbee utility functions."""
22
import hashlib
33
import json
4-
from typing import List, Dict, Any
4+
from typing import List, Union, Dict, Any
55

66
import yaml
77
from pydantic import BaseModel as PydanticBaseModel
8-
from pydantic import validator, Field, constr
8+
from pydantic import Field, field_validator, ValidationError
99

1010
from .parser import parse_file
1111
from .variable import get_ref_variable
@@ -30,37 +30,42 @@ class BaseModelNoType(PydanticBaseModel):
3030
extensions.
3131
"""
3232

33-
def yaml(self, exclude_unset=False, **kwargs):
33+
def yaml(self, exclude_defaults: bool = False, exclude_none: bool = False, **kwargs):
3434
"""Get a YAML string from the model
3535
3636
Keyword Arguments:
37-
exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values
38-
(default: {False})
37+
exclude_defaults {bool} -- Whether to exclude fields that are set to their
38+
default values. (default: {False})
3939
4040
Returns:
4141
str -- A yaml string representing the model
4242
"""
43+
data = self.model_dump(
44+
mode='json', by_alias=True, exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs
45+
)
4346
return yaml.dump(
44-
json.loads(self.json(by_alias=True,
45-
exclude_unset=exclude_unset, **kwargs)),
47+
data,
4648
default_flow_style=False
4749
)
4850

49-
def to_dict(self, exclude_unset=False, by_alias=True, **kwargs):
51+
def to_dict(self, exclude_defaults: bool = False, by_alias: bool = True, exclude_none: bool = False, **kwargs):
5052
"""Get a dictionary from the model
5153
5254
Keyword Arguments:
53-
exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values
55+
exclude_defaults {bool} -- Whether to exclude fields that are set to their
56+
default values.
5457
(default: {False})
5558
by_alias {bool} -- Boolean toggle to use attribute alias or attribute names
5659
as key (default: {True})
5760
5861
Returns:
5962
dict -- A python dictionary representing the model
6063
"""
61-
return json.loads(self.json(by_alias=by_alias, exclude_unset=exclude_unset, **kwargs))
64+
return self.model_dump(
65+
mode='json', by_alias=by_alias, exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs
66+
)
6267

63-
def to_json(self, filepath, indent=None, **kwargs):
68+
def to_json(self, filepath: str, indent: int = None, **kwargs):
6469
"""Write a JSON file of the model
6570
6671
Arguments:
@@ -70,20 +75,19 @@ def to_json(self, filepath, indent=None, **kwargs):
7075
indent {int} -- indent amount (default: {None})
7176
"""
7277
with open(filepath, 'w') as file:
73-
file.write(self.json(by_alias=True, exclude_unset=False,
74-
indent=indent, **kwargs))
78+
file.write(self.model_dump_json(by_alias=True, indent=indent, exclude_none=True, **kwargs))
7579

76-
def to_yaml(self, filepath, exclude_unset=False, **kwargs):
80+
def to_yaml(self, filepath: str, exclude_defaults: bool = False, exclude_none: bool = True, **kwargs):
7781
"""Write a YAML file of the model
7882
7983
Arguments:
8084
filepath {str} -- Path to the file to be written
8185
8286
Keyword Arguments:
83-
exclude_unset {bool} -- Boolean toggle to add or remove any unset/None values
84-
(default: {False})
87+
exclude_defaults {bool} -- Whether to exclude fields that are set to their
88+
default values. (default: {False})
8589
"""
86-
content = self.yaml(exclude_unset=exclude_unset, **kwargs)
90+
content = self.yaml(exclude_defaults=exclude_defaults, exclude_none=exclude_none, **kwargs)
8791

8892
with open(filepath, 'w') as out_file:
8993
out_file.write(content)
@@ -99,7 +103,10 @@ def from_file(cls, filepath):
99103
cls -- An instance of the pydantic class
100104
"""
101105
data = parse_file(filepath)
102-
return cls.parse_obj(data)
106+
try:
107+
return cls.model_validate(data)
108+
except ValidationError as e:
109+
raise ValueError(e) from e
103110

104111
def __repr__(self):
105112
return self.yaml()
@@ -112,7 +119,7 @@ def __hash__(self):
112119
str -- A hash/digest of the model
113120
"""
114121
return hashlib.sha256(
115-
self.json(by_alias=True, exclude_unset=False).encode('utf-8')
122+
self.model_dump_json(by_alias=True, exclude_none=True).encode('utf-8')
116123
).hexdigest()
117124

118125
def _referenced_values(self, var_names: List[str]) -> Dict[str, List[str]]:
@@ -143,16 +150,17 @@ def _referenced_values(self, var_names: List[str]) -> Dict[str, List[str]]:
143150
class BaseModel(BaseModelNoType):
144151
"""BaseModel with functionality to return the object as a yaml string."""
145152

146-
type: constr(regex='^BaseModel$') = 'BaseModel'
153+
type: str = Field('BaseModel', pattern='^BaseModel$')
147154

148155
annotations: Dict[str, Any] = Field(
149-
None,
156+
default_factory=dict,
150157
description='An optional dictionary to add annotations to inputs. These '
151158
'annotations will be used by the client side libraries.'
152159
)
153160

154-
@validator('type')
155-
def ensure_type_match(cls, v):
161+
@field_validator('type')
162+
@classmethod
163+
def ensure_type_match(cls, v: str) -> str:
156164
name = cls.__name__
157165
if name != v:
158166
raise ValueError(
@@ -161,7 +169,3 @@ def ensure_type_match(cls, v):
161169
f'Pydantic object. In this case: {name}'
162170
)
163171
return v
164-
165-
@validator('annotations', always=True)
166-
def replace_none_value(cls, v):
167-
return {} if not v else v

queenbee/base/metadata.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,45 @@
66
https://swagger.io/specification/#infoObject
77
"""
88

9-
from typing import List
10-
from pydantic import Field, constr, AnyUrl
9+
from typing import List, Literal, Union
10+
from pydantic import Field, AnyUrl
1111

1212
from .basemodel import BaseModel
1313

1414

1515
class Maintainer(BaseModel):
1616
"""Maintainer information"""
17-
type: constr(regex='^Maintainer$') = 'Maintainer'
17+
type: Literal['Maintainer'] = 'Maintainer'
1818

1919
name: str = Field(
2020
...,
2121
description='The name of the author/maintainer person or organization.'
2222
)
2323

24-
email: str = Field(
24+
email: Union[str, None] = Field(
2525
None,
2626
description='The email address of the author/maintainer person or organization.'
2727
)
2828

2929

3030
class License(BaseModel):
3131
"""License information for the Package"""
32-
type: constr(regex='^License$') = 'License'
32+
type: Literal['License'] = 'License'
3333

3434
name: str = Field(
3535
...,
3636
description='The license name used for the package.'
3737
)
3838

39-
url: AnyUrl = Field(
39+
url: Union[AnyUrl, None] = Field(
4040
None,
4141
description='A URL to the license used for the package.'
4242
)
4343

4444

4545
class MetaData(BaseModel):
4646
"""Package metadata information."""
47-
type: constr(regex='^MetaData$') = 'MetaData'
47+
type: Literal['MetaData'] = 'MetaData'
4848

4949
name: str = Field(
5050
...,
@@ -56,47 +56,47 @@ class MetaData(BaseModel):
5656
description='The tag of the package'
5757
)
5858

59-
app_version: str = Field(
59+
app_version: Union[str, None] = Field(
6060
None,
6161
description='The version of the application code underlying the manifest'
6262
)
6363

64-
keywords: List[str] = Field(
64+
keywords: Union[List[str], None] = Field(
6565
None,
6666
description='A list of keywords to search the package by'
6767
)
6868

69-
maintainers: List[Maintainer] = Field(
69+
maintainers: Union[List[Maintainer], None] = Field(
7070
None,
7171
description='A list of maintainers for the package'
7272
)
7373

74-
home: str = Field(
74+
home: Union[str, None] = Field(
7575
None,
7676
description='The URL of this package\'s home page'
7777
)
7878

79-
sources: List[str] = Field(
79+
sources: Union[List[str], None] = Field(
8080
None,
8181
description='A list of URLs to source code for this project'
8282
)
8383

84-
icon: str = Field(
84+
icon: Union[str, None] = Field(
8585
None,
8686
description='A URL to an SVG or PNG image to be used as an icon'
8787
)
8888

89-
deprecated: bool = Field(
89+
deprecated: Union[bool, None] = Field(
9090
None,
9191
description='Whether this package is deprecated'
9292
)
9393

94-
description: str = Field(
94+
description: Union[str, None] = Field(
9595
None,
9696
description='A description of what this package does'
9797
)
9898

99-
license: License = Field(
99+
license: Union[License, None] = Field(
100100
None,
101101
description='The license information.'
102102
)

queenbee/cli/plugin/new.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def new(name, path):
7474
input_dict = parse_file(path)
7575
input_dict['metadata']['name'] = name
7676

77-
plugin = Plugin.parse_obj(input_dict)
77+
plugin = Plugin.model_validate(input_dict)
7878

7979
# Readme
8080
readme_string = f"""

queenbee/cli/recipe/lint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def lint(path, dependency_update):
4242
config=ctx.obj.config
4343
)
4444
obj = br.to_dict()
45-
BakedRecipe.parse_obj(obj)
45+
BakedRecipe.model_validate(obj)
4646
except ValidationError as error:
4747
raise click.ClickException(error)
4848
except FileNotFoundError as error:

queenbee/cli/recipe/new.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def new(name, path):
8282
input_dict = parse_file(path)
8383
input_dict['metadata']['name'] = name
8484

85-
recipe = Recipe.parse_obj(input_dict)
85+
recipe = Recipe.model_validate(input_dict)
8686

8787
# Readme
8888
readme_string = f"""

queenbee/config/__init__.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
from typing import List, Union, Dict
1+
from typing import List, Union, Dict, Literal
22
from urllib.parse import urlparse
3-
from pydantic import Field, SecretStr, constr
3+
from pydantic import Field, SecretStr
44

55
from ..base.basemodel import BaseModel
66
from .auth import JWTAuth, HeaderAuth
77
from .repositories import RepositoryReference
88

99

1010
class Config(BaseModel):
11-
type: constr(regex='^Config$') = 'Config'
11+
type: Literal['Config'] = 'Config'
1212

1313
auth: List[Union[JWTAuth, HeaderAuth]] = Field(
1414
[],
@@ -111,8 +111,3 @@ def remove_repository(self, name: str):
111111
existing_index = i
112112

113113
del self.repositories[existing_index]
114-
115-
class Config:
116-
json_encoders = {
117-
SecretStr: lambda v: v.get_secret_value() if v else None,
118-
}

queenbee/config/auth.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
1-
import json
2-
from enum import Enum
3-
from urllib import request
4-
from urllib.error import HTTPError
5-
from typing import Union, Dict
6-
from pydantic import Field, SecretStr, constr
7-
1+
from typing import Dict, Literal, Union
2+
from pydantic import Field, SecretStr
83

94
from ..base.basemodel import BaseModel
10-
from ..base.request import make_request, USER_AGENT_STRING
115

126

137
class BaseAuth(BaseModel):
14-
15-
type: constr(regex='^BaseAuth$') = 'BaseAuth'
8+
type: Literal['BaseAuth'] = 'BaseAuth'
169

1710
domain: str = Field(
1811
...,
1912
description='The host domain to authenticate to',
20-
example='api.pollination.solutions'
13+
json_schema_extra={'example': 'api.pollination.solutions'}
2114
)
2215

23-
access_token: SecretStr = Field(
16+
access_token: Union[SecretStr, None] = Field(
2417
None,
25-
description='A JWT token retrieved from a previous login'
18+
description='An access token for the domain.'
2619
)
2720

2821
@property
@@ -41,12 +34,11 @@ def refresh_token(self):
4134

4235

4336
class HeaderAuth(BaseAuth):
44-
45-
type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt'
37+
type: Literal['HeaderAuth'] = 'HeaderAuth'
4638

4739
header_name: str = Field(
4840
...,
49-
description='The HTTP header to user'
41+
description='The HTTP header to use'
5042
)
5143

5244
@property
@@ -56,11 +48,10 @@ def auth_header(self) -> Dict[str, str]:
5648
Returns:
5749
Dict[str, str]: a header with an API token
5850
"""
51+
if self.access_token is None:
52+
return {}
5953
return {self.header_name: self.access_token.get_secret_value()}
6054

6155

6256
class JWTAuth(BaseAuth):
63-
64-
type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt'
65-
66-
57+
type: Literal['JWTAuth'] = 'JWTAuth'

0 commit comments

Comments
 (0)