Skip to content
Snippets Groups Projects
Verified Commit e24b28e8 authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

resolve linting errors

resolve linting errors flagged by mypy
ignore mypy errors in ipam and _ipam as there modules are scheduled to be reworked at a later point in time
move setup.py functionality into pyproject.toml
parent 6e82c388
No related branches found
No related tags found
1 merge request!36Add linter tools, and resolve all linting errors
# mypy: ignore-errors
import ipaddress
from enum import Enum
from typing import Union
......
# mypy: ignore-errors
import ipaddress
from typing import Union, Optional
from typing import Optional, Union
from pydantic import BaseSettings
......
......@@ -11,7 +11,7 @@ from orchestrator.config.assignee import Assignee
from orchestrator.domain import SubscriptionModel
from orchestrator.forms import FormPage, ReadOnlyField
from orchestrator.forms.validators import Accept, Label, LongText
from orchestrator.types import State, UUIDstr, strEnum
from orchestrator.types import FormGenerator, State, UUIDstr, strEnum
from orchestrator.utils.json import json_dumps
from pydantic import validator
......@@ -36,18 +36,15 @@ class CUDOperation(strEnum):
DELETE = "DELETE"
def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operation: CUDOperation):
def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operation: CUDOperation) -> None:
"""Send a request to LSO. The callback address is derived using the process ID provided.
:param str endpoint: The LSO-specific endpoint to call, depending on the
type of service object that is acted upon.
:param dict parameters: JSON body for the request, which will almost always
at least consist of a subscription object, and a boolean value to
indicate a dry run.
:param UUIDstr process_id: The process ID that this request is a part of,
used to call back to when the execution of the playbook is completed.
:param :class:`CUDOperation` operation: The specific operation that is
performed with the request.
:param str endpoint: The LSO-specific endpoint to call, depending on the type of service object that is acted upon.
:param dict parameters: JSON body for the request, which will almost always at least consist of a subscription
object, and a boolean value to indicate a dry run.
:param UUIDstr process_id: The process ID that this request is a part of, used to call back to when the execution
of the playbook is completed.
:param :class:`CUDOperation` operation: The specific operation that is performed with the request.
"""
oss = settings.load_oss_params()
pp_params = oss.PROVISIONING_PROXY
......@@ -73,7 +70,7 @@ def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operatio
raise AssertionError(request.content)
def provision_device(subscription: DeviceProvisioning, process_id: UUIDstr, dry_run: bool = True):
def provision_device(subscription: DeviceProvisioning, process_id: UUIDstr, dry_run: bool = True) -> None:
"""Provision a new device using LSO.
:param :class:`DeviceProvisioning` subscription: The subscription object
......@@ -89,7 +86,7 @@ def provision_device(subscription: DeviceProvisioning, process_id: UUIDstr, dry_
def provision_ip_trunk(
subscription: IptrunkProvisioning, process_id: UUIDstr, config_object: str, dry_run: bool = True
):
) -> None:
"""Provision an IP trunk service using LSO.
:param :class:`IptrunkProvisioning` subscription: The subscription object
......@@ -134,7 +131,7 @@ def provision_ip_trunk(
# _send_request('ip_trunk', parameters, process_id, CUDOperation.PUT)
def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bool = True):
def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bool = True) -> None:
"""Deprovision an IP trunk service using LSO.
:param :class:`IptrunkProvisioning` subscription: The subscription object
......@@ -149,17 +146,17 @@ def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bo
@inputstep("Await provisioning proxy results", assignee=Assignee("SYSTEM"))
def await_pp_results(subscription: SubscriptionModel, label_text: str) -> State:
def await_pp_results(subscription: SubscriptionModel, label_text: str) -> FormGenerator:
class ProvisioningResultPage(FormPage):
class Config:
title = f"Deploying {subscription.product.name}..."
warning_label: Label = label_text
pp_run_results: dict = None
warning_label: Label = label_text # type: ignore
pp_run_results: dict = None # type: ignore
confirm: Accept = Accept("INCOMPLETE")
@validator("pp_run_results", allow_reuse=True, pre=True, always=True)
def run_results_must_be_given(cls, run_results):
def run_results_must_be_given(cls, run_results: dict) -> dict | None:
if run_results is None:
raise ValueError("Run results may not be empty. Wait for the provisioning proxy to finish.")
return run_results
......@@ -170,7 +167,7 @@ def await_pp_results(subscription: SubscriptionModel, label_text: str) -> State:
@inputstep("Confirm provisioning proxy results", assignee=Assignee("SYSTEM"))
def confirm_pp_results(state: State) -> State:
def confirm_pp_results(state: State) -> FormGenerator:
class ConfirmRunPage(FormPage):
class Config:
title = (
......
# mypy: ignore-errors
import requests
from gso import settings
......
......@@ -9,7 +9,7 @@ from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import done, init, step, workflow
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form
......@@ -35,10 +35,7 @@ def site_selector() -> Choice:
site_subscriptions[str(site_id)] = site_description
# noinspection PyTypeChecker
return Choice(
"Select a site",
zip(site_subscriptions.keys(), site_subscriptions.items()),
)
return Choice("Select a site", zip(site_subscriptions.keys(), site_subscriptions.items())) # type: ignore
def initial_input_form_generator(product_name: str) -> FormGenerator:
......@@ -46,7 +43,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
class Config:
title = product_name
device_site: site_selector()
device_site: site_selector() # type: ignore
hostname: str
ts_address: ipaddress.IPv4Address
ts_port: int
......@@ -68,8 +65,8 @@ def create_subscription(product: UUIDstr) -> State:
}
def iso_from_ipv4(ipv4_address):
padded_octets = [f"{x:>03}" for x in ipv4_address.split(".")]
def iso_from_ipv4(ipv4_address: ipaddress.IPv4Address) -> str:
padded_octets = [f"{x:>03}" for x in str(ipv4_address).split(".")]
joined_octets = "".join(padded_octets)
re_split = ".".join(re.findall("....", joined_octets))
return ".".join(["49.51e5.0001", re_split, "00"])
......@@ -82,7 +79,7 @@ def get_info_from_ipam(subscription: DeviceProvisioning) -> State:
lo0_addr = _ipam.allocate_service_host(hostname=lo0_name, service_type="LO", cname_aliases=[lo0_alias])
subscription.device.device_lo_ipv4_address = lo0_addr.v4
subscription.device.device_lo_ipv6_address = lo0_addr.v6
subscription.device.device_lo_iso_address = iso_from_ipv4(str(subscription.device.device_lo_ipv4_address))
subscription.device.device_lo_iso_address = iso_from_ipv4(subscription.device.device_lo_ipv4_address)
subscription.device.device_si_ipv4_network = _ipam.allocate_service_ipv4_network(
service_type="SI", comment=f"SI for {lo0_name}"
).v4
......@@ -100,13 +97,13 @@ def initialize_subscription(
subscription: device.DeviceInactive,
hostname: str,
ts_address: ipaddress.IPv4Address,
ts_port: str,
ts_port: int,
device_vendor: device_pb.DeviceVendor,
device_site: str,
device_role: device_pb.DeviceRole,
) -> State:
subscription.device.device_ts_address = str(ts_address)
subscription.device.device_ts_port = str(ts_port)
subscription.device.device_ts_port = ts_port
subscription.device.device_vendor = device_vendor
subscription.device.device_site = Site.from_subscription(device_site).site
fqdn = (
......@@ -161,7 +158,7 @@ def provision_device_real(subscription: DeviceProvisioning, process_id: UUIDstr)
initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
target=Target.CREATE,
)
def create_device():
def create_device() -> StepList:
return (
init
>> create_subscription
......
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Label
from orchestrator.targets import Target
# from orchestrator.types import SubscriptionLifecycle
from orchestrator.types import InputForm, UUIDstr
from orchestrator.workflow import done, init, step, workflow
# from orchestrator.workflows.steps import (
# resync,
# set_status,
# store_process_subscription,
# unsync,
# )
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_types.device import Device
def initial_input_form_generator(subscription_id: UUIDstr, organisation: UUIDstr) -> InputForm:
def initial_input_form_generator(subscription_id: UUIDstr) -> InputForm:
subscription = Device.from_subscription(subscription_id)
class TerminateForm(FormPage):
are_you_sure: Label = f"Are you sure you want to get facts from \
{subscription.description}?"
are_you_sure: Label = f"Are you sure you want to get facts from {subscription.description}?" # type: ignore
return TerminateForm
@step("Get facts")
def get_facts(subscription_id) -> None:
def get_facts(subscription_id: UUIDstr) -> dict:
subscription = Device.from_subscription(subscription_id)
import ansible_runner
r = ansible_runner.run(
private_data_dir="/opt",
playbook="get_facts.yaml",
inventory=subscription.device.fqdn,
)
out = r.stdout.read()
out_splitted = out.splitlines()
# import ansible_runner
#
# r = ansible_runner.run(
# private_data_dir="/opt",
# playbook="get_facts.yaml",
# inventory=subscription.device.device_fqdn,
# )
# out = r.stdout.read()
# out_splitted = out.splitlines()
return {"output": out_splitted}
return {"output": subscription}
@workflow(
......@@ -48,7 +38,7 @@ def get_facts(subscription_id) -> None:
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
target=Target.SYSTEM,
)
def get_facts_from_device():
def get_facts_from_device() -> StepList:
return (
init
>> get_facts
......
......@@ -4,8 +4,8 @@ import logging
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Label
from orchestrator.targets import Target
from orchestrator.types import SubscriptionLifecycle, UUIDstr, FormGenerator
from orchestrator.workflow import done, init, step, workflow, StepList
from orchestrator.types import FormGenerator, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
......@@ -21,9 +21,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
subscription = Device.from_subscription(subscription_id)
class TerminateForm(FormPage):
are_you_sure: Label = f"Are you sure you want to remove {subscription.description}?"
are_you_sure: Label = f"Are you sure you want to remove {subscription.description}?" # type: ignore
return TerminateForm
return TerminateForm # type: ignore
def _deprovision_in_user_management_system(fqdn: str) -> None:
......@@ -34,8 +34,8 @@ def _deprovision_in_user_management_system(fqdn: str) -> None:
@step("Deprovision loopback IPs from IPAM/DNS")
def deprovision_loopback_ips(subscription: Device) -> dict[str, V4HostAddress | V6HostAddress]:
input_host_addresses = ipam.HostAddresses(
v4=ipaddress.ip_address(subscription.device.device_lo_ipv4_address),
v6=ipaddress.ip_address(subscription.device.device_lo_ipv6_address),
v4=ipaddress.IPv4Address(subscription.device.device_lo_ipv4_address),
v6=ipaddress.IPv6Address(subscription.device.device_lo_ipv6_address),
)
fqdn_as_list = subscription.device.device_fqdn.split(".")
hostname = str(fqdn_as_list[0]) + "." + str(fqdn_as_list[1]) + "." + str(fqdn_as_list[2])
......
from uuid import uuid4
from orchestrator.db.models import ProductTable, SubscriptionTable
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, UniqueConstrainedList
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import done, init, step, workflow, StepList
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form
......@@ -49,13 +48,13 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
class AeMembersListA(UniqueConstrainedList[str]):
min_items = initial_user_input.iptrunk_minimum_links
DeviceEnumA: str = Choice("Select a device", zip(devices.keys(), devices.items()))
DeviceEnumA = Choice("Select a device", zip(devices.keys(), devices.items())) # type: ignore
class CreateIptrunkSideAForm(FormPage):
class Config:
title = "Provide subscription details for side A of the trunk."
iptrunk_sideA_node_id: DeviceEnumA
iptrunk_sideA_node_id: DeviceEnumA # type: ignore
iptrunk_sideA_ae_iface: str
iptrunk_sideA_ae_geant_a_sid: str
iptrunk_sideA_ae_members: AeMembersListA
......@@ -65,7 +64,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
# We remove the selected device for side A, to prevent any loops
devices.pop(str(user_input_side_a.iptrunk_sideA_node_id.name))
DeviceEnumB: str = Choice("Select a device", zip(devices.keys(), devices.items()))
DeviceEnumB = Choice("Select a device", zip(devices.keys(), devices.items())) # type: ignore
class AeMembersListB(UniqueConstrainedList[str]):
min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
......@@ -75,7 +74,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
class Config:
title = "Provide subscription details for side B of the trunk."
iptrunk_sideB_node_id: DeviceEnumB
iptrunk_sideB_node_id: DeviceEnumB # type: ignore
iptrunk_sideB_ae_iface: str
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: AeMembersListB
......
......@@ -4,7 +4,7 @@ from orchestrator.forms import FormPage, ReadOnlyField
from orchestrator.forms.validators import UniqueConstrainedList
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.workflow import done, init, step, workflow
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
......@@ -22,7 +22,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
geant_s_sid: str = subscription.iptrunk.geant_s_sid
iptrunk_description: str = subscription.iptrunk.iptrunk_description
iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type
iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed
iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed # type: ignore
iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links
iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric)
iptrunk_ipv4_network: ipaddress.IPv4Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv4_network)
......@@ -40,9 +40,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
iptrunk_sideA_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sideA_node.device_fqdn)
iptrunk_sideA_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sideA_ae_iface)
iptrunk_sideA_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sideA_ae_geant_a_sid
iptrunk_sideA_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sideA_ae_members
iptrunk_sideA_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sideA_ae_members # type: ignore
iptrunk_sideA_ae_members_descriptions: AeMembersListA = (
subscription.iptrunk.iptrunk_sideA_ae_members_description
subscription.iptrunk.iptrunk_sideA_ae_members_description # type: ignore
)
user_input_side_a = yield ModifyIptrunkSideAForm
......@@ -58,9 +58,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
iptrunk_sideB_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sideB_node.device_fqdn)
iptrunk_sideB_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sideB_ae_iface)
iptrunk_sideB_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sideB_ae_geant_a_sid
iptrunk_sideB_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sideB_ae_members
iptrunk_sideB_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sideB_ae_members # type: ignore
iptrunk_sideB_ae_members_descriptions: AeMembersListB = (
subscription.iptrunk.iptrunk_sideB_ae_members_description
subscription.iptrunk.iptrunk_sideB_ae_members_description # type: ignore
)
user_input_side_b = yield ModifyIptrunkSideBForm
......@@ -147,7 +147,7 @@ def provision_ip_trunk_lldp_iface_real(subscription: Iptrunk, process_id: UUIDst
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
target=Target.MODIFY,
)
def modify_generic():
def modify_generic() -> StepList:
return (
init
>> store_process_subscription(Target.MODIFY)
......
from orchestrator.forms import FormPage
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, UUIDstr
from orchestrator.workflow import done, init, step, workflow
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
......@@ -65,7 +65,7 @@ def provision_ip_trunk_isis_iface_real(subscription: Iptrunk, process_id: UUIDst
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
target=Target.MODIFY,
)
def modify_isis_metric():
def modify_isis_metric() -> StepList:
return (
init
>> store_process_subscription(Target.MODIFY)
......
......@@ -4,23 +4,24 @@ import ipaddress
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Label
from orchestrator.targets import Target
from orchestrator.types import InputForm, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import done, init, step, workflow
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_types.iptrunk import Iptrunk
from gso.services import ipam, provisioning_proxy
from gso.services.ipam import V4ServiceNetwork, V6ServiceNetwork
from gso.services.provisioning_proxy import await_pp_results, confirm_pp_results
def initial_input_form_generator(subscription_id: UUIDstr) -> InputForm:
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
subscription = Iptrunk.from_subscription(subscription_id)
class TerminateForm(FormPage):
are_you_sure: Label = f"Are you sure you want to remove {subscription.description}?"
are_you_sure: Label = f"Are you sure you want to remove {subscription.description}?" # type: ignore
return TerminateForm
return TerminateForm # type: ignore
@step("Set iptrunk ISIS metric to 9000")
......@@ -63,7 +64,7 @@ def deprovision_ip_trunk_real(subscription: Iptrunk, process_id: UUIDstr) -> Sta
@step("Deprovision IPv4 networks")
def deprovision_ip_trunk_ipv4(subscription: Iptrunk) -> None:
def deprovision_ip_trunk_ipv4(subscription: Iptrunk) -> dict[str, V4ServiceNetwork | V6ServiceNetwork]:
service_network = ipam.delete_service_network(
network=ipaddress.ip_network(subscription.iptrunk.iptrunk_ipv4_network),
service_type="TRUNK",
......@@ -72,7 +73,7 @@ def deprovision_ip_trunk_ipv4(subscription: Iptrunk) -> None:
@step("Deprovision IPv6 networks")
def deprovision_ip_trunk_ipv6(subscription: Iptrunk) -> None:
def deprovision_ip_trunk_ipv6(subscription: Iptrunk) -> dict[str, V4ServiceNetwork | V6ServiceNetwork]:
service_network = ipam.delete_service_network(
network=ipaddress.ip_network(subscription.iptrunk.iptrunk_ipv6_network),
service_type="TRUNK",
......@@ -85,7 +86,7 @@ def deprovision_ip_trunk_ipv6(subscription: Iptrunk) -> None:
initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
target=Target.TERMINATE,
)
def terminate_iptrunk():
def terminate_iptrunk() -> StepList:
return (
init
>> store_process_subscription(Target.TERMINATE)
......
......@@ -3,7 +3,7 @@ from uuid import uuid4
from orchestrator.forms import FormPage
from orchestrator.targets import Target
from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
from orchestrator.workflow import done, init, step, workflow, StepList
from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, set_status, store_process_subscription
from orchestrator.workflows.utils import wrap_create_initial_input_form
......
[project]
name = "geant-service-orchestrator"
dynamic = ['version']
description = "GEANT Service Orchestrator"
authors = [
{name = "GEANT", email = "swd@geant.org"}
]
dependencies = [
"orchestrator-core==1.0.0",
"pydantic",
"requests",
]
requires-python = ">=3.9,<3.12"
[tool.setuptools]
packages = ["gso"]
[tool.isort]
profile = "black"
line_length = 120
......@@ -17,7 +34,6 @@ exclude = '''
| \.tox
| venv
| gso/migrations
| gso/services/_ipam\.py
)/
)
'''
......@@ -25,8 +41,7 @@ exclude = '''
[tool.mypy]
exclude = [
"venv",
"test/*",
"gso/services/_ipam.py" # TODO: remove
"test/*"
]
ignore_missing_imports = true
disallow_untyped_calls = true
......@@ -70,6 +85,8 @@ ignore = [
"D106",
"D107",
"D202",
"D203",
"D213",
"E501",
"N806",
"B905",
......
from setuptools import find_packages, setup
setup(
name="geant-service-orchestrator",
version="0.1",
author="GEANT",
author_email="swd@geant.org",
description="GEANT Service Orchestrator",
url="https://gitlab.geant.org/goat/geant-service-orchestrator",
packages=find_packages(),
install_requires=[
"orchestrator-core==1.0.0",
"pydantic",
"requests",
],
)
[flake8]
ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202,E501,RST301,RST304,W503,E203,C417,T202
ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202,E501,RST301,RST304,W503,E203,C417,T202,S101
; extend-ignore = E203
exclude = .git,.*_cache,.eggs,*.egg-info,__pycache__,venv,.tox,gso/migrations
enable-extensions = G
......@@ -9,7 +9,6 @@ ban-relative-imports = true
per-file-ignores =
# Allow first argument to be cls instead of self for pydantic validators
gso/*: B902
test/*: S101
[testenv]
deps =
......@@ -19,6 +18,7 @@ deps =
mypy
ruff
isort
types-requests
-r requirements.txt
commands =
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment