-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Zach Price
committed
Apr 3, 2024
1 parent
dade89d
commit cb9308a
Showing
9 changed files
with
195 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"""Utilities that don't fit well in other modules.""" | ||
|
||
from typing import TYPE_CHECKING, TypeGuard | ||
|
||
from annotated_types import T | ||
|
||
|
||
def type_of(baseclass: T) -> T: | ||
"""Inherit from `baseclass` only for type checking purposes. | ||
This allows informing type checkers that the inheriting class ducktypes | ||
as the given `baseclass` without actually inheriting from it. | ||
Notes: | ||
- `typing.Protocol` is the right answer to this problem, but `sys.modules.__setitem__` | ||
currently checks for the ModuleType directly rather than a Protocol. | ||
- `pydantic_settings.BaseSettings` can't inherit from `ModuleType` due to conflicts | ||
in its use of `__slots__` | ||
""" | ||
if TYPE_CHECKING: | ||
return baseclass | ||
return object | ||
|
||
|
||
def is_list(value: T) -> TypeGuard[list]: | ||
"""TypeGuard based on whether the value is a list. | ||
Parameters: | ||
value (T): The value to be checked. | ||
Returns: | ||
TypeGuard[list]: Returns True if the value is a list, otherwise False. | ||
""" | ||
return isinstance(value, list) | ||
|
||
|
||
def one_or_list(value: list[T] | T) -> T | list[T]: | ||
"""Unwrap length 1 lists. | ||
This function takes a value that can be either a single item or a list of items. If the passed value is a list of length 1, the function returns the single item in the list. Otherwise, it returns the original list. | ||
Args: | ||
value (list[T] | T): The value to be unwrapped. | ||
Returns: | ||
T | list[T]: If the passed list is length 1, the function returns the single item in the list. Otherwise, it returns the original list. | ||
Raises: | ||
TypeError: If the passed value is neither a single item nor a list of items. | ||
Example: | ||
>>> one_or_list([1, 2, 3]) | ||
[1, 2, 3] | ||
>>> one_or_list(4) | ||
4 | ||
>>> one_or_list("hello") | ||
'hello' | ||
>>> one_or_list([1]) | ||
1 | ||
""" | ||
if is_list(value) and len(value) == 1: | ||
return value[0] | ||
return value | ||
|
||
|
||
def ensure_list(value: T) -> T | list[T]: | ||
"""If value is a list, return as is. Otherwise, wrap it in a list. | ||
Args: | ||
value (T): The value to be ensured as a list. | ||
Returns: | ||
list: Either the original list passed in, or the passed value wrapped in a list. | ||
Raises: | ||
TypeError: If the passed value is not a list and cannot be converted to one. | ||
Examples: | ||
>>> ensure_list(123) | ||
[123] | ||
>>> ensure_list([123, 456]) | ||
[123, 456] | ||
""" | ||
if isinstance(value, list): | ||
return value | ||
else: | ||
return [value] |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
Feature: The API provides observability endpoints for use by Kubernetes | ||
|
||
Kubernetes expects pods to provide a method to query their liveness (running or not) | ||
and readiness (whether its ready to serve traffic) states. | ||
|
||
Scenario Outline: Happy path (live and ready) | ||
|
||
Given a <probe_type> | ||
When its <endpoint> is querried | ||
Then it should return a positive <status> | ||
|
||
Examples: | ||
| probe_type | endpoint | status | | ||
| readiness | /healthz/readiness | ready | | ||
| liveness | /healthz/liveness | live | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
"""Step definitions for Behave/Cucumber style tests.""" | ||
|
||
import json | ||
from pathlib import Path | ||
from typing import Literal, Type, TypedDict | ||
|
||
from fastapi.testclient import TestClient | ||
from pytest_bdd import given, scenarios, then, when | ||
from pytest_bdd.parsers import parse | ||
from pytest_mock import MockerFixture | ||
|
||
from esg_fastapi import api | ||
from esg_fastapi.observability.models import ProbeResponse | ||
|
||
scenarios("Observability") | ||
|
||
|
||
class RequestResponseFixture(TypedDict): | ||
"""Type hint for example request/response fixtures loaded from JSON files.""" | ||
|
||
request: dict | ||
globus_response: dict | ||
esgsearch_response: dict | ||
|
||
|
||
class ComparisonFixture(RequestResponseFixture): | ||
"""RequestResponseFixture with fastapi_response populated.""" | ||
|
||
fastapi_response: dict | ||
|
||
|
||
@given(parse("a {probe_type}")) | ||
def load_example(probe_type: Literal["ready", "live"]) -> None: | ||
"""Handle the type of probe to be tested (Currently unused).""" | ||
|
||
|
||
@when(parse("its {endpoint} is querried"), target_fixture="probe_response") | ||
def send_request(endpoint: str) -> ProbeResponse: | ||
"""Send request to ESG FastAPI and return its response as a fixture. | ||
Notes: | ||
- We use the TestClient from FastAPI to send the request to the ESG FastAPI service | ||
which currently raises a Warning until the next release of FastAPI. | ||
ref: https://github.com/encode/starlette/issues/2524 | ||
""" | ||
client = TestClient(api) | ||
|
||
return ProbeResponse.model_validate(client.get(endpoint).json()) | ||
|
||
|
||
@then(parse("it should return a positive {status}")) | ||
def compare_responses( | ||
status: Literal["ready", "live"], probe_response: ProbeResponse | ||
) -> None: | ||
"""Ensure the expected response is returned for the probe.""" | ||
assert status == probe_response.status |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
"""Ensure that the settings module works as intended.""" | ||
|
||
from types import ModuleType | ||
|
||
import pytest | ||
from pytest_mock import MockerFixture | ||
from typing_extensions import assert_type | ||
|
||
from esg_fastapi.utils import type_of | ||
|
||
|
||
def test_settings_is_usable() -> None: | ||
"""Ensure that the settings module can be imported and provides at least one setting.""" | ||
from esg_fastapi import settings | ||
|
||
assert settings.globus_search_index is not None | ||
|
||
|
||
@pytest.mark.parametrize("enabled", [True, False]) | ||
def test_type_of_indicates_correctly(mocker: MockerFixture, enabled: bool) -> None: | ||
"""Ensure that a class inheriting from type_of(T) type checks as a T but otherwise doesn't change its inheritance.""" | ||
mocker.patch("typing.TYPE_CHECKING", enabled) | ||
|
||
class TestObj(type_of(ModuleType)): ... | ||
|
||
if enabled: | ||
assert_type(TestObj, ModuleType) | ||
else: | ||
assert_type(TestObj, object) |