Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,18 @@ Configured under `[tool.mypy]` in `pyproject.toml`. The generated `src/dp_pytho
package has no type information, so it is excluded and its imports resolve to `Any` -- both an
`exclude` and a `follow_imports = "skip"` override are needed, and the comment there says why.
They go once dp-grpc ships typed stubs (osprey-dcs/dp-grpc#158), at which point a `py.typed`
marker becomes worth adding. There is deliberately no `python_version`: numpy's stubs use 3.12
syntax, and pinning 3.10 stops mypy checking anything. Two consequences worth knowing:
marker becomes worth adding. The suppression does **not** hold against `.pyi` files: mypy reads a
stub whenever one is present, whatever `follow_imports` says. So the first stub sync that carries
`.pyi` files type-checks the hand-written code against real protobuf types even with the
suppression still in place, and that code has to be clean against them *before* the sync arrives.
It was made so ahead of time; keep it so by checking a change against locally generated stubs
(`[codegen]` extra, at the versions pinned in dp-grpc's `tools/python-stubs-requirements.in` and
with its `generate-python-stubs.yml` flags) when it touches proto types in a way `Any` would hide.
`grpc` itself is typed through `types-grpcio`. The rest of the adoption (removing the
suppression, typing `_stub`, `py.typed`) is tracked in #61.

There is deliberately no `python_version`: numpy's stubs use 3.12 syntax, and pinning 3.10 stops
mypy checking anything. Two consequences worth knowing:

- An alias whose union includes a proto type needs an explicit `TypeAlias` annotation
(`TimestampInput: TypeAlias = ...`); with the proto resolving to `Any`, mypy no longer infers it.
Expand Down Expand Up @@ -167,8 +177,9 @@ Core dependencies are managed in `pyproject.toml`:

Optional extras:
- `[analysis]` - `pandas`, `numpy`, `openpyxl` for the query-result conversions
- `[dev]` - `pytest`, `mypy` (with `types-PyYAML`, `types-protobuf`, `pandas-stubs`), `ruff`, `build`, `twine`;
- `[dev]` - `pytest`, `mypy` (with `types-PyYAML`, `types-protobuf`, `types-grpcio`, `pandas-stubs`), `ruff`, `build`, `twine`;
install with `pip install -e ".[analysis,dev]"`
- `[codegen]` - `grpcio-tools`, `mypy-protobuf`; only for regenerating `src/dp_python_lib/grpc/`

## Ticket Planning Workflow

Expand Down
6 changes: 3 additions & 3 deletions doc/cookbook/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,9 @@ Time inputs accept any of three forms, converted by the shared `to_timestamp()`
# cookbook:partial
from datetime import datetime, timezone

t1 = to_timestamp(datetime(2026, 2, 2, 18, 4, 1, tzinfo=timezone.utc))
t2 = to_timestamp(1770055441)
t3 = to_timestamp(1770055441.5) # .5 -> 500_000_000 nanoseconds
ts1 = to_timestamp(datetime(2026, 2, 2, 18, 4, 1, tzinfo=timezone.utc))
ts2 = to_timestamp(1770055441)
ts3 = to_timestamp(1770055441.5) # .5 -> 500_000_000 nanoseconds
```

**Naive datetimes raise `ValueError`.** This is the most likely first-run error, and it is
Expand Down
15 changes: 15 additions & 0 deletions doc/release-notes/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ person cutting the release has any reason to re-read.

