2.0 Migration Guide
In this document we'll help you migrate your orchestrator application from orchestrator-core 1.3 to 2.0.
About 2.0
In this release we migrate the orchestrator-core from Pydantic v1 to Pydantic v2. This major change took a lot of effort (PR) as Pydantic v2 is quite different.
There were quite a few breaking changes that we had to deal with. We have adapted orchestrator-core to mitigate the impact of these breaking changes. Your orchestrator application may need to be changed as well, we will guide you in this process. Several changes have been automated through a migration script - more about this later.
Steps
Make sure you have upgraded to the latest orchestrator-core 1.3.x release before starting.
- Create a new branch in your own orchestrator
- Create and activate a virtualenv with python 3.11 (recommended), 3.10 or 3.9 and install your dependencies
- Run
pip install -U orchestrator-core~=2.0.0
to get the latest 2.0 core version and upgrade all other dependencies- Add this to your pinned dependencies
- Run
pip install bump-pydantic
to install a migration tool made by the Pydantic maintainers- No need to add this to your dependencies
- Run
bump-pydantic .
- It is a bit fragile and not very fast on large codebases. You can run it against specific subdirectories to work around problematic files or slow performance
- The tool will leave
# TODO[pydantic]
comments on pydantic 1.x code that it cannot automatically rewrite to pydantic 2.x
- Run
python -m orchestrator.devtools.scripts.migrate_20 <dir>
and point<dir>
to your orchestrator code and tests- This migration script will perform a number of rewrites on your code for orchestrator-core 2.0
Then continue with the following sections.
Pydantic v2 Changes
First and foremost, the full Pydantic v2 migration guide is located here https://docs.pydantic.dev/2.5/migration.
We recommend to read through it quickly before continuing with this guide.
Next we'll give you some pointers of what to change based on our experience of migrating the SURF orchestrator.
Change renamed BaseModel functions
Replace these deprecated function calls on any Pydantic Models (i.e. Subscriptions, ProductBlocks).
Pydantic V1 | Pydantic V2 |
---|---|
__fields__ | model_fields |
__validators__ | __pydantic_validator__ |
construct() | model_construct() |
copy() | model_copy() |
dict() | model_dump() |
json_schema() | model_json_schema() |
json() | model_dump_json() |
parse_obj() | model_validate() |
update_forward_refs() | model_rebuild() |
Full overview: https://docs.pydantic.dev/2.5/migration/#changes-to-pydanticbasemodel
Replace constrained types to Python Annotated[ ]
Most constrained types have been removed from Pydantic v2. We will show constrained int as an example.
The symbols in the after example can be imported with:
from typing import Annotated
from annotated_types import Ge, Le, Len, MinLen, MaxLen, doc
conint
Before:
ipv4_prefixlen: conint(ge=30, le=31)
After:
ipv4_prefixlen: Annotated[int, Ge(30), Le(31)]
ConstrainedInt
Before:
class NumberOfPeerings(ConstrainedInt):
"""Number of peerings."""
ge = 1
le = MAX_NUMBER_OF_PEERINGS
After:
NumberOfPeerings = Annotated[
int, Ge(1), Le(MAX_NUMBER_OF_PEERINGS), doc("Number of peerings.")
]
orchestrator-core 2.0 changes
The following breaking changes have been made:
- Removed
SubscriptionInstanceList
-> see SubscriptionInstanceList removed (covered by migration-script) - Removed
@serializable_property
-> use@computed_field
instead (covered by migration-script) - Removed
DomainModel.get_properties
-> useBaseModel.model_computed_fields()
instead - Removed
build_extendend_domain_model()
-> usebuild_extended_domain_model()
instead - (pydantic-forms) Removed class
UniqueConstrainedList
-> see UniqueConstrainedList - (pydantic-forms) Removed class
ChoiceList
-> usechoice_list()
instead - (pydantic-forms) Removed class
ContactPersonList
-> usecontact_person_list()
instead - (pydantic-forms) Moved
ReadOnlyField
import (covered by migration-script) - (pydantic-forms) Changed
ReadOnlyField
from a Field to an Annotated Literal -> change your code fromfield: int = ReadOnlyField(123)
tofield: ReadOnlyField(123)
SubscriptionInstanceList removed
There are 2 different usecases of SubscriptionInstanceList
that can be changed as follows.
Note: this change is covered by the migration script.
Note: you can no longer instantiate an annotated list type.
I.e. before you could write mylist = ListMax2()
but this is no longer valid.
Instead, write mylist: ListMax2 = []
.
1. Generic List
class ListMax2(SubscriptionInstanceList[SI]):
max_items = 2
becomes
ListMax2 = Annotated[list[SI], Len(max_length=2)]
Which can then be used in a pydantic model, i.e. a product block, with a type subscription
class MainProductBlockInactive(ProductBlockModel):
values: ListMax2[SubProductBlockInactive]
When inheriting from this model you can change the type of the list, however a type: ignore comment is needed to silence errors from type checkers like mypy.
class MainProductBlockProvisioning(MyModel):
values: ListMax2[SubProductBlockProvisioning] # type: ignore
2. Typed List
class ListMax2Numbers(SubscriptionInstanceList[int]):
max_items = 2
Becomes:
ListMax2Numbers = Annotated[list[int], Len(max_length=2)]
Which can then be used in a pydantic model without a type subscription
class MyModel(BaseModel):
values: ListMax2Numbers
UniqueConstrainedList
Class type has been removed. Replace with one of
* unique_conlist(T)
can be imported from pydantic-forms
* Annotated[list[T], AfterValidator(validate_unique_list)]
* Annotated[set, ...]
Before:
class ListOfTwo(UniqueConstrainedList[T]):
min_items = 2
max_items = 2
After:
ListOfTwo = Annotated[list[T], AfterValidator(validate_unique_list), Len(2, 2)]
Recommendations and examples
Set validators on the annotated type rather than on a FormPage/Model
For example, instead of:
AddedServicePorts = conlist(
BgpServicePort, min_items=0, max_items=6 - len(current_service_ports)
)
class ModifySN8IPForm(FormPage):
added_service_ports: AddedServicePorts
_validate_single_vlan: classmethod = validator(
"added_service_ports", allow_reuse=True
)(validate_single_vlan)
_validate_unique_vlans: classmethod = validator(
"added_service_ports", allow_reuse=True
)(validate_service_ports_unique_vlans)
Write:
AddedServicePorts = Annotated[
list[BgpServicePort],
Len(0, 6 - len(current_service_ports)),
AfterValidator(validate_single_vlan),
AfterValidator(validate_service_ports_unique_vlans),
]
class ModifySN8IPForm(FormPage):
added_service_ports: AddedServicePorts
This encapsulates validation logic to the type, making it easier to reuse in other places.
This also makes it possible to extend types, for example we could write the following to perform an extra validation on top of the existing ones.
AddedServicePorts_Extra = Annotated[AddedServicePorts, AfterValidator(validate_extra)]
(mypy) Define a TypeAlias for Choice fields
A form using a Choice
with dynamic values requires a # type: ignore
on the FormPage field to prevent mypy errors.
Example in core v1:
from orchestrator.forms.validators import Choice
from orchestrator.forms import FormPage
from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr
def node_selector(enum: str = "NodesEnum") -> Choice:
node_subscriptions = subscriptions_by_product_type(
"Node", [SubscriptionLifecycle.ACTIVE]
)
nodes = {
str(subscription.subscription_id): subscription.description
for subscription in sorted(
node_subscriptions, key=lambda node: node.description
)
}
return Choice(enum, zip(nodes.keys(), nodes.items())) # type:ignore
def initial_input_form_generator(product: UUIDstr, product_name: str) -> FormGenerator:
class SelectNode(FormPage):
class Config:
title = f"{product_name} - select node"
node_subscription_id: node_selector() # type:ignore # noqa: F821
select_node = yield SelectNode
The same code migrated to core v2, and with a TypeAlias
to prevent the # type: ignore
on the FormPage field.
from pydantic import ConfigDict
from orchestrator.types import SubscriptionLifecycle, UUIDstr
from pydantic_forms.core import FormPage
from pydantic_forms.validators import Choice
from pydantic_forms.types import FormGenerator
def node_selector(enum: str = "NodesEnum") -> type[Choice]:
node_subscriptions = subscriptions_by_product_type(
"Node", [SubscriptionLifecycle.ACTIVE]
)
nodes = {
str(subscription.subscription_id): subscription.description
for subscription in sorted(
node_subscriptions, key=lambda node: node.description
)
}
return Choice(enum, zip(nodes.keys(), nodes.items())) # type:ignore
def initial_input_form_generator(product: UUIDstr, product_name: str) -> FormGenerator:
NodeChoice: TypeAlias = cast(type[Choice], node_selector()) # noqa: F821
class SelectNode(FormPage):
model_config = ConfigDict(title=f"{product_name} - select node")
node_subscription_id: NodeChoice
select_node = yield SelectNode
What now?
After following all of these tips your IDE may still show plenty of errors in your orchestrator.
The best thing to do next is try to start your orchestrator and see which errors are critical to actually running it.
After that, you can start running your testsuite.
Pytest has (among many other things) the helpful option --stepwise
that runs all your tests until the first failure.
Rerunning the command will resume running tests from the last failed test.
This will make it more manageable to start fixing the errors.
Once that is done, your pytest or orchestrator output may still contain many warnings to look into regarding deprecated changes.
If you are not sure how to proceed, don't hesitate to reach out through Slack or a Github Discussion.