Skip to content

Commit

Permalink
Merge pull request #464 from lyz-code/fix/remove-mypy-typedict-depend…
Browse files Browse the repository at this point in the history
…ency

fix: remove mypy-typedict dependency using dataclasses for models
  • Loading branch information
lyz-code committed May 23, 2022
2 parents 9ba19d1 + e477aeb commit 8ebbdd7
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 295 deletions.
2 changes: 1 addition & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ test = [
"pytest-cov>=3.0.0",
"pytest-xdist>=2.4.0",
"pytest-freezegun>=0.4.2",
"pydantic-factories>=0.5.0",
"pydantic-factories>=1.2.9",
"requests-mock>=1.9.3",
]
doc = [
Expand Down
38 changes: 23 additions & 15 deletions src/drode/adapters/drone.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Gather the integration with the Drone web application."""

import logging
from typing import Any, List
from dataclasses import dataclass
from typing import Any, List, Optional

import requests
from mypy_extensions import TypedDict

log = logging.getLogger(__name__)

Expand All @@ -25,12 +25,14 @@ class DronePromoteError(Exception):
"""Exception to gather job promotion errors."""


class BuildInfo(TypedDict, total=False):
@dataclass
# R0902: Too many attributes, but it's a model, so it doesn't mind
class BuildInfo: # noqa: R0902
"""Build information schema."""

# VNE003: variables should not shadow builtins. As we're defining just the schema
# of a dictionary we can safely ignore it.
id: int # noqa: VNE003
id: int # noqa: VNE003, C0103
status: str
number: int
trigger: str
Expand All @@ -39,10 +41,14 @@ class BuildInfo(TypedDict, total=False):
source: str
after: str
target: str
author_name: str
deploy_to: str
started: int
finished: int
parent: Optional[int]
before: Optional[str]
author_login: Optional[str]
author_name: Optional[str]
sender: Optional[str]
stages: List[Any]


Expand Down Expand Up @@ -91,9 +97,10 @@ def build_info(self, project_pipeline: str, build_number: int) -> BuildInfo:
info: build information.
"""
try:
return self.get(
build_data = self.get(
f"{self.drone_url}/api/repos/{project_pipeline}/builds/{build_number}"
).json()
).json()[0]
return BuildInfo(**build_data)
except DroneAPIError as error:
raise DroneBuildError(
f"The build {build_number} was not found at "
Expand Down Expand Up @@ -152,9 +159,10 @@ def last_build_info(self, project_pipeline: str) -> BuildInfo:
Returns:
info: Last build information.
"""
return self.get(f"{self.drone_url}/api/repos/{project_pipeline}/builds").json()[
0
]
build_data = self.get(
f"{self.drone_url}/api/repos/{project_pipeline}/builds"
).json()[0]
return BuildInfo(**build_data)

def last_success_build_info(
self, project_pipeline: str, branch: str = "master"
Expand All @@ -173,13 +181,13 @@ def last_success_build_info(
f"{self.drone_url}/api/repos/{project_pipeline}/builds"
).json()

for build in build_history:
for build_data in build_history:
if (
build["status"] == "success"
and build["target"] == branch
and build["event"] == "push"
build_data["status"] == "success"
and build_data["target"] == branch
and build_data["event"] == "push"
):
return build
return BuildInfo(**build_data)
raise DroneBuildError(
f"There are no successful jobs with target branch {branch}"
)
Expand Down
43 changes: 18 additions & 25 deletions src/drode/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,25 @@ def wait(
"""
if build_number is None:
last_build = drone.last_build_info(project_pipeline)
if last_build["finished"] != 0:
if last_build.finished != 0:
log.info("There are no active jobs")
return True
last_build_number = last_build["number"]
build_number = last_build_number
build_number = last_build.number

first_time = True
while True:
build = drone.build_info(project_pipeline, build_number)

try:
if build["finished"] == 0:
if first_time:
log.info(
f"Waiting for job #{build['number']} started by "
f"a {build['event']} event by {build['trigger']}."
)
first_time = False
time.sleep(1)
continue
log.info(
f"Job #{build['number']} has finished with status {build['status']}"
)
except KeyError:
log.info(f"Job #{build_number} has not started yet")
if build.finished == 0:
if first_time:
log.info(
f"Waiting for job #{build.number} started by "
f"a {build.event} event by {build.trigger}."
)
first_time = False
time.sleep(1)
continue
log.info(f"Job #{build.number} has finished with status {build.status}")
return True


Expand Down Expand Up @@ -149,23 +143,22 @@ def promote(
"""
if build_number is None:
build = drone.last_success_build_info(project_pipeline)
build_number = build["number"]
else:
build = drone.build_info(project_pipeline, build_number)

if build["status"] != "success":
if build.status != "success":
raise DronePromoteError(
f"You can't promote job #{build_number} to {environment} "
f"as it's status is {build['status']}"
f"You can't promote job #{build.number} to {environment} "
f"as it's status is {build.status}"
)

log.info(
f"You're about to promote job #{build_number} "
f"You're about to promote job #{build.number} "
f"of the pipeline {project_pipeline} to {environment}\n\n"
f" With commit {build['after'][:8]}: {build['message']}"
f" With commit {build.after[:8]}: {build.message}"
)
if ask("Are you sure? [y/N]: "):
return drone.promote(project_pipeline, build_number, environment)
return drone.promote(project_pipeline, build.number, environment)
return None


Expand Down
117 changes: 66 additions & 51 deletions tests/e2e/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from drode.entrypoints.cli import cli
from drode.version import __version__

from ..factories import BuildInfoFactory

FakeDeps = TypedDict("FakeDeps", {"drone": FakeDrone, "aws": FakeAWS})


Expand Down Expand Up @@ -174,7 +176,16 @@ def test_wait_subcommand_calls_wait_service(
Then: The service wait is called and it informs that there are no active jobs,
raising the terminal bell when finished.
"""
fake_dependencies["drone"].set_builds({209: [{"number": 209, "finished": 1}]})
fake_dependencies["drone"].set_builds(
{
209: [
BuildInfoFactory.build(
number=209,
finished=1,
),
]
}
)

result = runner.invoke(cli, ["wait"], obj=fake_dependencies)

Expand All @@ -195,16 +206,20 @@ def test_wait_subcommand_accepts_build_number_argument(
When: The wait command is called with a non existent build number.
Then: The service wait is called with the build number.
"""
fake_dependencies["drone"].set_builds({209: [{"number": 209}]})
fake_dependencies["drone"].set_builds(
{
209: [
BuildInfoFactory.build(
number=209,
finished=1,
),
]
}
)

result = runner.invoke(cli, ["wait", "209"], obj=fake_dependencies)

assert result.exit_code == 0
assert (
"drode.services",
logging.INFO,
"Job #209 has not started yet",
) in caplog.record_tuples


def test_wait_subcommand_handles_unhappy_path(
Expand All @@ -215,9 +230,15 @@ def test_wait_subcommand_handles_unhappy_path(
When: The wait subcommand is called with an inexistent job number.
Then: The exception is handled gracefully.
"""
# ignore: we know that the type of number is wrong, we want to raise the exception.
fake_dependencies["drone"].set_builds(
{209: [{"number": "invalid", "finished": 1}]} # type: ignore
{
209: [
BuildInfoFactory.build(
number="invalid",
finished=1,
),
]
}
)

result = runner.invoke(cli, ["wait", "1"], obj=fake_dependencies)
Expand Down Expand Up @@ -268,24 +289,24 @@ def test_promote_happy_path(
fake_dependencies["drone"].set_builds(
{
208: [
{
"number": 208,
"finished": 1,
"target": "master",
"status": "success",
"after": "9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
"message": "updated README",
"event": "push",
},
{
"number": 208,
"finished": 1,
"target": "master",
"status": "success",
"after": "9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
"message": "updated README",
"event": "push",
},
BuildInfoFactory.build(
number=208,
finished=1,
target="master",
status="success",
after="9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
message="updated README",
event="push",
),
BuildInfoFactory.build(
number=208,
finished=1,
target="master",
status="success",
after="9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
message="updated README",
event="push",
),
],
}
)
Expand Down Expand Up @@ -319,24 +340,24 @@ def test_promote_happy_path_with_wait_flag(
fake_dependencies["drone"].set_builds(
{
208: [
{
"number": 208,
"finished": 1,
"target": "master",
"status": "success",
"after": "9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
"message": "updated README",
"event": "push",
},
{
"number": 208,
"finished": 1,
"target": "master",
"status": "success",
"after": "9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
"message": "updated README",
"event": "push",
},
BuildInfoFactory.build(
number=208,
finished=1,
target="master",
status="success",
after="9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
message="updated README",
event="push",
),
BuildInfoFactory.build(
number=208,
finished=1,
target="master",
status="success",
after="9fc1ad6ebf12462f3f9773003e26b4c6f54a772e",
message="updated README",
event="push",
),
],
}
)
Expand All @@ -354,12 +375,6 @@ def test_promote_happy_path_with_wait_flag(
"You're about to promote job #208 of the pipeline test_projects/webpage to "
"production\n\n With commit 9fc1ad6e: updated README",
) in caplog.record_tuples
# Assert we are waiting for the promote build
assert (
"drode.services",
logging.INFO,
"Job #209 has not started yet",
) in caplog.record_tuples


def test_promote_unhappy_path(
Expand Down
13 changes: 13 additions & 0 deletions tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Define the factories of the program."""

from typing import Any

from pydantic_factories import ModelFactory

from drode.adapters.drone import BuildInfo


class BuildInfoFactory(ModelFactory[Any]):
"""Define factory for the BuildInfo model."""

__model__ = BuildInfo

0 comments on commit 8ebbdd7

Please sign in to comment.