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

Update pydantic and in_place versions #176

Merged
merged 2 commits into from
Nov 9, 2023
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/inception-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
TRAVIS_TOKEN: ${{ secrets.TRAVIS_TOKEN }}
APPVEYOR_TOKEN: ${{ secrets.APPVEYOR_TOKEN }}
CIRCLECI_CLI_TOKEN: ${{ secrets.CIRCLECI_CLI_TOKEN }}
PYTHONWARNINGS: error

- name: Coverage report
run: |
Expand Down
9 changes: 7 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ python_requires = >=3.8
install_requires =
click >= 7.0
click-loglevel ~= 0.2
in_place ~= 0.4
ghtoken ~= 0.1
pydantic ~= 1.7
in_place ~= 1.0
pydantic ~= 2.0
python-dateutil ~= 2.7
python-dotenv >= 0.11, < 2.0
PyYAML >= 5.0
requests ~= 2.20
typing_extensions; python_version < '3.9'

[options.extras_require]
all =
Expand Down Expand Up @@ -81,3 +82,7 @@ show_error_codes = True
show_traceback = True
pretty = True
plugins = pydantic.mypy

[pydantic-mypy]
init_forbid_extra = True
warn_required_dynamic_aliases = True
6 changes: 3 additions & 3 deletions src/tinuous/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def fetch(config_file: str, state_path: Optional[str], sanitize_secrets: bool) -
"""Download logs"""
try:
with open(config_file) as fp:
cfg = Config.parse_obj(safe_load(fp))
cfg = Config.model_validate(safe_load(fp))
except FileNotFoundError:
raise click.UsageError(f"Configuration file not found: {config_file}")
if sanitize_secrets and not cfg.secrets:
Expand Down Expand Up @@ -179,7 +179,7 @@ def fetch_commit(config_file: str, committish: str, sanitize_secrets: bool) -> N
"""Download logs for a specific commit"""
try:
with open(config_file) as fp:
cfg = Config.parse_obj(safe_load(fp))
cfg = Config.model_validate(safe_load(fp))
except FileNotFoundError:
raise click.UsageError(f"Configuration file not found: {config_file}")
if sanitize_secrets and not cfg.secrets:
Expand Down Expand Up @@ -250,7 +250,7 @@ def sanitize_cmd(config_file: str, path: list[str]) -> None:
"""Sanitize secrets in logs"""
try:
with open(config_file) as fp:
cfg = Config.parse_obj(safe_load(fp))
cfg = Config.model_validate(safe_load(fp))
except FileNotFoundError:
raise click.UsageError(f"Configuration file not found: {config_file}")
for p in path:
Expand Down
2 changes: 1 addition & 1 deletion src/tinuous/appveyor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class Appveyor(CISystem):
accountName: str
projectSlug: Optional[str]
projectSlug: Optional[str] = None

@staticmethod
def get_auth_tokens() -> dict[str, str]:
Expand Down
54 changes: 25 additions & 29 deletions src/tinuous/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
from collections.abc import Iterator
from datetime import datetime, timezone
from enum import Enum
from functools import cached_property
import heapq
import os
from pathlib import Path, PurePosixPath
import re
from shutil import rmtree
import sys
import tempfile
from time import sleep
from typing import Any, List, Optional, Pattern, Tuple
from typing import Any, List, Optional, Tuple
from zipfile import BadZipFile, ZipFile

from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, BeforeValidator, Field, ValidationInfo
import requests
from requests.exceptions import ChunkedEncodingError
from requests.exceptions import ConnectionError as ReqConError
Expand All @@ -28,6 +28,11 @@
sanitize_pathname,
)

if sys.version_info >= (3, 9):
from typing import Annotated
else:
from typing_extensions import Annotated


class CommonStatus(Enum):
SUCCESS = "success"
Expand Down Expand Up @@ -201,7 +206,7 @@ class CISystem(ABC, BaseModel):
repo: str
token: str
since: datetime
until: Optional[datetime]
until: Optional[datetime] = None
fetched: List[Tuple[datetime, bool]] = Field(default_factory=list)

@staticmethod
Expand All @@ -227,26 +232,18 @@ def new_since(self) -> datetime:
prev_ts = ts
return prev_ts

class Config:
# <https://github.com/samuelcolvin/pydantic/issues/1241>
arbitrary_types_allowed = True
keep_untouched = (cached_property,)


class BuildAsset(ABC, BaseModel):
# The `arbitrary_types_allowed` is for APIClient
class BuildAsset(ABC, BaseModel, arbitrary_types_allowed=True):
client: APIClient
created_at: datetime
event_type: EventType
event_id: str
build_commit: str
commit: Optional[str]
commit: Optional[str] = None
number: int
status: str

class Config:
# To allow APIClient:
arbitrary_types_allowed = True

def path_fields(self) -> dict[str, Any]:
utc_date = self.created_at.astimezone(timezone.utc)
commit = "UNK" if self.commit is None else self.commit
Expand Down Expand Up @@ -288,26 +285,25 @@ class Artifact(BuildAsset):
# import issue:


