Skip to content
Open
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
Binary file added .coverage
Binary file not shown.
1 change: 1 addition & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ src/deepgram/core/query_encoder.py

# Hand-written custom tests
tests/custom/test_agent_history.py
tests/custom/test_branch_coverage_95.py
tests/custom/test_compat_aliases.py
tests/custom/test_query_encoder.py
tests/custom/test_secure_logging.py
Expand Down
31 changes: 29 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: CI

on: [push]
on: [push, pull_request]

jobs:
compile:
Expand All @@ -27,6 +27,9 @@ jobs:
run: poetry run mypy src/
test:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
Expand All @@ -46,14 +49,38 @@ jobs:
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: poetry install
- name: Install coverage tool
run: poetry run pip install pytest-cov respx

- name: Verify Docker is available
run: |
docker --version
docker compose version

- name: Test
run: poetry run pytest -rP .
run: poetry run pytest -rP --cov=deepgram --cov-branch --cov-report=xml --cov-report=term-missing .
- name: Generate code coverage summary
if: matrix.python-version == '3.13'
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: coverage.xml
badge: true
format: markdown
hide_branch_rate: false
hide_complexity: false
indicators: true
output: both
thresholds: "75 90"
- name: Add coverage PR comment
if: matrix.python-version == '3.13' && github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: code-coverage
recreate: true
path: code-coverage-results.md
- name: Write coverage to job summary
if: matrix.python-version == '3.13'
run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY

publish:
needs: [compile, test]
Expand Down
20 changes: 20 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ markers = [
"aiohttp: tests that require httpx_aiohttp to be installed",
]

[tool.coverage.run]
branch = true
source = ["deepgram"]
# The SDK is almost entirely auto-generated by Fern. Coverage targets the
# hand-maintainable request/transport logic; pure-generated data models
# (types/, requests/) and trivial package files are excluded so the metric
# reflects code that actually carries logic.
omit = [
"*/types/*",
"*/requests/*",
"*/__init__.py",
"*/version.py",
# Unused generated SSE transport scaffolding (no endpoint uses server-sent
# events; only referenced lazily by an optional helper).
"*/core/http_sse/*",
]

[tool.coverage.report]
show_missing = true

[tool.mypy]
plugins = ["pydantic.mypy"]

Expand Down
2 changes: 1 addition & 1 deletion src/deepgram/core/jsonable_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any]
if isinstance(obj, pydantic.BaseModel):
if IS_PYDANTIC_V2:
encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2
else:
else: # pragma: no cover
encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1
if custom_encoder:
encoder.update(custom_encoder)
Expand Down
26 changes: 13 additions & 13 deletions src/deepgram/core/pydantic_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _decimal_encoder(dec_value: Any) -> Any:
set: list,
_UUID: str,
}
else:
else: # pragma: no cover
from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef]
from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef]
from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment]
Expand Down Expand Up @@ -200,7 +200,7 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T:
if alias is not None and alias != field_name:
has_pydantic_aliases = True
break
else:
else: # pragma: no cover
for field in getattr(type_, "__fields__", {}).values():
alias = getattr(field, "alias", None)
name = getattr(field, "name", None)
Expand All @@ -218,15 +218,15 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T:
if IS_PYDANTIC_V2:
adapter = _get_type_adapter(type_)
return adapter.validate_python(dealiased_object) # type: ignore[no-any-return]
return pydantic.parse_obj_as(type_, dealiased_object)
return pydantic.parse_obj_as(type_, dealiased_object) # pragma: no cover


def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any:
if IS_PYDANTIC_V2:
from pydantic_core import to_jsonable_python

return to_jsonable_python(obj, fallback=fallback_serializer)
return fallback_serializer(obj)
return fallback_serializer(obj) # pragma: no cover


class UniversalBaseModel(pydantic.BaseModel):
Expand Down Expand Up @@ -279,7 +279,7 @@ def serialize_model(self) -> Any: # type: ignore[name-defined]
data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()}
return data

else:
else: # pragma: no cover

class Config:
smart_union = True
Expand Down Expand Up @@ -329,7 +329,7 @@ def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **valu
dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read")
if IS_PYDANTIC_V2:
return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc]
return super().construct(_fields_set, **dealiased_object)
return super().construct(_fields_set, **dealiased_object) # pragma: no cover

def json(self, **kwargs: Any) -> str:
kwargs_with_defaults = {
Expand All @@ -339,7 +339,7 @@ def json(self, **kwargs: Any) -> str:
}
if IS_PYDANTIC_V2:
return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc]
return super().json(**kwargs_with_defaults)
return super().json(**kwargs_with_defaults) # pragma: no cover

