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
35 changes: 32 additions & 3 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ annotated-types==0.7.0
anyio==4.14.0
# via
# httpx
Comment thread
bprobert97 marked this conversation as resolved.
# httpx2
# labthings-fastapi
# starlette
# watchfiles
Expand Down Expand Up @@ -81,6 +82,10 @@ email-validator==2.3.0
# via
# fastapi
# pydantic
exceptiongroup==1.3.1
# via
# anyio
# pytest
fastapi==0.135.4
# via labthings-fastapi
fastapi-cli==0.0.27
Expand Down Expand Up @@ -109,24 +114,29 @@ flake8-rst-docstrings==0.4.0
h11==0.16.0
# via
# httpcore
# httpcore2
# uvicorn
html5lib==1.1
# via sphinx-toolbox
httpcore==1.0.9
# via httpx
httpcore2==2.5.0
# via httpx2
httptools==0.8.0
# via uvicorn
httpx==0.28.1
Comment thread
bprobert97 marked this conversation as resolved.
# via
# fastapi
# fastapi-cloud-cli
# labthings-fastapi
httpx2==2.5.0
# via labthings-fastapi
idna==3.18
# via
# anyio
# apeye-core
# email-validator
# httpx
# httpx2
# requests
# sphinx-prompt
ifaddr==0.2.0
Expand Down Expand Up @@ -325,14 +335,26 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphobjinv==2.4
# via labthings-fastapi
standard-imghdr==3.10.14
# via sphinx-jinja2-compat
starlette==1.3.1
# via fastapi
tabulate==0.10.0
# via sphinx-toolbox
tinycss2==1.5.1
# via dict2css
tomli==2.4.1
# via
# coverage
# fastapi-cli
# flake8-pyproject
# labthings-fastapi
# mypy
# pydoclint
# pytest
# sphinx
truststore==0.10.4
# via
# httpcore2
# httpx2
typer==0.26.7
# via
# fastapi-cli
Expand All @@ -341,17 +363,24 @@ types-jsonschema==4.26.0.20260518
# via labthings-fastapi
typing-extensions==4.15.0
# via
# anyio
# astroid
# beautifulsoup4
# domdf-python-tools
# exceptiongroup
# fastapi
# httpx2
# labthings-fastapi
# mypy
# pydantic
# pydantic-core
# pydantic-extra-types
# referencing
# rich-toolkit
# sphinx-toolbox
# starlette
# typing-inspection
# uvicorn
typing-inspection==0.4.2
# via
# fastapi
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies = [
"jsonschema",
"typing_extensions",
"anyio ~=4.0",
"httpx",
"httpx2",
"fastapi[all]~=0.135.0",
"zeroconf >=0.28.0",
]
Expand Down
24 changes: 12 additions & 12 deletions src/labthings_fastapi/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Code to access `~lt.Thing` features over HTTP.

This module defines a base class for controlling LabThings-FastAPI over HTTP.
It is based on `httpx`, and attempts to create a simple wrapper such that
It is based on `httpx2`, and attempts to create a simple wrapper such that
each Action becomes a method and each Property becomes an attribute.
"""

Expand All @@ -12,7 +12,7 @@
from typing import Any, Optional, Union
from urllib.parse import urljoin, urlparse

import httpx
import httpx2
from pydantic import BaseModel, TypeAdapter, ValidationError
from typing_extensions import Self # 3.9, 3.10 compatibility

Expand Down Expand Up @@ -63,11 +63,11 @@
:raise KeyError: if there is no link with the specified ``rel`` value.
"""
if "links" not in obj:
raise ObjectHasNoLinksError(f"Can't find any links on {obj}.")

Check warning on line 66 in src/labthings_fastapi/client/__init__.py

View workflow job for this annotation

GitHub Actions / coverage

66 line is not covered with tests
try:
return next(link for link in obj["links"] if link["rel"] == rel)
except StopIteration as e:
raise KeyError(f"No link was found with rel='{rel}' on {obj}.") from e

Check warning on line 70 in src/labthings_fastapi/client/__init__.py

View workflow job for this annotation

