Skip to content

Commit 8d2901b

Browse files
authored
chore: add type checking, CI workflow, fix type errors (#15)
1 parent 6d12d92 commit 8d2901b

51 files changed

Lines changed: 558 additions & 1181 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
lint:
11+
name: Lint & Format
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v4
19+
with:
20+
version: "latest"
21+
22+
- name: Setup Python
23+
run: uv python install 3.12
24+
25+
- name: Install dependencies
26+
run: uv sync --all-extras
27+
28+
- name: Check formatting
29+
run: uv run ruff format --check drift/ tests/
30+
31+
- name: Lint
32+
run: uv run ruff check drift/ tests/
33+
34+
typecheck:
35+
name: Type Check
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v4
40+
41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v4
43+
with:
44+
version: "latest"
45+
46+
- name: Setup Python
47+
run: uv python install 3.12
48+
49+
- name: Install dependencies
50+
run: uv sync --all-extras
51+
52+
- name: Type check
53+
run: uv run ty check drift/ tests/
54+
55+
test:
56+
name: Unit Tests
57+
runs-on: ubuntu-latest
58+
steps:
59+
- name: Checkout
60+
uses: actions/checkout@v4
61+
62+
- name: Install uv
63+
uses: astral-sh/setup-uv@v4
64+
with:
65+
version: "latest"
66+
67+
- name: Setup Python
68+
run: uv python install 3.12
69+
70+
- name: Install dependencies
71+
run: uv sync --all-extras
72+
73+
- name: Run unit tests
74+
run: uv run pytest tests/unit/ -v
75+
76+
build:
77+
name: Build Package
78+
runs-on: ubuntu-latest
79+
steps:
80+
- name: Checkout
81+
uses: actions/checkout@v4
82+
83+
- name: Install uv
84+
uses: astral-sh/setup-uv@v4
85+
with:
86+
version: "latest"
87+
88+
- name: Setup Python
89+
run: uv python install 3.12
90+
91+
- name: Install dependencies
92+
run: uv sync --all-extras
93+
94+
- name: Build package
95+
run: uv build
96+
97+
- name: Verify package can be installed
98+
run: |
99+
uv venv /tmp/test-install
100+
uv pip install dist/*.whl --python /tmp/test-install/bin/python
101+
/tmp/test-install/bin/python -c "import drift; print('Package imported successfully')"
102+

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ uv sync --all-extras
2121
This project uses [ruff](https://docs.astral.sh/ruff/) for linting/formatting and [ty](https://github.com/astral-sh/ty) for type checking.
2222

2323
```bash
24-
uv run ruff check drift/ --fix # Lint and auto-fix
25-
uv run ruff format drift/ # Format
26-
uv run ty check drift/ # Type check
24+
uv run ruff check drift/ tests/ --fix # Lint and auto-fix
25+
uv run ruff format drift/ tests/ # Format
26+
uv run ty check drift/ tests/ # Type check
2727
```
2828

2929
## Running Tests

drift/core/communication/communicator.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from typing import Any
1212

1313
from tusk.drift.core.v1 import GetMockRequest as ProtoGetMockRequest
14-
from ..span_serialization import clean_span_to_proto
14+
1515
from ...version import MIN_CLI_VERSION, SDK_VERSION
16+
from ..span_serialization import clean_span_to_proto
1617
from ..types import CleanSpanData, calling_library_context
1718
from .types import (
1819
CliMessage,
@@ -566,7 +567,7 @@ async def _receive_response(self, request_id: str) -> MockResponseOutput:
566567
response = cli_message.connect_response
567568
if response.success:
568569
logger.debug("CLI acknowledged connection")
569-
self._session_id = response.session_id
570+
# Note: session_id is not in the protobuf schema
570571
else:
571572
logger.error(f"CLI rejected connection: {response.error}")
572573
continue
@@ -576,6 +577,8 @@ async def _receive_response(self, request_id: str) -> MockResponseOutput:
576577

577578
def _recv_exact(self, n: int) -> bytes | None:
578579
"""Receive exactly n bytes from socket."""
580+
if self._socket is None:
581+
return None
579582
data = bytearray()
580583
while len(data) < n:
581584
chunk = self._socket.recv(n - len(data))
@@ -655,7 +658,8 @@ def _handle_cli_message(self, message: CliMessage) -> MockResponseOutput:
655658
if message.connect_response:
656659
response = message.connect_response
657660
if response.success:
658-
self._session_id = response.session_id
661+
logger.debug("CLI acknowledged connection")
662+
# Note: session_id is not in the protobuf schema
659663
return MockResponseOutput(found=False, error="Unexpected connect response")
660664

661665
return MockResponseOutput(found=False, error="Unknown message type")

drift/core/communication/types.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,43 @@
7777
CLIMessageType = MessageType
7878

7979

80+
def _python_to_value(value: Any) -> Any:
81+
"""Convert Python value to protobuf Value."""
82+
from betterproto.lib.google.protobuf import ListValue, Value
83+
84+
if value is None:
85+
from betterproto.lib.google.protobuf import NullValue
86+
87+
return Value(null_value=NullValue.NULL_VALUE) # type: ignore[arg-type]
88+
elif isinstance(value, bool):
89+
return Value(bool_value=value)
90+
elif isinstance(value, (int, float)):
91+
return Value(number_value=float(value))
92+
elif isinstance(value, str):
93+
return Value(string_value=value)
94+
elif isinstance(value, dict):
95+
from betterproto.lib.google.protobuf import Struct
96+
97+
struct = Struct()
98+
struct.fields = {k: _python_to_value(v) for k, v in value.items()}
99+
return Value(struct_value=struct)
100+
elif isinstance(value, (list, tuple)):
101+
list_val = ListValue(values=[_python_to_value(item) for item in value])
102+
return Value(list_value=list_val)
103+
else:
104+
return Value(string_value=str(value))
105+
106+
107+
def _dict_to_struct(data: dict[str, Any]) -> Any:
108+
"""Convert Python dict to protobuf Struct."""
109+
from betterproto.lib.google.protobuf import Struct
110+
111+
struct = Struct()
112+
if data:
113+
struct.fields = {k: _python_to_value(v) for k, v in data.items()}
114+
return struct
115+
116+
80117
@dataclass
81118
class ConnectRequest:
82119
"""Initial connection request from SDK to CLI.
@@ -102,11 +139,18 @@ class ConnectRequest:
102139

103140
def to_proto(self) -> ProtoConnectRequest:
104141
"""Convert to protobuf message."""
142+
from betterproto.lib.google.protobuf import Struct
143+
144+
# Convert metadata dict to protobuf Struct
145+
metadata_struct = Struct()
146+
if self.metadata:
147+
metadata_struct.fields = {k: _python_to_value(v) for k, v in self.metadata.items()}
148+
105149
return ProtoConnectRequest(
106150
service_id=self.service_id,
107151
sdk_version=self.sdk_version,
108152
min_cli_version=self.min_cli_version,
109-
metadata=self.metadata,
153+
metadata=metadata_struct,
110154
)
111155

112156

@@ -129,10 +173,12 @@ class ConnectResponse:
129173
@classmethod
130174
def from_proto(cls, proto: ProtoConnectResponse) -> ConnectResponse:
131175
"""Create from protobuf message."""
176+
# Note: ProtoConnectResponse only has success and error fields
177+
# cli_version and session_id are SDK-only extensions not in the protobuf schema
132178
return cls(
133179
success=proto.success,
134-
cli_version=proto.cli_version or None,
135-
session_id=proto.session_id or None,
180+
cli_version=None,
181+
session_id=None,
136182
error=proto.error or None,
137183
)
138184

@@ -164,7 +210,7 @@ class GetMockRequest:
164210

165211
def to_proto(self) -> ProtoGetMockRequest:
166212
"""Convert to protobuf message."""
167-
span = dict_to_span(self.outbound_span) if self.outbound_span else None
213+
span = dict_to_span(self.outbound_span) if self.outbound_span else ProtoSpan()
168214
return ProtoGetMockRequest(
169215
request_id=self.request_id,
170216
test_id=self.test_id,
@@ -253,7 +299,8 @@ def dict_to_span(data: dict[str, Any]) -> ProtoSpan:
253299
message=status_data.get("message", ""),
254300
)
255301
else:
256-
status = ProtoSpanStatus(code=ProtoStatusCode.UNSET)
302+
# UNSET is not a valid StatusCode in the proto - use UNSPECIFIED (0)
303+
status = ProtoSpanStatus(code=ProtoStatusCode.UNSPECIFIED)
257304

258305
return ProtoSpan(
259306
trace_id=data.get("trace_id", data.get("traceId", "")),
@@ -263,8 +310,8 @@ def dict_to_span(data: dict[str, Any]) -> ProtoSpan:
263310
package_name=data.get("package_name", data.get("packageName", "")),
264311
instrumentation_name=data.get("instrumentation_name", data.get("instrumentationName", "")),
265312
submodule_name=data.get("submodule_name", data.get("submoduleName", "")),
266-
input_value=data.get("input_value", data.get("inputValue", {})),
267-
output_value=data.get("output_value", data.get("outputValue", {})),
313+
input_value=_dict_to_struct(data.get("input_value", data.get("inputValue", {}))),
314+
output_value=_dict_to_struct(data.get("output_value", data.get("outputValue", {}))),
268315
input_schema_hash=data.get("input_schema_hash", data.get("inputSchemaHash", "")),
269316
output_schema_hash=data.get("output_schema_hash", data.get("outputSchemaHash", "")),
270317
input_value_hash=data.get("input_value_hash", data.get("inputValueHash", "")),
@@ -306,8 +353,8 @@ def span_to_proto(span: Any) -> ProtoSpan:
306353
package_name=span.package_name or "",
307354
instrumentation_name=span.instrumentation_name or "",
308355
submodule_name=span.submodule_name or "",
309-
input_value=span.input_value or {},
310-
output_value=span.output_value or {},
356+
input_value=_dict_to_struct(span.input_value or {}),
357+
output_value=_dict_to_struct(span.output_value or {}),
311358
input_schema_hash=span.input_schema_hash or "",
312359
output_schema_hash=span.output_schema_hash or "",
313360
input_value_hash=span.input_value_hash or "",

drift/core/mock_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
from typing import TYPE_CHECKING, Any
1313

1414
if TYPE_CHECKING:
15+
from .communication.types import MockResponseOutput
1516
from .drift_sdk import TuskDrift
1617
from .json_schema_helper import SchemaMerges
17-
from .types import CleanSpanData, MockResponseOutput
18+
from .types import CleanSpanData
1819

1920
from .json_schema_helper import JsonSchemaHelper
2021
from .types import (
@@ -91,7 +92,7 @@ def convert_mock_request_to_clean_span(
9192
input_schema=input_result.schema,
9293
input_schema_hash=input_result.decoded_schema_hash,
9394
input_value_hash=input_result.decoded_value_hash,
94-
output_schema=None,
95+
output_schema=None, # type: ignore[arg-type] - Must be None to avoid betterproto serialization issues
9596
output_schema_hash="",
9697
output_value_hash="",
9798
kind=kind,

drift/core/span_serialization.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828

2929
from .json_schema_helper import DecodedType, EncodingType, JsonSchema, JsonSchemaType
30-
from .types import CleanSpanData
30+
from .types import CleanSpanData, PackageType
3131

3232

3333
def _value_to_proto(value: Any) -> ProtoValue:
@@ -37,7 +37,9 @@ def _value_to_proto(value: Any) -> ProtoValue:
3737
proto_value = ProtoValue()
3838

3939
if value is None:
40-
proto_value.null_value = 0
40+
from betterproto.lib.google.protobuf import NullValue
41+
42+
proto_value.null_value = NullValue.NULL_VALUE # type: ignore[assignment]
4143
elif isinstance(value, bool):
4244
proto_value.bool_value = value
4345
elif isinstance(value, (int, float)):
@@ -94,7 +96,7 @@ def clean_span_to_proto(span: CleanSpanData) -> ProtoSpan:
9496
package_name=span.package_name,
9597
instrumentation_name=span.instrumentation_name,
9698
submodule_name=span.submodule_name,
97-
package_type=span.package_type.value if span.package_type else 0,
99+
package_type=span.package_type.value if span.package_type else PackageType.UNSPECIFIED.value, # type: ignore[arg-type]
98100
environment=span.environment,
99101
kind=span.kind.value if hasattr(span.kind, "value") else span.kind,
100102
input_value=_dict_to_struct(span.input_value),
@@ -119,7 +121,7 @@ def clean_span_to_proto(span: CleanSpanData) -> ProtoSpan:
119121
seconds=span.duration.seconds,
120122
microseconds=span.duration.nanos // 1000,
121123
),
122-
metadata=_metadata_to_dict(span.metadata),
124+
metadata=_dict_to_struct(_metadata_to_dict(span.metadata)),
123125
)
124126

125127

drift/core/trace_blocking_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def get_instance(cls) -> TraceBlockingManager:
5959
if cls._instance is None:
6060
cls._instance = TraceBlockingManager()
6161
cls._instance._start_cleanup_thread()
62+
assert cls._instance is not None
6263
return cls._instance
6364

6465
def block_trace(self, trace_id: str, reason: str = "size_limit") -> None:

0 commit comments

Comments
 (0)