feat!: compatibility with pydantic v2

Pydantic v1 is EOL, but some users of this library still use it. Pydantic v2 contains the pydantic v1
in the pydantic.v1 namespace, and pydantic v1.10.17+ also contains this same namespace. So, step one
to migrating to Pydantic v2 is to use the pydantic.v1 namespace throughout. Then, in future, the models
can be updated to true Pydantic v2 when all consumers are ready for it.

I'm marking this as a breaking change since the pydantic library update is not backwards compatible.

Signed-off-by: Daniel Neilson <53624638+ddneilson@users.noreply.github.com>

@@ -30,7 +30,7 @@ classifiers = [
 ]
 dependencies = [
   "pyyaml == 6.0.*",
-  "pydantic ~= 1.10",
+  "pydantic >= 1.10.17",
 ]

 [project.urls]
@@ -99,7 +99,7 @@ mypy_path = "src"

 # See: https://docs.pydantic.dev/mypy_plugin/
 #  - Helps mypy understand pydantic typing.
-plugins = "pydantic.mypy"
+plugins = "pydantic.v1.mypy"

 [tool.ruff]
 line-length = 100

@@ -1,7 +1,7 @@
 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

 from typing import TypedDict, Union, Type
-from pydantic import BaseModel
+from pydantic.v1 import BaseModel
 from inspect import getmodule

 # Calling pydantic's ValidationError.errors() returns a list[ErrorDict], but

@@ -4,7 +4,7 @@
 from pathlib import Path
 from typing import Optional, cast

-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from ._errors import CompatibilityError, DecodeValidationError
 from ._symbol_table import SymbolTable

@@ -3,12 +3,12 @@
 import re
 from typing import TYPE_CHECKING, Any, Callable, Optional, Pattern, Union

-from pydantic.errors import AnyStrMaxLengthError, AnyStrMinLengthError, StrRegexError
-from pydantic.utils import update_not_none
-from pydantic.validators import strict_str_validator
+from pydantic.v1.errors import AnyStrMaxLengthError, AnyStrMinLengthError, StrRegexError
+from pydantic.v1.utils import update_not_none
+from pydantic.v1.validators import strict_str_validator

 if TYPE_CHECKING:
-    from pydantic.typing import CallableGenerator
+    from pydantic.v1.typing import CallableGenerator

 class DynamicConstrainedStr(str):

@@ -10,7 +10,7 @@
 from ._expression import InterpolationExpression

 if TYPE_CHECKING:
-    from pydantic.typing import CallableGenerator
+    from pydantic.v1.typing import CallableGenerator

 @dataclass

@@ -2,8 +2,8 @@

 from typing import Any, Union

-from pydantic import ValidationError
-from pydantic.error_wrappers import ErrorWrapper
+from pydantic.v1 import ValidationError
+from pydantic.v1.error_wrappers import ErrorWrapper

 from .._symbol_table import SymbolTable
 from .._format_strings import FormatString, FormatStringError

@@ -4,9 +4,9 @@
 from typing import Any, Optional, Type
 from inspect import isclass

-from pydantic.error_wrappers import ErrorWrapper
-import pydantic.fields
-from pydantic.typing import is_literal_type
+from pydantic.v1.error_wrappers import ErrorWrapper
+import pydantic.v1.fields
+from pydantic.v1.typing import is_literal_type

 from .._types import OpenJDModel, ResolutionScope
 from .._format_strings import FormatString, FormatStringError
@@ -104,7 +104,7 @@
 # 4. Since this validation is a pre-validator, we basically have to re-implement a fragment of Pydantic's model parser for this
 #    depth first traversal. Thus, you'll need to know the following about Pydantic v1.x's data model and parser to understand this
 #    implementation:
-#    a) All models are derived from pydantic.BaseModel
+#    a) All models are derived from pydantic.v1.BaseModel
 #    b) pydantic.BaseModel.__fields__: dict[str, pydantic.ModelField] is injected into all BaseModels by pydantic's BaseModel metaclass.
 #       This member is what gives pydantic information about each of the fields defined in the model class. The key of the dict is the
 #       name of the field in the model.