class NoExtraModel(BaseModel):
class Config:
allow_population_by_field_name = True
extra = "forbid"
class NoExtraModel(BaseModel, populate_by_name=True, extra="forbid"):
pass


def literalize_str(v: Any, info: ValidationInfo) -> Any:
if isinstance(v, str) and not info.data.get("regex"):
v = r"\A" + re.escape(v) + r"\Z"
return v


StrOrRegex = Annotated[re.Pattern, BeforeValidator(literalize_str)]


class WorkflowSpec(NoExtraModel):
regex: bool = False
# Workflow names are stored as compiled regexes regardless of whether
# `regex` is true in order to keep type-checking simple.
include: List[Pattern] = Field(default_factory=lambda: [re.compile(".*")])
exclude: List[Pattern] = Field(default_factory=list)

@validator("include", "exclude", pre=True, each_item=True)
def _maybe_regex(
cls, v: str | re.Pattern[str], values: dict[str, Any] # noqa: B902, U100
) -> str | re.Pattern[str]:
if not values["regex"] and isinstance(v, str):
v = r"\A" + re.escape(v) + r"\Z"
return v
include: List[StrOrRegex] = Field(default_factory=lambda: [re.compile(".*")])
exclude: List[StrOrRegex] = Field(default_factory=list)

def match(self, wf_name: str) -> bool:
return any(r.search(wf_name) for r in self.include) and not any(
Expand Down
10 changes: 5 additions & 5 deletions src/tinuous/circleci.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,24 @@ def paginate(

def get_pipelines(self) -> Iterator[Pipeline]:
for item in self.paginate(f"/v2/project/gh/{self.repo}/pipeline"):
yield Pipeline.parse_obj(item)
yield Pipeline.model_validate(item)

def get_workflows(self, pipeline_id: str) -> Iterator[Workflow]:
for item in self.paginate(f"/v2/pipeline/{pipeline_id}/workflow"):
wf = Workflow.parse_obj(item)
wf = Workflow.model_validate(item)
if self.workflow_spec.match(wf.name):
yield wf

def get_jobs(self, workflow_id: str) -> Iterator[Job]:
for item in self.paginate(f"/v2/workflow/{workflow_id}/job"):
yield Job.parse_obj(item)
yield Job.model_validate(item)

def get_artifacts(self, job_number: int) -> Iterator[Artifact]:
for item in self.paginate(f"/v2/project/gh/{self.repo}/{job_number}/artifacts"):
yield Artifact.parse_obj(item)
yield Artifact.model_validate(item)

def get_jobv1(self, job_number: int) -> Jobv1:
return Jobv1.parse_obj(
return Jobv1.model_validate(
self.client.get(f"/v1.1/project/gh/{self.repo}/{job_number}").json()
)

Expand Down
28 changes: 15 additions & 13 deletions src/tinuous/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import re
from typing import Any, Dict, List, Optional, Pattern

from pydantic import Field, validator
from pydantic.fields import ModelField
from pydantic import Field, field_validator

from .appveyor import Appveyor
from .base import CISystem, EventType, GHWorkflowSpec, NoExtraModel, WorkflowSpec
Expand All @@ -32,7 +31,8 @@ class GHPathsDict(PathsDict):
releases: Optional[str] = None

def gets_builds(self) -> bool:
return self.logs is not None or self.artifacts is not None
# <https://github.com/pydantic/pydantic/issues/8052>
return self.logs is not None or self.artifacts is not None # type: ignore[unreachable]

def gets_releases(self) -> bool:
return self.releases is not None
Expand All @@ -42,7 +42,8 @@ class CCIPathsDict(PathsDict):
artifacts: Optional[str] = None

def gets_builds(self) -> bool:
return self.logs is not None or self.artifacts is not None
# <https://github.com/pydantic/pydantic/issues/8052>
return self.logs is not None or self.artifacts is not None # type: ignore[unreachable]


class CIConfig(NoExtraModel, ABC):
Expand Down Expand Up @@ -74,8 +75,9 @@ class GitHubConfig(CIConfig):
paths: GHPathsDict = Field(default_factory=GHPathsDict)
workflows: GHWorkflowSpec = Field(default_factory=GHWorkflowSpec)

@validator("workflows", pre=True)
def _workflow_list(cls, v: Any) -> Any: # noqa: B902, U100
@field_validator("workflows", mode="before")
@classmethod
def _workflow_list(cls, v: Any) -> Any:
if isinstance(v, list):
return {"include": v}
else:
Expand Down Expand Up @@ -205,18 +207,18 @@ class Config(NoExtraModel):
allow_secrets_regex: Optional[Pattern] = Field(None, alias="allow-secrets-regex")
datalad: DataladConfig = Field(default_factory=DataladConfig)

@validator("repo")
def _validate_repo(cls, v: str) -> str: # noqa: B902, U100
@field_validator("repo")
@classmethod
def _validate_repo(cls, v: str) -> str:
if not re.fullmatch(r"[^/]+/[^/]+", v):
raise ValueError("Repo must be in the form 'OWNER/NAME'")
return v

@validator("since", "until")
def _validate_datetimes(
cls, v: Optional[datetime], field: ModelField # noqa: B902, U100
) -> Optional[datetime]:
@field_validator("since", "until")
@classmethod
def _validate_datetimes(cls, v: Optional[datetime]) -> Optional[datetime]:
if v is not None and v.tzinfo is None:
raise ValueError(f"{field.name!r} timestamp must include timezone offset")
raise ValueError("timestamps must include timezone offset")
return v

def get_since(self, state_since: Optional[datetime]) -> datetime:
Expand Down
25 changes: 11 additions & 14 deletions src/tinuous/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ def paginate(

def get_workflows(self) -> Iterator[Workflow]:
for item in self.paginate(f"/repos/{self.repo}/actions/workflows"):
wf = Workflow.parse_obj(item)
wf = Workflow.model_validate(item)
if self.workflow_spec.match(wf.path):
yield wf

def get_runs(self, wf: Workflow, since: datetime) -> Iterator[WorkflowRun]:
for item in self.paginate(f"/repos/{self.repo}/actions/workflows/{wf.id}/runs"):
r = WorkflowRun.parse_obj(item)
r = WorkflowRun.model_validate(item)
if r.created_at <= since:
break
yield r
Expand All @@ -76,7 +76,7 @@ def get_runs_for_head(self, wf: Workflow, head_sha: str) -> Iterator[WorkflowRun
f"/repos/{self.repo}/actions/workflows/{wf.id}/runs",
params={"head_sha": head_sha},
):
yield WorkflowRun.parse_obj(item)
yield WorkflowRun.model_validate(item)

def expand_committish(self, committish: str) -> str:
try:
Expand Down Expand Up @@ -220,7 +220,7 @@ def get_artifacts(self, run: WorkflowRun) -> Iterator[tuple[str, str]]:

def get_releases(self) -> Iterator[Release]:
for item in self.paginate(f"/repos/{self.repo}/releases"):
yield Release.parse_obj(item)
yield Release.model_validate(item)

def get_release_assets(self) -> Iterator[GHReleaseAsset]:
log.info("Fetching releases newer than %s", self.since)
Expand Down Expand Up @@ -395,18 +395,15 @@ def download(self, path: Path) -> list[Path]:
return list(iterfiles(target_dir))


class GHReleaseAsset(BaseModel):
# The `arbitrary_types_allowed` is for APIClient
class GHReleaseAsset(BaseModel, arbitrary_types_allowed=True):
client: APIClient
published_at: datetime
tag_name: str
commit: str
name: str
download_url: str

class Config:
# To allow APIClient:
arbitrary_types_allowed = True

def path_fields(self) -> dict[str, Any]:
utc_date = self.published_at.astimezone(timezone.utc)
return {
Expand Down Expand Up @@ -466,14 +463,14 @@ class Repository(BaseModel):

class WorkflowRun(BaseModel):
id: int
name: Optional[str]
head_branch: Optional[str]
name: Optional[str] = None
head_branch: Optional[str] = None
head_sha: str
run_number: int
run_attempt: Optional[int] = None
event: str
status: Optional[str]
conclusion: Optional[str]
status: Optional[str] = None
conclusion: Optional[str] = None
workflow_id: int
pull_requests: List[PullRequest]
created_at: datetime
Expand All @@ -494,5 +491,5 @@ class Release(BaseModel):
draft: bool
prerelease: bool
created_at: datetime
published_at: Optional[datetime]
published_at: Optional[datetime] = None
assets: List[ReleaseAsset]
6 changes: 3 additions & 3 deletions src/tinuous/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def from_file(cls, path: str | Path | None = None) -> StateFile:
if s.strip() == "":
state = State()
else:
state = State.parse_raw(s)
state = State.model_validate_json(s)
return cls(path=p, state=state, migrating=migrating)

def get_since(self, ciname: str) -> Optional[datetime]:
Expand All @@ -65,10 +65,10 @@ def set_since(self, ciname: str, since: datetime) -> None:
if self.migrating:
log.debug("Renaming old statefile %s to %s", OLD_STATE_FILE, STATE_FILE)
newpath = self.path.with_name(STATE_FILE)
newpath.write_text(self.state.json())
newpath.write_text(self.state.model_dump_json())
self.path.unlink(missing_ok=True)
self.path = newpath
self.migrating = False
else:
self.path.write_text(self.state.json())
self.path.write_text(self.state.model_dump_json())
self.modified = True
2 changes: 1 addition & 1 deletion src/tinuous/travis.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_github_commit(self, commit_sha: str) -> Optional[Commit]:
else:
raise e
else:
return Commit.parse_obj(r.json())
return Commit.model_validate(r.json())

def paginate(
self, path: str, params: Optional[dict[str, str]] = None
Expand Down
Loading
Loading