Skip to content

Commit 4bec539

Browse files
jens-kuertenJens Kürten
and
Jens Kürten
authored
Feat: Add MyPy support (#20)
* add mypy * mark package as typed --------- Co-authored-by: Jens Kürten <[email protected]>
1 parent ec1886e commit 4bec539

16 files changed

+73
-81
lines changed

.pre-commit-config.yaml

+11-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@ repos:
1111
args: [ --fix ]
1212
# Run the formatter.
1313
- id: ruff-format
14-
14+
- repo: https://github.com/pre-commit/mirrors-mypy
15+
rev: 'v1.15.0'
16+
hooks:
17+
- id: mypy
18+
additional_dependencies:
19+
[
20+
types-mock,
21+
types-requests,
22+
types-PyYAML,
23+
pydantic
24+
]
1525
- repo: https://github.com/pre-commit/pre-commit-hooks
1626
rev: v4.4.0
1727
hooks:

csfunctions/actions/dummy.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,4 @@ class DummyAction(BaseAction):
88
Dummy Action, for unit testing
99
"""
1010

11-
def __init__(self, id: str, **kwargs): # pylint: disable=redefined-builtin
12-
super().__init__(name=ActionNames.DUMMY, id=id, data=kwargs["data"])
13-
14-
name: Literal[ActionNames.DUMMY]
11+
name: Literal[ActionNames.DUMMY] = ActionNames.DUMMY

csfunctions/events/dummy.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,5 @@ class DummyEvent(BaseEvent):
1717
Dummy Event, for unit testing
1818
"""
1919

20-
def __init__(self, event_id: str, data: DummyEventData, **_):
21-
super().__init__(name=EventNames.DUMMY, event_id=event_id, data=data)
22-
23-
name: Literal[EventNames.DUMMY]
24-
data: DummyEventData = Field([])
20+
name: Literal[EventNames.DUMMY] = EventNames.DUMMY
21+
data: DummyEventData = Field(..., description="Dummy Event Data")

csfunctions/events/workflow_task_trigger.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,5 @@ class WorkflowTaskTriggerEventData(BaseModel):
1818

1919

2020
class WorkflowTaskTriggerEvent(BaseEvent):
21-
def __init__(self, event_id: str, data: WorkflowTaskTriggerEventData, **_):
22-
super().__init__(name=EventNames.WORKFLOW_TASK_TRIGGER, event_id=event_id, data=data)
23-
24-
name: Literal[EventNames.WORKFLOW_TASK_TRIGGER]
21+
name: Literal[EventNames.WORKFLOW_TASK_TRIGGER] = EventNames.WORKFLOW_TASK_TRIGGER
2522
data: WorkflowTaskTriggerEventData

csfunctions/handler.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from typing import Callable
77

88
import yaml
9-
from pydantic import BaseModel
109

1110
from csfunctions import ErrorResponse, Event, Request, WorkloadResponse
1211
from csfunctions.actions import ActionUnion
1312
from csfunctions.config import ConfigModel, FunctionModel
13+
from csfunctions.events import EventData
1414
from csfunctions.objects import BaseObject
1515
from csfunctions.response import ResponseUnion
1616
from csfunctions.service import Service
@@ -31,7 +31,7 @@ def _get_function(function_name: str, function_dir: str) -> FunctionModel:
3131
config = _load_config(function_dir)
3232
func = next(func for func in config.functions if func.name == function_name)
3333
if not func:
34-
raise ValueError(f"Could not find function with name { function_name} in the environment.yaml.")
34+
raise ValueError(f"Could not find function with name {function_name} in the environment.yaml.")
3535
return func
3636

3737

@@ -53,7 +53,7 @@ def link_objects(event: Event):
5353
e.g. document.part
5454
"""
5555
data = getattr(event, "data", None)
56-
if not isinstance(data, BaseModel):
56+
if data is None or not isinstance(data, EventData): # type: ignore # MyPy doesn't like PEP604
5757
return
5858

5959
# we expect all objects to be passed in Event.data
@@ -81,24 +81,26 @@ def execute(function_name: str, request_body: str, function_dir: str = "src") ->
8181
try:
8282
request = Request(**json.loads(request_body))
8383
link_objects(request.event)
84+
8485
function_callback = get_function_callable(function_name, function_dir)
85-
service = Service(str(request.metadata.service_url), request.metadata.service_token)
86+
service = Service(
87+
str(request.metadata.service_url) if request.metadata.service_url else None, request.metadata.service_token
88+
)
8689

8790
response = function_callback(request.metadata, request.event, service)
8891

8992
if response is None:
9093
return ""
9194

92-
if isinstance(response, ActionUnion):
95+
if isinstance(response, ActionUnion): # type: ignore # MyPy doesn't like PEP604
9396
# wrap returned Actions into a WorkloadResponse
9497
response = WorkloadResponse(actions=[response])
95-
elif isinstance(response, list) and all(isinstance(o, ActionUnion) for o in response):
98+
elif isinstance(response, list) and all(isinstance(o, ActionUnion) for o in response): # type: ignore # MyPy doesn't like PEP604
9699
# wrap list of Actions into a WorkloadResponse
97100
response = WorkloadResponse(actions=response)
98101

99-
if not isinstance(
100-
response, ResponseUnion
101-
): # need to check for ResponseUnion instead of Response, because isinstance doesn't work with annotated unions
102+
if not isinstance(response, ResponseUnion): # type: ignore # MyPy doesn't like PEP604
103+
# need to check for ResponseUnion instead of Response, because isinstance doesn't work with annotated unions
102104
raise ValueError("Function needs to return a Response object or None.")
103105

104106
# make sure the event_id is filled out correctly

csfunctions/metadata.py

+2-23
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,10 @@
11
from datetime import datetime
2+
from typing import Optional
23

34
from pydantic import AnyHttpUrl, BaseModel, Field
45

56

67
class MetaData(BaseModel):
7-
def __init__(
8-
self,
9-
app_lang: str,
10-
app_user: str,
11-
request_id: str,
12-
request_datetime: datetime,
13-
transaction_id: str,
14-
instance_url: str,
15-
db_service_url: str | None = None,
16-
**kwargs,
17-
):
18-
super().__init__(
19-
app_lang=app_lang,
20-
app_user=app_user,
21-
request_id=request_id,
22-
db_service_url=db_service_url,
23-
request_datetime=request_datetime,
24-
transaction_id=transaction_id,
25-
instance_url=instance_url,
26-
**kwargs,
27-
)
28-
298
app_lang: str = Field(..., description="ISO code of the session language that triggered the webhook.")
309
app_user: str = Field(..., description="User id of the user that triggered the webhook. (personalnummer)")
3110
request_id: str = Field(..., description="Unique identifier of this request.")
@@ -34,6 +13,6 @@ def __init__(
3413
request_datetime: datetime = Field(..., description="Time when the request was started.")
3514
transaction_id: str = Field(..., description="Unique identifier of the transaction.")
3615
instance_url: AnyHttpUrl = Field(..., description="URL to the instance where the webhook was triggered.")
37-
db_service_url: AnyHttpUrl | None = Field(
16+
db_service_url: Optional[AnyHttpUrl] = Field(
3817
None, description="URL to the DB Access Service responsible for the instance."
3918
)

csfunctions/objects/classification.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class ObjectPropertyValue(BaseObject):
1616
ref_object_id: str = Field(..., description="Referenced Object")
1717
boolean_value: int | None = Field(..., description="Boolean Value")
1818
datetime_value: datetime | None = Field(..., description="Datetime Value")
19-
float_value: float | None = Field("", description="Float Value")
20-
float_value_normalized: float | None = Field("", description="Float Value Normalized")
21-
integer_value: int | None = Field("", description="Integer Value")
19+
float_value: float | None = Field(None, description="Float Value")
20+
float_value_normalized: float | None = Field(None, description="Float Value Normalized")
21+
integer_value: int | None = Field(None, description="Integer Value")
2222
iso_language_code: str | None = Field(None, description="ISO Language Code")
23-
value_pos: int | None = Field("", description="Position")
23+
value_pos: int | None = Field(None, description="Position")
2424
property_code: str | None = Field("", description="Property Code")
2525
property_path: str | None = Field(None, description="Property Path")
2626
property_type: str | None = Field(None, description="Property Type")

csfunctions/objects/document.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,4 @@ class CADDocument(Document):
9898
Special Document type that contains a CAD-Model.
9999
"""
100100

101-
object_type: Literal[ObjectType.CAD_DOCUMENT] = ObjectType.CAD_DOCUMENT
101+
object_type: Literal[ObjectType.CAD_DOCUMENT] = ObjectType.CAD_DOCUMENT # type: ignore[assignment]

csfunctions/objects/workflow.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ class Workflow(BaseObject):
2525
global_briefcases: list[Briefcase] = Field([], exclude=True)
2626

2727
def link_objects(self, data: "EventData"):
28-
local_briefcases = getattr(data, "local_briefcases", None)
29-
global_briefcases = getattr(data, "local_briefcases", None)
28+
local_briefcases: list[Briefcase] | None = getattr(data, "local_briefcases", None)
29+
global_briefcases: list[Briefcase] | None = getattr(data, "local_briefcases", None)
3030

3131
if local_briefcases and self.local_briefcase_ids:
3232
self._link_local_briefcases(local_briefcases)
3333

3434
if global_briefcases and self.global_briefcase_ids:
35-
self._link_global_briefcases(local_briefcases)
35+
self._link_global_briefcases(global_briefcases)
3636

3737
def _link_local_briefcases(self, local_briefcases: list["Briefcase"]):
3838
for local_briefcase in local_briefcases:

csfunctions/py.typed

Whitespace-only changes.

csfunctions/service.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
import requests
24

35

@@ -6,7 +8,7 @@ class Service:
68
Provides access to services on the elements instance, e.g. generating numbers.
79
"""
810

9-
def __init__(self, service_url: str, service_token: str):
11+
def __init__(self, service_url: str | None, service_token: str | None):
1012
self.generator = NumberGeneratorService(service_url, service_token)
1113

1214

@@ -23,7 +25,7 @@ def __init__(self, service_url: str | None, service_token: str | None):
2325
self.service_url = service_url
2426
self.service_token = service_token
2527

26-
def request(self, endpoint: str, method: str = "GET", params: dict = None) -> dict | list:
28+
def request(self, endpoint: str, method: str = "GET", params: Optional[dict] = None) -> dict | list:
2729
"""
2830
Make a request to the access service.
2931
"""
@@ -35,7 +37,7 @@ def request(self, endpoint: str, method: str = "GET", params: dict = None) -> di
3537
headers = {"Authorization": f"Bearer {self.service_token}"}
3638
params = params or {}
3739
url = self.service_url.rstrip("/") + "/" + endpoint.lstrip("/")
38-
response = requests.request(method, url=url, params=params, headers=headers)
40+
response = requests.request(method, url=url, params=params, headers=headers, timeout=10)
3941

4042
if response.status_code == 401:
4143
raise Unauthorized
@@ -78,4 +80,8 @@ def get_numbers(self, name: str, count: int) -> list[int]:
7880
"""
7981
params = {"name": name, "count": count}
8082
data = self.request(self.endpoint, params=params)
83+
if not isinstance(data, dict):
84+
raise ValueError(f"Access service returned invalid data. Expected dict, got {type(data)}")
85+
if "numbers" not in data:
86+
raise ValueError(f"Access service returned invalid data. Expected 'numbers' key, got {data.keys()}")
8187
return data["numbers"]

json_schemas/request.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,7 @@
882882
"properties": {
883883
"name": {
884884
"const": "dummy",
885+
"default": "dummy",
885886
"title": "Name",
886887
"type": "string"
887888
},
@@ -892,12 +893,12 @@
892893
},
893894
"data": {
894895
"$ref": "#/$defs/DummyEventData",
895-
"default": []
896+
"description": "Dummy Event Data"
896897
}
897898
},
898899
"required": [
899-
"name",
900-
"event_id"
900+
"event_id",
901+
"data"
901902
],
902903
"title": "DummyEvent",
903904
"type": "object"
@@ -2666,6 +2667,7 @@
26662667
"properties": {
26672668
"name": {
26682669
"const": "workflow_task_trigger",
2670+
"default": "workflow_task_trigger",
26692671
"title": "Name",
26702672
"type": "string"
26712673
},
@@ -2679,7 +2681,6 @@
26792681
}
26802682
},
26812683
"required": [
2682-
"name",
26832684
"event_id",
26842685
"data"
26852686
],

