Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrate to pydantic2 #4800

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ repos:
args:
- --lock
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982
rev: v1.14.1
hooks:
- id: mypy
exclude: tests/.*|demisto_sdk/commands/init/templates/.*
Expand Down
37 changes: 29 additions & 8 deletions demisto_sdk/commands/common/clients/configs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import os
from typing import Any, Dict, Optional

from pydantic import AnyUrl, BaseModel, Field, SecretStr, root_validator
from pydantic import (
AnyUrl,
BaseModel,
Field,
SecretStr,
field_validator,
model_validator,
)

from demisto_sdk.commands.common.constants import (
AUTH_ID,
Expand All @@ -22,9 +29,7 @@ class XsoarClientConfig(BaseModel):
api client config for xsoar-on-prem
"""

base_api_url: AnyUrl = Field(
default=os.getenv(DEMISTO_BASE_URL), description="XSOAR Tenant Base API URL"
)
base_api_url: AnyUrl = Field(..., description="XSOAR Tenant Base API URL")
api_key: SecretStr = Field(
default=SecretStr(os.getenv(DEMISTO_KEY, "")), description="XSOAR API Key"
)
Expand All @@ -36,7 +41,20 @@ class XsoarClientConfig(BaseModel):
)
verify_ssl: bool = string_to_bool(os.getenv(DEMISTO_VERIFY_SSL, False))

@root_validator()
@field_validator("base_api_url", mode="before")
@classmethod
def validate_base_url(cls, value):
if not value:
env_url = os.getenv(DEMISTO_BASE_URL, "").strip()
if not env_url:
raise ValueError(
f"Environment variable {DEMISTO_BASE_URL} is not set or is empty."
)
return env_url
return value

@model_validator(mode="before")
@classmethod
def validate_auth_params(cls, values: Dict[str, Any]):
if not values.get("api_key") and not (
values.get("user") and values.get("password")
Expand Down Expand Up @@ -72,12 +90,15 @@ def __eq__(self, other):


class XsoarSaasClientConfig(XsoarClientConfig):
auth_id: str = Field(default=os.getenv(AUTH_ID), description="XSOAR/XSIAM Auth ID")
auth_id: str = Field(
default=os.getenv(AUTH_ID, ""), description="XSOAR/XSIAM Auth ID"
)
project_id: str = Field(
default=os.getenv(PROJECT_ID), description="XSOAR/XSIAM Project ID"
default=os.getenv(PROJECT_ID, ""), description="XSOAR/XSIAM Project ID"
)

@root_validator()
@model_validator(mode="before")
@classmethod
def validate_auth_params(cls, values: Dict[str, Any]):
if not values.get("api_key"):
raise ValueError("api_key is required for xsoar-saas/xsiam")
Expand Down
2 changes: 1 addition & 1 deletion demisto_sdk/commands/common/docker_images_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


class DockerImageTagMetadata(BaseModel):
python_version: Optional[str]
python_version: Optional[str] = None


class DockerImagesMetadata(PydanticSingleton, BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions demisto_sdk/commands/common/native_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

class NativeImage(BaseModel):
supported_docker_images: List[str]
docker_ref: Optional[str]
docker_ref: Optional[str] = None


class IgnoredContentItem(BaseModel):
Expand Down Expand Up @@ -155,7 +155,7 @@ def __docker_image_to_native_images_support(self) -> List[str]:
"""
return (
self.native_image_config.docker_images_to_native_images_mapping.get(
self.docker_image
self.docker_image # type: ignore[arg-type]
)
or []
)
Expand Down
2 changes: 1 addition & 1 deletion demisto_sdk/commands/content_graph/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ class Relationship(BaseModel):
source_id: Optional[str] = None
source_type: Optional[ContentType] = None
source_fromversion: Optional[str] = None
source_marketplaces: Optional[List[MarketplaceVersions]]
source_marketplaces: Optional[List[MarketplaceVersions]] = None
target: Optional[str] = None
target_type: Optional[ContentType] = None
target_min_version: Optional[str] = None
Expand Down
37 changes: 21 additions & 16 deletions demisto_sdk/commands/content_graph/objects/base_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

import demisto_client
from packaging.version import Version
from pydantic import BaseModel, DirectoryPath, Field
from pydantic.main import ModelMetaclass
from pydantic import BaseModel, ConfigDict, DirectoryPath, Field
from pydantic._internal._model_construction import ModelMetaclass

from demisto_sdk.commands.common.constants import (
MARKETPLACE_MIN_VERSION,
Expand Down Expand Up @@ -57,7 +57,12 @@

class BaseContentMetaclass(ModelMetaclass):
def __new__(
cls, name, bases, namespace, content_type: ContentType = None, **kwargs
cls,
name,
bases,
namespace,
content_type: Optional[ContentType] = None,
**kwargs,
):
"""This method is called before every creation of a ContentItem *class* (NOT class instances!).
If `content_type` is passed as an argument of the class, we add a mapping between the content type
Expand All @@ -78,7 +83,7 @@ def __new__(
Returns:
BaseNode: The model class.
"""
super_cls: BaseContentMetaclass = super().__new__(cls, name, bases, namespace)
super_cls: Type["BaseModel"] = super().__new__(cls, name, bases, namespace)
# for type checking
model_cls: Type["BaseContent"] = cast(Type["BaseContent"], super_cls)
if content_type:
Expand All @@ -98,22 +103,22 @@ def __new__(
class BaseNode(ABC, BaseModel, metaclass=BaseContentMetaclass):
database_id: Optional[str] = Field(None, exclude=True) # used for the database
object_id: str = Field(alias="id")
content_type: ClassVar[ContentType] = Field(include=True)
content_type: ClassVar[ContentType] = Field(exclude=False)
source_repo: str = "content"
node_id: str
marketplaces: List[MarketplaceVersions] = list(MarketplaceVersions)

relationships_data: Dict[RelationshipType, Set["RelationshipData"]] = Field(
defaultdict(set), exclude=True, repr=False
)

class Config:
arbitrary_types_allowed = (
model_config = ConfigDict(
arbitrary_types_allowed=(
True # allows having custom classes for properties in model
)
orm_mode = True # allows using from_orm() method
allow_population_by_field_name = True # when loading from orm, ignores the aliases and uses the property name
keep_untouched = (cached_property,)
),
from_attributes=True,
populate_by_name=True,
ignored_types=(cached_property,),
)

def __getstate__(self):
"""Needed to for the object to be pickled correctly (to use multiprocessing)"""
Expand Down Expand Up @@ -199,8 +204,8 @@ def add_relationship(
class BaseContent(BaseNode):
field_mapping: dict = Field({}, exclude=True)
path: Path
git_status: Optional[GitStatuses]
git_sha: Optional[str]
git_status: Optional[GitStatuses] = None
git_sha: Optional[str] = None
old_base_content_object: Optional["BaseContent"] = None
related_content_dict: dict = Field({}, exclude=True)
structure_errors: List[StructureError] = Field(default_factory=list, exclude=True)
Expand Down Expand Up @@ -326,11 +331,11 @@ def from_path(
logger.exception(
f"Could not parse content item from path {path} using {content_item_parser}"
)
return None
return None

@staticmethod
def match(_dict: dict, path: Path) -> bool:
pass
raise NotImplementedError


class UnknownContent(BaseNode):
Expand Down
4 changes: 2 additions & 2 deletions demisto_sdk/commands/content_graph/objects/classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@


class Classifier(ContentItem, content_type=ContentType.CLASSIFIER): # type: ignore[call-arg]
type: Optional[str]
definition_id: Optional[str] = Field(alias="definitionId")
type: Optional[str] = None
definition_id: Optional[str] = Field(None, alias="definitionId")
version: Optional[int] = 0

@classmethod
Expand Down
14 changes: 7 additions & 7 deletions demisto_sdk/commands/content_graph/objects/conf_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from more_itertools import always_iterable
from packaging.version import Version
from pydantic import BaseModel, Extra, Field, validator
from pydantic import BaseModel, ConfigDict, Field, field_validator

from demisto_sdk.commands.common.constants import MarketplaceVersions
from demisto_sdk.commands.common.content_constant_paths import CONF_PATH
Expand All @@ -19,8 +19,7 @@


class StrictBaseModel(BaseModel):
class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class DictWithSingleSimpleString(StrictBaseModel):
Expand Down Expand Up @@ -50,13 +49,14 @@ class Test(StrictBaseModel):
toversion: Optional[str] = None
nightly: Optional[bool] = None
context_print_dt: Optional[str] = None
scripts: Optional[Union[str, List[str]]]
scripts: Optional[Union[str, List[str]]] = None
runnable_on_docker_only: Optional[bool] = None
external_playbook_config: Optional[ExternalPlaybookConfig] = None
instance_configuration: Optional[InstanceConfiguration] = None
marketplaces: Optional[MarketplaceVersions] = None

@validator("fromversion", "toversion")
@field_validator("fromversion", "toversion")
@classmethod
def validate_version(cls, v):
Version(v)

Expand All @@ -67,7 +67,7 @@ class ImageConfig(StrictBaseModel):


class DockerThresholds(StrictBaseModel):
field_comment: str = Field(..., alias="_comment")
field_comment: str = Field(alias="_comment")
images: Dict[str, ImageConfig]


Expand All @@ -78,7 +78,7 @@ class ConfJSON(StrictBaseModel):
tests: List[Test]
skipped_tests: Dict[str, str]
skipped_integrations: Dict[str, str]
native_nightly_packs: Optional[List[str]]
native_nightly_packs: Optional[List[str]] = None
nightly_packs: List[str]
unmockable_integrations: Dict[str, str]
parallel_integrations: List[str]
Expand Down
15 changes: 9 additions & 6 deletions demisto_sdk/commands/content_graph/objects/content_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from demisto_sdk.commands.content_graph.objects.relationship import RelationshipData
from demisto_sdk.commands.content_graph.objects.test_playbook import TestPlaybook

from pydantic import DirectoryPath, Field, fields, validator
from pydantic import DirectoryPath, Field, field_validator, fields

from demisto_sdk.commands.common.constants import PACKS_FOLDER, MarketplaceVersions
from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH
Expand Down Expand Up @@ -54,11 +54,12 @@ class ContentItem(BaseContent):
deprecated: bool
description: Optional[str] = ""
is_test: bool = False
pack: Any = Field(None, exclude=True, repr=False)
support: str = ""
pack: Any = Field(None, exclude=True, repr=False, validate_default=True)
support: str = Field("", validate_default=True)
is_silent: bool = False

@validator("path", always=True)
@field_validator("path")
@classmethod
def validate_path(cls, v: Path, values) -> Path:
if v.is_absolute():
return v
Expand All @@ -78,7 +79,8 @@ def match(_dict: dict, path: Path) -> bool:
def pack_id(self) -> str:
return self.in_pack.pack_id if self.in_pack else ""

@validator("pack", always=True)
@field_validator("pack")
@classmethod
def validate_pack(cls, v: Any, values) -> Optional["Pack"]:
# Validate that we have the pack containing the content item.
# The pack is either provided directly or needs to be located.
Expand All @@ -87,7 +89,8 @@ def validate_pack(cls, v: Any, values) -> Optional["Pack"]:
return v
return cls.get_pack(values.get("relationships_data"), values.get("path"))

@validator("support", always=True)
@field_validator("support")
@classmethod
def validate_support(cls, v: str, values) -> str:
# Ensure the 'support' field is present.
# If not directly provided, the support level from the associated pack will be used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import demisto_client
from packaging.version import Version
from pydantic import DirectoryPath, validator
from pydantic import DirectoryPath, field_validator

from demisto_sdk.commands.common.constants import (
DEFAULT_CONTENT_ITEM_FROM_VERSION,
Expand All @@ -26,7 +26,8 @@


class ContentItemXSIAM(ContentItem, ABC):
@validator("fromversion", always=True)
@field_validator("fromversion")
@classmethod
def validate_from_version(cls, v: str) -> str:
if not v or DEFAULT_CONTENT_ITEM_FROM_VERSION == v:
return MINIMUM_XSOAR_SAAS_VERSION
Expand Down
6 changes: 3 additions & 3 deletions demisto_sdk/commands/content_graph/objects/generic_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@


class GenericField(ContentItem, content_type=ContentType.GENERIC_FIELD): # type: ignore[call-arg]
definition_id: Optional[str] = Field(alias="definitionId")
field_type: Optional[str] = Field(alias="type")
definition_id: Optional[str] = Field(None, alias="definitionId")
field_type: Optional[str] = Field(None, alias="type")
version: Optional[int] = 0
group: int = Field(None, exclude=True)
group: Optional[int] = Field(None, exclude=True)
unsearchable: Optional[bool] = Field(None, exclude=True)

def metadata_fields(self) -> Set[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class GenericModule(ContentItem, content_type=ContentType.GENERIC_MODULE): # type: ignore[call-arg]
definition_ids: Optional[List[str]] = Field(alias="definitionIds")
definition_ids: Optional[List[str]] = Field(None, alias="definitionIds")
version: Optional[int] = 0

@staticmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


class GenericType(ContentItem, content_type=ContentType.GENERIC_TYPE): # type: ignore[call-arg]
definition_id: Optional[str] = Field(alias="definitionId")
definition_id: Optional[str] = Field(None, alias="definitionId")
version: Optional[int] = 0

def dump(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

class IndicatorType(ContentItem, content_type=ContentType.INDICATOR_TYPE): # type: ignore[call-arg]
description: str = Field(alias="details")
regex: Optional[str]
regex: Optional[str] = None
reputation_script_name: Optional[str] = Field("", alias="reputationScriptName")
expiration: Any
expiration: Any = None
enhancement_script_names: Optional[List[str]] = Field(
alias="enhancementScriptNames"
None, alias="enhancementScriptNames"
)
version: Optional[int] = 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ def to_raw_dict(self) -> Dict:

class IntegrationScript(ContentItem):
type: str
subtype: Optional[str]
subtype: Optional[str] = None
docker_image: DockerImage = DockerImage("")
alt_docker_images: List[str] = []
auto_update_docker_image: bool = True
description: Optional[str] = Field("")
is_unified: bool = Field(False, exclude=True)
code: Optional[str] = Field(None, exclude=True)
unified_data: dict = Field(None, exclude=True)
unified_data: Optional[dict] = Field(None, exclude=True)
version: Optional[int] = 0
tests: Any = ""

Expand Down
Loading
Loading