GitHub Actions / coverage

69-70 lines are not covered with tests


def invocation_href(invocation: dict) -> str:
Expand All @@ -86,7 +86,7 @@


def poll_invocation(
client: httpx.Client,
client: httpx2.Client,
invocation: dict,
interval: float = 0.5,
first_interval: float = 0.05,
Expand All @@ -99,7 +99,7 @@
has completed, whether it was successful, and retrieve its
output.

:param client: the `httpx.Client` to use for HTTP requests.
:param client: the `httpx2.Client` to use for HTTP requests.
:param invocation: the dictionary returned from the initial POST request.
:param interval: sets how frequently we poll, in seconds.
:param first_interval: sets how long we wait before the first
Expand Down Expand Up @@ -139,20 +139,20 @@
creates a subclass with the right attributes.
"""

def __init__(self, base_url: str, client: Optional[httpx.Client] = None) -> None:
def __init__(self, base_url: str, client: Optional[httpx2.Client] = None) -> None:
"""Create a ThingClient connected to a remote Thing.

:param base_url: the base URL of the Thing. This should be the URL
of the Thing Description document.
:param client: an optional `httpx.Client` object to use for all
:param client: an optional `httpx2.Client` object to use for all
HTTP requests. This may be a `fastapi.TestClient` object for
testing purposes.
"""
parsed = urlparse(base_url)
server = f"{parsed.scheme}://{parsed.netloc}"
self.server = server
self.path = parsed.path
self.client = client or httpx.Client(base_url=server)
self.client = client or httpx2.Client(base_url=server)

def get_property(self, path: str) -> Any:
"""Make a GET request to retrieve the value of a property.
Expand Down Expand Up @@ -253,7 +253,7 @@
# error DOC503 for this function.
raise _invocation_error(invocation)

def follow_link(self, response: dict, rel: str) -> httpx.Response:
def follow_link(self, response: dict, rel: str) -> httpx2.Response:
"""Follow a link in a response object, by its `rel` attribute.

:param response: is the dictionary returned by e.g. `.poll_invocation`.
Expand All @@ -262,29 +262,29 @@

:return: the response to making a ``GET`` request to the link.
"""
href = _get_link(response, rel)["href"]
r = self.client.get(href)
r.raise_for_status()
return r

Check warning on line 268 in src/labthings_fastapi/client/__init__.py

View workflow job for this annotation

GitHub Actions / coverage

265-268 lines are not covered with tests

@classmethod
def from_url(cls, thing_url: str, client: Optional[httpx.Client] = None) -> Self:
def from_url(cls, thing_url: str, client: Optional[httpx2.Client] = None) -> Self:
"""Create a ThingClient from a URL.

This will dynamically create a subclass with properties and actions,
and return an instance of that subclass pointing at the Thing URL.

:param thing_url: The base URL of the Thing, which should also be the
URL of its Thing Description.
:param client: is an optional `httpx.Client` object. If not present,
:param client: is an optional `httpx2.Client` object. If not present,
one will be created. This is particularly useful if you need to
set HTTP options, or if you want to work with a local server
object for testing purposes (see `fastapi.TestClient`).

:return: a `~lt.ThingClient` subclass with properties and methods that
match the retrieved Thing Description (see :ref:`wot_thing`).
"""
td_client = client or httpx
td_client = client or httpx2
r = td_client.get(thing_url)
r.raise_for_status()
subclass = cls.subclass_from_td(r.json())
Expand Down Expand Up @@ -372,7 +372,7 @@
_objtype: Optional[type[ThingClient]] = None,
) -> Any:
if obj is None:
return self

Check warning on line 375 in src/labthings_fastapi/client/__init__.py

View workflow job for this annotation

GitHub Actions / coverage

375 line is not covered with tests
return obj.get_property(self.name)
else:

Expand Down Expand Up @@ -460,7 +460,7 @@
)


def _construct_failed_to_invoke_message(path: str, response: httpx.Response) -> str:
def _construct_failed_to_invoke_message(path: str, response: httpx2.Response) -> str:
"""Format an error for ThingClient to raise if an invocation fails to start.

:param path: The path of the action
Expand All @@ -472,7 +472,7 @@
details = response.json().get("detail", [])

if isinstance(details, str):
message = f"Error when invoking action {path}: {details}"

Check warning on line 475 in src/labthings_fastapi/client/__init__.py

View workflow job for this annotation

GitHub Actions / coverage

475 line is not covered with tests
if isinstance(details, list) and len(details) and isinstance(details[0], dict):
loc = details[0].get("loc", [])
loc_str = "" if len(loc) < 2 else f"'{loc[1]}' - "
Expand Down
12 changes: 6 additions & 6 deletions src/labthings_fastapi/outputs/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
)
from weakref import WeakValueDictionary

import httpx
import httpx2
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, Response
from pydantic import (
Expand Down Expand Up @@ -195,18 +195,18 @@
"""

def __init__(
self, media_type: str, href: str, client: httpx.Client | None = None
self, media_type: str, href: str, client: httpx2.Client | None = None
) -> None:
"""Create a reference to remote `.Blob` data.

:param media_type: the MIME type of the data.
:param href: the URL where it may be downloaded.
:param client: if supplied, this `httpx.Client` will be used to
:param client: if supplied, this `httpx2.Client` will be used to
download the data.
"""
super().__init__(media_type=media_type)
self._href = href
self._client = client or httpx.Client()
self._client = client or httpx2.Client()

def get_href(self) -> str:
"""Return the URL to download the data.
Expand Down Expand Up @@ -301,7 +301,7 @@

:return: a list of UUIDs for all registered `.LocalBlobData` objects.
"""
return list(cls._all_blobdata.keys())

Check warning on line 304 in src/labthings_fastapi/outputs/blob.py

View workflow job for this annotation

GitHub Actions / coverage

304 line is not covered with tests

@property
def id(self) -> uuid.UUID:
Expand Down Expand Up @@ -882,7 +882,7 @@
def from_url(
cls,
href: str,
client: httpx.Client | None = None,
client: httpx2.Client | None = None,
media_type: str | None = None,
) -> Self:
"""Create a `.Blob` that references data at a URL.
Expand All @@ -892,7 +892,7 @@
of `.Blob` that has set ``media_type``.

:param href: the URL where the data may be downloaded.
:param client: if supplied, this `httpx.Client` will be used to
:param client: if supplied, this `httpx2.Client` will be used to
download the data.
:param media_type: the media type of the supplied data, defaults to
the ``media_type`` attribute of this class.
Expand Down Expand Up @@ -947,8 +947,8 @@
try:
blob = LocalBlobData.from_id(blob_id)
return blob.response()
except KeyError as e:
raise HTTPException(status_code=404, detail="Blob not found") from e

Check warning on line 951 in src/labthings_fastapi/outputs/blob.py

View workflow job for this annotation

GitHub Actions / coverage

950-951 lines are not covered with tests


def url_to_id(url: str) -> uuid.UUID | None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_action_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time

import httpx
import httpx2
import pytest

import labthings_fastapi as lt
Expand Down Expand Up @@ -59,7 +59,7 @@ def test_action_expires(client):
invocation["status"] = "running" # Force an extra poll
# When the second action runs, the first one should expire
# so polling it again should give a 404.
with pytest.raises(httpx.HTTPStatusError):
with pytest.raises(httpx2.HTTPStatusError):
poll_task(client, invocation)


Expand Down
6 changes: 3 additions & 3 deletions tests/test_thing_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import re

import httpx
import httpx2
import pytest

import labthings_fastapi as lt
Expand Down Expand Up @@ -288,8 +288,8 @@ def test_poll_invocation_errors(mocker):
"id": 0,
}
# This error conforms to "problem_details" format
client = mocker.Mock(spec=httpx.Client)
response = mocker.Mock(spec=httpx.Response)
client = mocker.Mock(spec=httpx2.Client)
response = mocker.Mock(spec=httpx2.Response)
response.is_error = True
response.status_code = 500
response.json = mocker.MagicMock(return_value={"detail": "expected error text"})
Expand Down
Loading