json_schemas/workload_response.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"properties": {
3636
"name": {
3737
"const": "dummy",
38+
"default": "dummy",
3839
"title": "Name",
3940
"type": "string"
4041
},
@@ -51,9 +52,6 @@
5152
"title": "Id"
5253
}
5354
},
54-
"required": [
55-
"name"
56-
],
5755
"title": "DummyAction",
5856
"type": "object"
5957
}

tests/test_service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class TestNumberGeneratorService(TestCase):
1010
endpoint = "numgen"
1111
service_url = "https://some_service_url"
1212
service_token = "some_service_token" # nosec
13-
service = None
13+
service: Service
1414

1515
@classmethod
1616
def setUpClass(cls) -> None:

tests/test_workloadresponse.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class TestWorkloadResponse(TestCase):
10-
def test_discriminator(self):
10+
def test_discriminator(self) -> None:
1111
"""
1212
Test that the discriminator on action objects (and responses) works
1313
"""

tests/utils.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from csfunctions import DataResponse, MetaData, Request, Service
44
from csfunctions.actions import AbortAndShowErrorAction
55
from csfunctions.events import DummyEvent
6+
from csfunctions.events.dummy import DummyEventData
67
from csfunctions.objects import Document, EngineeringChange, Part
78

89

@@ -35,8 +36,8 @@ def action_list_function(*args, **kwargs): # pylint: disable=unused-argument
3536
return [AbortAndShowErrorAction(message="Testerror"), AbortAndShowErrorAction(message="Testerror")]
3637