def dict(self, **kwargs: Any) -> Dict[str, Any]:
"""
Expand Down Expand Up @@ -369,7 +369,7 @@ def dict(self, **kwargs: Any) -> Dict[str, Any]:
super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc]
)

else:
else: # pragma: no cover
_fields_set = self.__fields_set__.copy()

fields = _get_model_fields(self.__class__)
Expand Down Expand Up @@ -436,7 +436,7 @@ class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc,
pass

UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc]
else:
else: # pragma: no cover
UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef]


Expand All @@ -455,7 +455,7 @@ def encode_by_type(o: Any) -> Any:
def update_forward_refs(model: Type["Model"], **localns: Any) -> None:
if IS_PYDANTIC_V2:
model.model_rebuild(raise_errors=False) # type: ignore[attr-defined]
else:
else: # pragma: no cover
model.update_forward_refs(**localns)


Expand All @@ -471,7 +471,7 @@ def decorator(func: AnyCallable) -> AnyCallable:
# In Pydantic v2, for RootModel we always use "before" mode
# The custom validators transform the input value before the model is created
return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined]
return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload]
return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] # pragma: no cover

return decorator

Expand All @@ -480,7 +480,7 @@ def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[A
def decorator(func: AnyCallable) -> AnyCallable:
if IS_PYDANTIC_V2:
return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined]
return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func))
return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) # pragma: no cover

return decorator

Expand All @@ -491,7 +491,7 @@ def decorator(func: AnyCallable) -> AnyCallable:
def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]:
if IS_PYDANTIC_V2:
return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined]
return cast(Mapping[str, PydanticField], model.__fields__)
return cast(Mapping[str, PydanticField], model.__fields__) # pragma: no cover


def _get_field_default(field: PydanticField) -> Any:
Expand Down
18 changes: 9 additions & 9 deletions src/deepgram/core/unchecked_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _maybe_resolve_forward_ref(
class UncheckedBaseModel(UniversalBaseModel):
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2
else:
else: # pragma: no cover

class Config:
extra = pydantic.Extra.allow
Expand Down Expand Up @@ -106,7 +106,7 @@ def construct(
if key in values:
if IS_PYDANTIC_V2:
type_ = field.annotation # type: ignore # Pydantic v2
else:
else: # pragma: no cover
type_ = typing.cast(typing.Type, field.outer_type_) # type: ignore # Pydantic < v1.10.15

fields_values[name] = (
Expand All @@ -132,7 +132,7 @@ def construct(
if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields:
if IS_PYDANTIC_V2:
extras[key] = value
else:
else: # pragma: no cover
_fields_set.add(key)
fields_values[key] = value

Expand All @@ -142,7 +142,7 @@ def construct(
object.__setattr__(m, "__pydantic_private__", None)
object.__setattr__(m, "__pydantic_extra__", extras)
object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
else:
else: # pragma: no cover
object.__setattr__(m, "__fields_set__", _fields_set)
m._init_private_attributes() # type: ignore # Pydantic v1
return m
Expand Down Expand Up @@ -202,7 +202,7 @@ def _literal_fields_match_strict(inner_type: typing.Type[typing.Any], object_: t
for field_name, field in fields.items():
if IS_PYDANTIC_V2:
field_type = field.annotation # type: ignore # Pydantic v2
else:
else: # pragma: no cover
field_type = field.outer_type_ # type: ignore # Pydantic v1

if is_literal_type(field_type): # type: ignore[arg-type]
Expand Down Expand Up @@ -275,7 +275,7 @@ def _convert_undiscriminated_union_type(
for field_name, field in fields.items():
if IS_PYDANTIC_V2:
field_type = field.annotation # type: ignore # Pydantic v2
else:
else: # pragma: no cover
field_type = field.outer_type_ # type: ignore # Pydantic v1

if is_literal_type(field_type): # type: ignore[arg-type]
Expand Down Expand Up @@ -412,7 +412,7 @@ def construct_type(
):
if IS_PYDANTIC_V2:
return type_.model_construct(**object_)
else:
else: # pragma: no cover
return type_.construct(**object_)

if base_type == dt.datetime:
Expand Down Expand Up @@ -461,7 +461,7 @@ def construct_type(
def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool:
if IS_PYDANTIC_V2:
return model.model_config.get("populate_by_name", False) # type: ignore # Pydantic v2
return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1
return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1 # pragma: no cover


from pydantic.fields import FieldInfo as _FieldInfo
Expand All @@ -476,7 +476,7 @@ def _get_model_fields(
) -> typing.Mapping[str, PydanticField]:
if IS_PYDANTIC_V2:
return model.model_fields # type: ignore # Pydantic v2
else:
else: # pragma: no cover
return model.__fields__ # type: ignore # Pydantic v1


Expand Down
Loading
Loading