Skip to content

Commit 2a9e638

Browse files
Add mypy strict type checking with pre-commit and CI integration
1 parent d4a939d commit 2a9e638

6 files changed

Lines changed: 178 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ jobs:
2020
- name: Lint
2121
run: uv run ruff check
2222

23+
- name: Type check
24+
run: uv run mypy src/linear_python_client
25+
2326
test:
2427
runs-on: ubuntu-latest
2528
steps:

.pre-commit-config.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.15.16
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format
8+
9+
- repo: local
10+
hooks:
11+
- id: mypy
12+
name: mypy
13+
entry: uv run mypy
14+
args: [src/linear_python_client]
15+
language: system
16+
pass_filenames: false
17+
types: [python]

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ build-backend = "uv_build"
3636

3737
[dependency-groups]
3838
dev = [
39+
"mypy>=1.11",
3940
"pytest>=8.0",
4041
"pytest-cov>=5.0",
4142
"respx>=0.21",
@@ -62,6 +63,11 @@ exclude_also = ["if __name__ == .__main__.:"]
6263

6364
[tool.ruff]
6465
line-length = 100
66+
exclude = ["tests", "scripts"]
6567

6668
[tool.ruff.lint]
6769
select = ["E", "F", "I", "UP", "B"]
70+
71+
[tool.mypy]
72+
strict = true
73+
exclude = ["tests/", "scripts/"]

src/linear_python_client/client.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,9 @@ def _raise_for_errors(self, errors: list[dict[str, Any]], response: httpx.Respon
320320
complexity_reset=_to_int(h.get("X-RateLimit-Complexity-Reset")),
321321
query_complexity=_to_int(h.get("X-Complexity")),
322322
endpoint_requests_limit=_to_int(h.get("X-RateLimit-Endpoint-Requests-Limit")),
323-
endpoint_requests_remaining=_to_int(h.get("X-RateLimit-Endpoint-Requests-Remaining")),
323+
endpoint_requests_remaining=_to_int(
324+
h.get("X-RateLimit-Endpoint-Requests-Remaining")
325+
),
324326
endpoint_requests_reset=_to_int(h.get("X-RateLimit-Endpoint-Requests-Reset")),
325327
endpoint_name=h.get("X-RateLimit-Endpoint-Name"),
326328
)
@@ -334,10 +336,10 @@ def _raise_for_errors(self, errors: list[dict[str, Any]], response: httpx.Respon
334336
def _lookup_team(self, name_or_key: str) -> str:
335337
"""Resolve a team display name or key to a UUID."""
336338
resp = self.find_team(FindTeamRequest(name=name_or_key))
337-
if resp.team is not None:
339+
if resp.team is not None and resp.team.id is not None:
338340
return resp.team.id
339341
resp = self.find_team(FindTeamRequest(key=name_or_key))
340-
if resp.team is not None:
342+
if resp.team is not None and resp.team.id is not None:
341343
return resp.team.id
342344
raise ValueError(f"Team not found: {name_or_key!r}")
343345

@@ -349,30 +351,28 @@ def _lookup_user(self, name_or_email: str) -> str:
349351
else FindUserRequest(name=name_or_email)
350352
)
351353
user = self.find_user(req).user
352-
if user is None:
354+
if user is None or user.id is None:
353355
raise ValueError(f"User not found: {name_or_email!r}")
354356
return user.id
355357

356358
def _lookup_project(self, name: str) -> str:
357359
"""Resolve a project name to a UUID."""
358360
project = self.find_project(FindProjectRequest(name=name)).project
359-
if project is None:
361+
if project is None or project.id is None:
360362
raise ValueError(f"Project not found: {name!r}")
361363
return project.id
362364

363365
def _lookup_label(self, name: str, *, team_id: str | None = None) -> str:
364366
"""Resolve a label name to a UUID, optionally scoped to a team."""
365367
label = self.find_label(FindLabelRequest(name=name, team_id=team_id)).label
366-
if label is None:
368+
if label is None or label.id is None:
367369
raise ValueError(f"Label not found: {name!r}")
368370
return label.id
369371

370372
def _lookup_workflow_state(self, name: str, team_id: str) -> str:
371373
"""Resolve a workflow state name to a UUID within a team."""
372-
state = self.find_workflow_state(
373-
FindWorkflowStateRequest(team_id=team_id, name=name)
374-
).state
375-
if state is None:
374+
state = self.find_workflow_state(FindWorkflowStateRequest(team_id=team_id, name=name)).state
375+
if state is None or state.id is None:
376376
raise ValueError(f"Workflow state {name!r} not found in team {team_id!r}")
377377
return state.id
378378

src/linear_python_client/models/responses.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from __future__ import annotations
1010

11+
from collections.abc import Iterator
12+
1113
from pydantic import Field
1214

1315
from .entities import (
@@ -34,7 +36,7 @@ class ConnectionResponse[NodeT](LinearModel):
3436
nodes: list[NodeT] = Field(default_factory=list)
3537
page_info: PageInfo = Field(default_factory=PageInfo)
3638

37-
def __iter__(self):
39+
def __iter__(self) -> Iterator[NodeT]: # type: ignore[override]
3840
"""Iterate over `nodes`."""
3941
return iter(self.nodes)
4042

0 commit comments

Comments
 (0)