3738

38-
dummy_document = Document(
39-
**{
39+
dummy_document = Document.model_validate(
40+
{
4041
"object_type": "document",
4142
"z_nummer": "D000017",
4243
"z_index": "a",
@@ -86,8 +87,8 @@ def action_list_function(*args, **kwargs): # pylint: disable=unused-argument
8687
}
8788
)
8889

89-
dummy_part = Part(
90-
**{
90+
dummy_part = Part.model_validate(
91+
{
9192
"object_type": "part",
9293
"teilenummer": "000000",
9394
"t_index": "a",
@@ -140,21 +141,25 @@ def action_list_function(*args, **kwargs): # pylint: disable=unused-argument
140141
)
141142

142143
dummy_request = Request(
143-
metadata=MetaData(
144-
request_id="123",
145-
app_lang="de",
146-
app_user="caddok",
147-
request_datetime=datetime(2000, 1, 1),
148-
transaction_id="123asd",
149-
instance_url="https://instance.contact-cloud.com",
150-
service_url=None,
144+
metadata=MetaData.model_validate(
145+
{
146+
"request_id": "123",
147+
"app_lang": "de",
148+
"app_user": "caddok",
149+
"request_datetime": datetime(2000, 1, 1),
150+
"transaction_id": "123asd",
151+
"instance_url": "https://instance.contact-cloud.com",
152+
"service_url": None,
153+
"service_token": "123",
154+
"db_service_url": None,
155+
}
151156
),
152-
event=DummyEvent(event_id="42", data={"documents": [dummy_document], "parts": [dummy_part]}),
157+
event=DummyEvent(event_id="42", data=DummyEventData(documents=[dummy_document], parts=[dummy_part])),
153158
)
154159

155160

156-
dummy_ec = EngineeringChange(
157-
**{
161+
dummy_ec = EngineeringChange.model_validate(
162+
{
158163
"object_type": "engineering_change",
159164
"cdb_ec_id": "EC00000005",
160165
"cdb_project_id": "",

0 commit comments

Comments
 (0)