@@ -228,7 +228,7 @@ def _validate_model_template_variable_references(
     value_symbols = _collect_variable_definitions(cls, values, current_scope, symbol_prefix)

     # Recursively validate the contents of FormatStrings within the model.
-    # Note: cls.__fields__: dict[str, pydantic.fields.ModelField]
+    # Note: cls.__fields__: dict[str, pydantic.v1.fields.ModelField]
     for field_name, field_model in cls.__fields__.items():
         field_value = values.get(field_name, None)
         if field_value is None:
@@ -248,7 +248,7 @@ def _validate_model_template_variable_references(
         # Add in all of the symbols passed down from the parent.
         validation_symbols.update_self(symbols)

-        if field_model.shape == pydantic.fields.SHAPE_SINGLETON:
+        if field_model.shape == pydantic.v1.fields.SHAPE_SINGLETON:
             _validate_singleton(
                 errors,
                 field_model,
@@ -258,7 +258,7 @@ def _validate_model_template_variable_references(
                 validation_symbols,
                 (*loc, field_name),
             )
-        elif field_model.shape == pydantic.fields.SHAPE_LIST:
+        elif field_model.shape == pydantic.v1.fields.SHAPE_LIST:
             if not isinstance(field_value, list):
                 continue
             assert field_model.sub_fields is not None  # For the type checker
@@ -273,7 +273,7 @@ def _validate_model_template_variable_references(
                     validation_symbols,
                     (*loc, field_name, i),
                 )
-        elif field_model.shape == pydantic.fields.SHAPE_DICT:
+        elif field_model.shape == pydantic.v1.fields.SHAPE_DICT:
             if not isinstance(field_value, dict):
                 continue
             assert field_model.sub_fields is not None  # For the type checker
@@ -300,7 +300,7 @@ def _validate_model_template_variable_references(

 def _validate_singleton(
     errors: list[ErrorWrapper],
-    field_model: pydantic.fields.ModelField,
+    field_model: pydantic.v1.fields.ModelField,
     field_value: Any,
     current_scope: ResolutionScope,
     symbol_prefix: str,
@@ -356,7 +356,7 @@ def _validate_singleton(

 def _validate_general_union(
     errors: list[ErrorWrapper],
-    field_model: pydantic.fields.ModelField,
+    field_model: pydantic.v1.fields.ModelField,
     field_value: Any,
     current_scope: ResolutionScope,
     symbol_prefix: str,
@@ -375,11 +375,11 @@ def _validate_general_union(
     # and attempt to process the value as that type.
     assert field_model.sub_fields is not None  # For the type checker
     for sub_field in field_model.sub_fields:
-        if sub_field.shape == pydantic.fields.SHAPE_SINGLETON:
+        if sub_field.shape == pydantic.v1.fields.SHAPE_SINGLETON:
             _validate_singleton(
                 errors, sub_field, field_value, current_scope, symbol_prefix, symbols, loc
             )
-        elif sub_field.shape == pydantic.fields.SHAPE_LIST:
+        elif sub_field.shape == pydantic.v1.fields.SHAPE_LIST:
             if not isinstance(field_value, list):
                 # The given value must be a list in this case.
                 continue
@@ -414,8 +414,8 @@ def _check_format_string(

 def _get_model_for_singleton_value(
-    field_model: pydantic.fields.ModelField, value: Any
-) -> Optional[pydantic.fields.ModelField]:
+    field_model: pydantic.v1.fields.ModelField, value: Any
+) -> Optional[pydantic.v1.fields.ModelField]:
     """Given a ModelField and the value that we're given for that field, determine
     the actual ModelField for the value in the event that the ModelField may be for
     a discriminated union."""
@@ -509,7 +509,7 @@ def _collect_variable_definitions(  # noqa: C901  (suppress: too complex)
             symbol_name = f"{symbol_prefix}{symbol}"
         _add_symbol(symbols["__self__"], current_scope, symbol_name)

-    # Note: cls.__fields__: dict[str, pydantic.fields.ModelField]
+    # Note: cls.__fields__: dict[str, pydantic.v1.fields.ModelField]
     for field_name, field_model in cls.__fields__.items():
         field_value = values.get(field_name, None)
         if field_value is None:
@@ -519,11 +519,11 @@ def _collect_variable_definitions(  # noqa: C901  (suppress: too complex)
             # Literals cannot define variables, so skip this field.
             continue

-        if field_model.shape == pydantic.fields.SHAPE_SINGLETON:
+        if field_model.shape == pydantic.v1.fields.SHAPE_SINGLETON:
             result = _collect_singleton(field_model, field_value, current_scope, symbol_prefix)
             if result:
                 symbols[field_name] = result
-        elif field_model.shape == pydantic.fields.SHAPE_LIST:
+        elif field_model.shape == pydantic.v1.fields.SHAPE_LIST:
             # If the shape expects a list, but the value isn't one then we have a validation error.
             # The error will get flagged by subsequent passes of the model validation.
             if not isinstance(field_value, list):
@@ -535,7 +535,7 @@ def _collect_variable_definitions(  # noqa: C901  (suppress: too complex)
                 result = _collect_singleton(item_model, item, current_scope, symbol_prefix)
                 if result:
                     symbols[field_name].update_self(result)
-        elif field_model.shape == pydantic.fields.SHAPE_DICT:
+        elif field_model.shape == pydantic.v1.fields.SHAPE_DICT:
             # dict[] fields can't define symbols.
             continue
         else:
@@ -566,7 +566,7 @@ def _add_symbol(into: ScopedSymtabs, scope: ResolutionScope, symbol_name: str) -

 def _collect_singleton(
-    model: pydantic.fields.ModelField,
+    model: pydantic.v1.fields.ModelField,
     value: Any,
     current_scope: ResolutionScope,
     symbol_prefix: str,

@@ -7,9 +7,9 @@
 from typing import Any, ClassVar, Optional, Type, TypeVar, Union, cast

 import yaml
-from pydantic import BaseModel
-from pydantic import ValidationError as PydanticValidationError
-from pydantic.error_wrappers import ErrorWrapper
+from pydantic.v1 import BaseModel
+from pydantic.v1 import ValidationError as PydanticValidationError
+from pydantic.v1.error_wrappers import ErrorWrapper

 from ._errors import DecodeValidationError
 from ._types import EnvironmentTemplate, JobTemplate, OpenJDModel, TemplateSpecificationVersion

@@ -8,7 +8,7 @@
 from enum import Enum
 from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Type, Union

-from pydantic import BaseModel, Extra
+from pydantic.v1 import BaseModel, Extra

 from ._symbol_table import SymbolTable

@@ -9,7 +9,7 @@
 from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Type, Union, cast
 from typing_extensions import Annotated

-from pydantic import (
+from pydantic.v1 import (
     Field,
     PositiveInt,
     PositiveFloat,
@@ -22,7 +22,7 @@
     root_validator,
     validator,
 )
-from pydantic.error_wrappers import ErrorWrapper
+from pydantic.v1.error_wrappers import ErrorWrapper

 from .._format_strings import FormatString
 from .._errors import ExpressionError, TokenError

@@ -5,7 +5,7 @@
 from typing import Optional, Union, cast

 import pytest
-from pydantic import PositiveInt, ValidationError
+from pydantic.v1 import PositiveInt, ValidationError

 from openjd.model import SymbolTable
 from openjd.model._format_strings import FormatString

@@ -3,7 +3,7 @@
 from typing import Any, Literal, Union
 from enum import Enum
 from typing_extensions import Annotated
-from pydantic import Field
+from pydantic.v1 import Field

 import pytest

@@ -3,7 +3,7 @@
 import re

 import pytest
-from pydantic import BaseModel, ValidationError
+from pydantic.v1 import BaseModel, ValidationError

 from openjd.model._format_strings._dyn_constrained_str import DynamicConstrainedStr

@@ -1,6 +1,6 @@
 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

-from pydantic import BaseModel, root_validator, Field
+from pydantic.v1 import BaseModel, root_validator, Field
 from typing import Literal, Union

 from openjd.model._convert_pydantic_error import (

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import Action, EnvironmentActions, StepActions

@@ -1,7 +1,7 @@
 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

 import pytest
-from pydantic import BaseModel
+from pydantic.v1 import BaseModel
 from typing import Type
 import openjd.model.v2023_09 as mod
 from inspect import getmembers, getmodule, isclass

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import EmbeddedFileText

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import EnvironmentTemplate

@@ -4,7 +4,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import (

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import JobTemplate

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import (

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import EnvironmentScript, StepScript

@@ -4,7 +4,7 @@
 import string

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import (

@@ -3,7 +3,7 @@
 from typing import Any

 import pytest
-from pydantic import ValidationError
+from pydantic.v1 import ValidationError

 from openjd.model._parse import _parse_model
 from openjd.model.v2023_09 import StepTemplate

@@ -4,7 +4,7 @@
 import string

 import pytest
-from pydantic import BaseModel, ValidationError
+from pydantic.v1 import BaseModel, ValidationError

 from openjd.model.v2023_09 import (
     AmountCapabilityName,


feat!: compatibility with pydantic v2

Fixes: https://github.com/OpenJobDescription/openjd-model-for-python/issues/107

### What was the problem/requirement? (What/Why)

Pydantic v1 is EOL, but some users of this library still use it. Those users would like to update to pydantic v2, but this library needs to be compatible with it for that to happen.

### What was the solution? (How)

Pydantic v2 contains the pydantic v1 in the pydantic.v1 namespace, and pydantic v1.10.17+ also contains this same namespace. So, step one to migrating to Pydantic v2 is to use the pydantic.v1 namespace throughout. Then, in future, the models can be updated to true Pydantic v2 when all consumers are ready for it.

### What is the impact of this change?

Consumers of this library can now update to a newer Pydantic without this library blocking them.

### How was this change tested?

The unit tests pass. I've run the tests both with pydantic 1.10.18 and pydantic 2.9.2.

### Was this change documented?

- Are relevant docstrings in the code base updated?

### Is this a breaking change?

I'm marking this as a breaking change since the pydantic library update is not backwards compatible.

### Does this change impact security?

No

----

*By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.*

openjd-model-for-python

Provides a Python implementation of the data model for Open Job Description's template schemas.


## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=OpenJobDescription_openjd-model-for-python&pullRequest=148) **Quality Gate passed**
Issues
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=OpenJobDescription_openjd-model-for-python&pullRequest=148&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=OpenJobDescription_openjd-model-for-python&pullRequest=148&issueStatuses=ACCEPTED)

Measures
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=OpenJobDescription_openjd-model-for-python&pullRequest=148&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=OpenJobDescription_openjd-model-for-python&pullRequest=148&metric=new_coverage&view=list)
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=OpenJobDescription_openjd-model-for-python&pullRequest=148&metric=new_duplicated_lines_density&view=list)

[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=OpenJobDescription_openjd-model-for-python&pullRequest=148)

SonarQubeCloud

SonarQube Cloud (formerly SonarCloud) is a SaaS code analysis tool designed to detect coding issues in 30+ languages, frameworks, and IaC platforms. Your code is checked against an extensive set of rules that cover many attributes of code, such as maintainability, reliability, and security issues, on each merge/pull request.

For those requiring US-based hosting, [SonarQube Cloud US](https://sonarqube.us/) offers the perfect alternative. If you need data residency outside the US or within the EU, [SonarQube Cloud](https://sonarcloud.io/) with this GitHub app is the ideal solution.

SonarQube Cloud integrates seamlessly into your GitHub workflow and provides clear guidance for resolving any code quality and security issues detected. The majority of popular languages can be analyzed automatically; no configuration is required!

With [SonarQube Cloud](https://sonarcloud.io/github), you get:

-   Fast, precise static analysis and feedback on your code

-   Support for popular languages, frameworks, and IaC tools

-   Free analysis for open-source projects and private projects up to max 50k LoC

-   Automatic analysis of pull requests and branches with results reported inline

-   IDE Support with [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/) integration

-   Quality Gate (added as a GitHub check) acting at two different levels - first, preventing you from merging pull requests that contain issues - and later, helping you to release only clean, safe code

-   Sign up for SonarQube free tier and begin scanning your private and public repositories

A [GitHub Action](https://github.com/marketplace/actions/sonarcloud-scan) is available in the marketplace to make it easy for you (for projects where automatic analysis isn't available). SonarQube Cloud also integrates smoothly with Jenkins, CircleCI, TravisCI, CirrusCI, and many other tools.

A free tier is available that allows you to explore the benefits of SonarQube Cloud using your public and private repositories. We also provide a no-commitment, [14-day trial](https://www.sonarsource.com/products/sonarcloud/signup/) of SonarQube Cloud Team for unlimited team members, up to 1.9M LoC, AI CodeFix recommendations, and more. No need to speak with a sales rep or request a license key - get automatic code analysis results on your private projects in minutes! Want to see more examples of SonarCloud in action? You can [explore current Open Source projects in SonarCloud](https://sonarcloud.io/explore/projects?sort=-analysis_date) that are using the [Clean as You Code](https://www.sonarsource.com/solutions/our-unique-approach/) methodology.

What are you waiting for? SonarQube Cloud helps you consistently deliver cleaner, secure software that future developers will appreciate and your users will love. Start writing Clean Code. [Now](https://www.sonarsource.com/products/sonarcloud/signup/)!

read


read