- [Release pages are the notes file, verbatim (#56)](#release-pages-are-the-notes-file-verbatim-issue-56)
- [Type checking in CI (#30)](#type-checking-in-ci-issue-30)
- [Ready for typed gRPC stubs (#61)](#ready-for-typed-grpc-stubs-issue-61)
- [Cutting the release](#cutting-the-release)

---
Expand Down Expand Up @@ -69,6 +70,20 @@ Two annotations became more accurate along the way:
`assert client.annotation is not None` if yours does.
- **`timestamp_list()` accepts any sequence** of timestamps (a tuple, say), not only a `list`.

## Ready for typed gRPC stubs (Issue #61)

dp-grpc is adding `.pyi` type stubs, generated by
[mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf), to the gRPC stubs it syncs into
`src/dp_python_lib/grpc/`
([osprey-dcs/dp-grpc#158](https://github.com/osprey-dcs/dp-grpc/issues/158)). The library's own
code now type-checks cleanly against those stubs, so a sync that brings them needs no change here.
Nothing about the library's behavior changes.

- **The `[codegen]` extra now includes `mypy-protobuf`.** If you regenerate the stubs yourself, it
supplies the `--mypy_out` / `--mypy_grpc_out` protoc plugins; without it you get the `.py` files
but no `.pyi`.
- **The `[dev]` extra now includes `types-grpcio`**, so `grpc` is type-checked rather than ignored.

## Installing

```bash
Expand Down
21 changes: 13 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dev = [
"mypy",
"types-PyYAML",
"types-protobuf",
"types-grpcio",
"pandas-stubs",
"pytest",
"ruff",
Expand All @@ -73,9 +74,15 @@ dev = [
]

# Toolchain for regenerating the gRPC stubs in src/dp_python_lib/grpc from the upstream
# dp-grpc protos. Not needed to use or develop against the library.
# dp-grpc protos. Not needed to use or develop against the library. mypy-protobuf is the
# protoc plugin behind --mypy_out / --mypy_grpc_out, which write the .pyi type stubs beside the
# generated .py files (osprey-dcs/dp-grpc#158); without it a regeneration produces untyped stubs.
# dp-grpc is the authority on the generator: its tools/python-stubs-requirements.in pins the
# versions and its generate-python-stubs.yml workflow sets the flags. Keep these minimums in step
# with those pins (#61).
codegen = [
"grpcio-tools>=1.84.0",
"mypy-protobuf>=5.1.0",
]

[tool.setuptools.packages.find]
Expand Down Expand Up @@ -106,19 +113,17 @@ markers = [
# The generated gRPC package is untyped (no .pyi stubs; osprey-dcs/dp-grpc#158), so it is
# excluded from checking and its imports are resolved to Any. Both are needed: `exclude`
# alone still follows imports into it, and `follow_imports = "skip"` alone does nothing for
# files `mypy src/` names on the command line. Remove both once typed stubs are synced.
# files `mypy src/` names on the command line. Remove both once typed stubs are synced (#61).
#
# Neither holds against .pyi files: mypy reads a stub whenever one is present, whatever
# follow_imports says. So once a sync brings dp-grpc's .pyi stubs, the hand-written code is
# checked against real protobuf types even with this suppression in place.
exclude = ["^src/dp_python_lib/grpc/"]

[[tool.mypy.overrides]]
module = ["dp_python_lib.grpc.*"]
follow_imports = "skip"

# grpcio ships no type information and there is no maintained stub package for it. Scoped
# to grpc rather than set globally, which would also hide a genuinely missing dependency.
[[tool.mypy.overrides]]
module = ["grpc", "grpc.*"]
ignore_missing_imports = true

[tool.ruff]
# 120 matches the margin this codebase was already written to: at line-length 100 there were
# 307 violations, nearly all docstrings and long string literals that the formatter will not
Expand Down
2 changes: 1 addition & 1 deletion src/dp_python_lib/client/export_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ExportFormat(str, Enum):
CSV = "csv"
XLSX = "xlsx"

def to_proto(self) -> "annotation_pb2.ExportDataRequest.ExportOutputFormat":
def to_proto(self) -> "annotation_pb2.ExportDataRequest.ExportOutputFormat.ValueType":
"""
Converts this format into its protobuf enum value.
:return: The corresponding ExportDataRequest.ExportOutputFormat value.
Expand Down
4 changes: 4 additions & 0 deletions src/dp_python_lib/client/machine_config_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,8 @@ def _build_get_configuration_activation_request(
"Building GetConfigurationActivationRequest by composite key: %s",
configuration_name,
)
# Guaranteed by _validate_activation_key above; the assert only narrows the Optionals for mypy.
assert configuration_name is not None and start_time is not None
request.compositeKey.configurationName = configuration_name
request.compositeKey.startTime.CopyFrom(to_timestamp(start_time))
return request
Expand Down Expand Up @@ -1278,6 +1280,8 @@ def _build_delete_configuration_activation_request(
"Building DeleteConfigurationActivationRequest by composite key: %s",
configuration_name,
)
# Guaranteed by _validate_activation_key above; the assert only narrows the Optionals for mypy.
assert configuration_name is not None and start_time is not None
request.compositeKey.configurationName = configuration_name
request.compositeKey.startTime.CopyFrom(to_timestamp(start_time))
return request
Expand Down
5 changes: 5 additions & 0 deletions src/dp_python_lib/client/mldp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def __init__(
self.logger.error(error_msg)
raise ValueError(error_msg)

# The query and annotation channels are optional, so declare them as such up front; the
# ingestion channel is always set (or the constructor raises).
self._query_channel: grpc.Channel | None
self._annotation_channel: grpc.Channel | None

if query_channel is not None:
self.logger.debug("Using explicit query channel")
self._query_channel = query_channel
Expand Down
6 changes: 2 additions & 4 deletions src/dp_python_lib/client/query_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@
# Excel's hard row ceiling (1,048,576 rows including a header row).
_EXCEL_MAX_ROWS = 1_048_576 - 1

# Mapping from the Image.FileType enum number to its name, resolved once from the descriptor.
_IMAGE_FILE_TYPE_NAMES = {
v.number: v.name for v in common_pb2.Image.DESCRIPTOR.fields_by_name["fileType"].enum_type.values
}
# Mapping from the Image.FileType enum number to its name, resolved once from the enum wrapper.
_IMAGE_FILE_TYPE_NAMES = {number: name for name, number in common_pb2.Image.FileType.items()}


class Image:
Expand Down
Loading