labthings_fastapi.outputs.blob
BLOB Output Module.
The .Blob class is used when you need to return something file-like that can’t
easily (or efficiently) be converted to JSON. This is useful for returning large objects
like images, especially where an existing file-type is the obvious way to handle it.
There is a documentation page on Blob input/output that explains how to use this mechanism.
To return a file from an action, you should declare its return type as a Blob
subclass, defining the
Blob.media_type attribute.
class MyImageBlob(Blob):
media_type = "image/png"
class MyThing(Thing):
@action
def get_image(self) -> MyImageBlob:
# Do something to get the image data
data = self._get_image_data()
return MyImageBlob.from_bytes(data)
The action should then return an instance of that subclass, with data supplied
either as a bytes object or a file on disk. If files are used, it’s your
responsibility to ensure the file is deleted after the
Blob object is garbage-collected. Constructing it using the class methods
Blob.from_bytes or Blob.from_temporary_directory will ensure this is
done for you.
Bear in mind a tempfile.TemporaryFile object only holds a file descriptor
and is not safe for concurrent use, which does not work well with the HTTP API:
action outputs may be retrieved multiple times after the action has
completed, possibly concurrently. Creating a temp folder and making a file inside it
with Blob.from_temporary_directory is the safest way to deal with this.
Serialisation
Blob objects are serialised to a JSON representation that includes a download
href. This is generated using middleware.url_for which uses a context
variable to pass the function that generates URLs to the serialiser code. That
context variable is available in every response handler function in the FastAPI
app - but it is not, in general, available in action or property code (because
actions and properties run their code in separate threads). The sequence of events
that leads to a Blob being downloaded as a result of an action is roughly:
- A
POSTrequest invokes the action. middleware.url_for.url_for_middlewaremakesurl_foraccessible viaa context variable
A
201response is returned that includes anhrefto poll the action.- Action code is run in a separate thread (without
url_forin the context):
- Action code is run in a separate thread (without
The output value of the action is stored in the
Invocationthread.
- A
- A
GETrequest polls the action. Once it has completed: middleware.url_for.url_for_middlewaremakesurl_foraccessible viaa context variable
- The
Invocationmodel is returned, which includes theBlobin the outputfield.
- The
- A
A further
GETrequest actually downloads theBlob.
This slightly complicated sequence ensures that we only ever send URLs back to the
client using url_for from the current fastapi.Request object. That means the
URL used should be consistent with the URL of the request - so if an action is
started by a client using one IP address or DNS name, and polled by a different
client, each client will get a download href that matches the address they are
already using.
In the future, it may be possible to respond directly with the Blob data to
the original POST request, however this only works for quick actions so for now
we use the sequence above, which will work for both quick and slow actions.
Attributes
A FastAPI router for BlobData download endpoints. |
Classes
The data store of a Blob. |
|
A BlobData subclass that references remote data via a URL. |
|
A BlobData subclass where the data is stored locally. |
|
A |
|
A model for JSON-serialised |
|
A container for binary data that may be retrieved over HTTP. |
Functions
|
Parse a media type string into its type and subtype. |
|
Check if a media type matches a pattern. |
|
Download a |
|
Extract the blob ID from a URL. |
Module Contents
- class labthings_fastapi.outputs.blob.BlobData(media_type: str)
The data store of a Blob.
Blobobjects can represent their data in various ways. Each of those options must provide three ways to access the data, which are thecontentproperty, thesave()method, and theopen()method.This base class defines the interface needed by any data store used by a
Blob.Blobs that store their data locally should subclass
LocalBlobDatawhich adds aresponse()method andidproperty, appropriate for data that would need to be downloaded from a server. It also takes care of generating a download URL when it’s needed.Initialise a
BlobDataobject.- Parameters:
media_type – the MIME type of the data.
- _media_type
- abstract get_href() str
Return the URL to download the blob.
The implementation of this method for local blobs will need
url_for.url_forand thus it should only be called in a response handler when themiddeware.url_formiddleware is enabled.- Returns:
the URL as a string.
- Raises:
NotImplementedError – always, as this must be implemented by subclasses.
- property content: bytes
- Abstractmethod:
The data as a
bytesobject.- Raises:
NotImplementedError – always, as this must be implemented by subclasses.
- abstract save(filename: str) None
Save the data to a file.
- Parameters:
filename – the path where the file should be saved.
- Raises:
NotImplementedError – always, as this must be implemented by subclasses.
- abstract open() io.IOBase
Return a file-like object that may be read from.
- Returns:
an open file-like object.
- Raises:
NotImplementedError – always, as this must be implemented by subclasses.
- class labthings_fastapi.outputs.blob.RemoteBlobData(media_type: str, href: str, client: httpx.Client | None = None)
Bases:
BlobDataA BlobData subclass that references remote data via a URL.
This
BlobDataimplementation will download data lazily, and provides it in the three ways defined byBlobData. It does not cache downloaded data: if thecontentattribute is accessed multiple times, the data will be downloaded again each time.Note
This class is rarely instantiated directly. It is usually best to use
Blob.from_urlon aBlobsubclass.Create a reference to remote
Blobdata.- Parameters:
media_type – the MIME type of the data.
href – the URL where it may be downloaded.
client – if supplied, this
httpx.Clientwill be used to download the data.
- _href
- _client
- save(filepath: str) None
Save the output to a file.
Note that the current implementation retrieves the data into memory in its entirety, and saves to file afterwards.
- Parameters:
filepath – the file will be saved at this location.
- open() io.IOBase
Open the output as a binary file-like object.
Internally, this will download the file to memory, and wrap the resulting
bytesobject in anio.BytesIOobject to allow it to function as a file-like object.To work with the data on disk, use
saveinstead.- Returns:
a file-like object containing the downloaded data.
- class labthings_fastapi.outputs.blob.LocalBlobData(media_type: str)
Bases:
BlobDataA BlobData subclass where the data is stored locally.
Blobobjects can reference data by a URL, or can wrap data held in memory or on disk. For the non-URL options, we need to register the data with theBlobManagerand allow it to be downloaded. This class takes care of registering with theBlobManagerand adds theresponsemethod that must be overridden by subclasses to allow downloading.See
BlobBytesorBlobFilefor concrete implementations.Initialise the
LocalBlobDataobject.- Parameters:
media_type – the MIME type of the data.
- _all_blobdata: ClassVar[weakref.WeakValueDictionary[uuid.UUID, LocalBlobData]]
A way to retrieve
LocalBlobDataobjects by their ID.Note that this does not interfere with garbage collection, as it only holds weak references to the
LocalBlobDataobjects.
- _id
- classmethod from_id(id: uuid.UUID) LocalBlobData
Retrieve a
LocalBlobDataobject by its ID.Note that this does not imply
LocalBlobDataobjects are permanently stored: if there are no strong references to the object, it may have been garbage collected and will no longer be available.- Parameters:
id – the UUID of the desired
LocalBlobDataobject.- Returns:
the corresponding
LocalBlobDataobject.- Raises:
KeyError – if no such object exists.
- classmethod all_ids() list[uuid.UUID]
Return a list of all currently registered BlobData IDs.
- Returns:
a list of UUIDs for all registered
LocalBlobDataobjects.
- property id: uuid.UUID
A unique identifier for this BlobData object.
The ID is set when the BlobData object is added to the
BlobDataManagerduring initialisation.
- get_href() str
Return a URL where this data may be downloaded.
Note that this should only be called in a response handler, as it relies on
url_for.url_for.- Returns:
the URL as a string.
- abstract response() fastapi.responses.Response
Return a`fastapi.Response` object that sends binary data.
- Returns:
a response that streams the data from disk or memory.
- Raises:
NotImplementedError – always, as this must be implemented by subclasses.
- class labthings_fastapi.outputs.blob.BlobBytes(data: bytes, media_type: str)
Bases:
LocalBlobDataA
Blobthat holds its data in memory as abytesobject.Blobobjects use objects conforming to theBlobDataprotocol to store their data either on disk or in a file. This implements the protocol using abytesobject in memory.Note
This class is rarely instantiated directly. It is usually best to use
Blob.from_byteson aBlobsubclass.Create a
BlobBytesobject.Note
This class is rarely instantiated directly. It is usually best to use
Blob.from_byteson aBlobsubclass.- Parameters:
data – is the data to be wrapped.
media_type – is the MIME type of the data.
- _bytes
- save(filename: str) None
Save the wrapped data to a file.
- Parameters:
filename – where to save the data.
- open() io.IOBase
Return an open file-like object containing the data.
This wraps the underlying
bytesin anio.BytesIO.- Returns:
an
io.BytesIOobject wrapping the data.
- response() fastapi.responses.Response
Send the underlying data over the network.
- Returns:
a response that streams the data from memory.
- class labthings_fastapi.outputs.blob.BlobFile(file_path: str, media_type: str, **kwargs: Any)
Bases:
LocalBlobDataA
BlobDatabacked by a file on disk.Only the filepath is retained by default. If you are using e.g. a temporary directory, you should add the
TemporaryDirectoryas an instance attribute, to stop it being garbage collected. SeeBlob.from_temporary_directory.Note
This class is rarely instantiated directly. It is usually best to use
Blob.from_fileon aBlobsubclass.Create a
BlobFileto wrap data stored on disk.BlobFileobjects wrap data stored on disk as files. They are not usually instantiated directly, but made usingBlob.from_temporary_directoryorBlob.from_file.- Parameters:
file_path – is the path to the file.
media_type – is the MIME type of the data.
**kwargs – will be added to the object as instance attributes. This may be used to stop temporary directories from being garbage collected while the
Blobexists.
- Raises:
IOError – if the file specified does not exist.
- _file_path
- property content: bytes
The wrapped data, as a
bytesobject in memory.This reads the file on disk into a
bytesobject.- Returns:
the contents of the file in a
bytesobject.
- save(filename: str) None
Save the wrapped data to a file.
BlobFileobjects already store their data on disk. Currently, this method copies the file to the given filename. In the future, this may change tomovefor increased efficiency.- Parameters:
filename – the path where the file should be saved.
- open() io.IOBase
Return an open file-like object containing the data.
In the case of
BlobFile, this is an open file handle to the underlying file, which is where the data is already stored. It is opened with mode"rb"i.e. read-only and binary.- Returns:
an open file handle.
- response() fastapi.responses.Response
Generate a response allowing the file to be downloaded.
- Returns:
a response that streams the file from disk.
- class labthings_fastapi.outputs.blob.BlobModel(/, **data: Any)
Bases:
pydantic.BaseModelA model for JSON-serialised
Blobobjects.This model describes the JSON representation of a
Bloband does not offer any useful functionality.Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.
- labthings_fastapi.outputs.blob.parse_media_type(media_type: str) tuple[str, str]
Parse a media type string into its type and subtype.
- Parameters:
media_type – the media type string to parse.
- Returns:
a tuple of (type, subtype) where each is a string or None.
- Raises:
ValueError – if the media type is invalid.
- labthings_fastapi.outputs.blob.match_media_types(media_type: str, pattern: str) bool
Check if a media type matches a pattern.
The pattern may include wildcards, e.g.
image/*or*/*.- Parameters:
media_type – the media type to check.
pattern – the pattern to match against.
- Returns:
True if the media type matches the pattern, False otherwise.
- class labthings_fastapi.outputs.blob.Blob(data: BlobData, description: str | None = None)
A container for binary data that may be retrieved over HTTP.
See Blob input/output for more information on how to use this class.
A
Blobmay be created to hold data using the class methodsBlob.from_bytes,Blob.from_fileorBlob.from_temporary_directory. It may also reference remote data, usingBlob.from_url, though this is currently only used on the client side. The constructor requires aBlobDatainstance, so the methods mentioned previously are likely a more convenient way to instantiate aBlob.You are strongly advised to use a subclass of this class that specifies the
Blob.media_typeattribute, as this will propagate to the auto-generated documentation and make the return type of your action clearer.This class is
pydanticcompatible, in that it provides a schema, validator and serialiser. However, it may useurl_for.url_forduring serialisation, so it should only be serialised in a request handler function. This functionality is intended for use by LabThings library functions only. Validation and serialisation behaviour is described in the docstrings ofBlob._validateandBlob._serialise.Create a
Blobobject wrapping the given data.- Parameters:
data – the
BlobDataobject that stores the data.description – an optional description of the blob.
- Raises:
ValueError – if the media_type of the data does not match the media_type of the
Blobsubclass.
- classmethod __get_pydantic_core_schema__(source: type[Any], handler: pydantic.GetCoreSchemaHandler) pydantic_core.core_schema.CoreSchema
Get the pydantic core schema for this type.
This magic method allows
pydanticto serialiseBlobinstances, and generate a JSONSchema for them.We tell
pydanticto base its handling ofBlobon theBlobModelschema, with custom validation and serialisation. Validation and serialisation behaviour is described in the docstrings ofBlob._validateandBlob._serialise.The JSONSchema is generated for
BlobModelbut is then refined in__get_pydantic_json_schema__to include themedia_typeanddescriptiondefaults.- Parameters:
source – The source type being converted.
handler – The pydantic core schema handler.
- Returns:
The pydantic core schema for the URLFor type.
- classmethod __get_pydantic_json_schema__(core_schema: Blob.__get_pydantic_json_schema__.core_schema, handler: pydantic.GetJsonSchemaHandler) pydantic.json_schema.JsonSchemaValue
Customise the JSON Schema to include the media_type.
- Parameters:
core_schema – The core schema for the Blob type.
handler – The pydantic JSON schema handler.
- Returns:
The JSON schema for the Blob type, with media_type included.
- classmethod _validate(value: Any, handler: collections.abc.Callable[[Any], BlobModel]) Self
Validate and convert a value to a
Blobinstance.- Parameters:
value – The input value, as passed in or loaded from JSON.
handler – A function that runs the validation logic of BlobModel.
If the value is already a
Blob, it will be returned directly. Otherwise, we first validate the input using theBlobModelschema.When a
Blobis validated, we check to see if the URL given as itshreflooks like aBlobdownload URL on this server. If it does, the returned object will hold a reference to the local data.If we can’t match the URL to a
Blobon this server, we will raise an error. Handling ofBlobinput is currently experimental, and limited to passing the output of one Action as input to a subsequent one.- Returns:
a
Blobobject pointing to the data.- Raises:
ValueError – if the
hrefdoes not contain a valid Blob ID, or if the Blob ID is not found on this server.
- classmethod _serialise(obj: Self, handler: collections.abc.Callable[[BlobModel], Mapping[str, str]]) Mapping[str, str]
Serialise the Blob to a dictionary.
See
Blob.to_blobmodelfor a description of how we serialise.- Parameters:
obj – the
Blobinstance to serialise.handler – the handler (provided by pydantic) takes a BlobModel and converts it to a dictionary. The handler runs the serialiser of the core schema we’ve wrapped, in this case the BlobModel serialiser.
- Returns:
a JSON-serialisable dictionary with a URL that allows the
Blobto be downloaded from theBlobManager.
- to_blobmodel() BlobModel
Represent the
Blobas aBlobModelto get ready to serialise.When
pydanticserialises this object, we first generate aBlobModelwith just the information to be serialised. We usefrom_url.from_urlto generate the URL, so this will error if it is serialised anywhere other than a request handler with the middleware frommiddleware.url_forenabled.- Returns:
a JSON-serialisable dictionary with a URL that allows the
Blobto be downloaded from theBlobManager.
- property data: BlobData
The data store for this Blob.
It is recommended to use the
Blob.contentproperty orBlob.saveorBlob.openmethods rather than accessing this property directly.- Returns:
the data store wrapping data on disk or in memory.
- property content: bytes
Return the the output as a
bytesobject.This property may return the
bytesobject, or if we have a file it will read the file and return the contents. Client objects may use this property to download the output.This property is read-only. You should also only read it once, as no guarantees are given about caching - reading it many times risks reading the file from disk many times, or re-downloading an artifact.
- Returns:
a
bytesobject containing the data.
- save(filepath: str) None
Save the output to a file.
This may remove the need to hold the output in memory, especially if it is already stored on disk.
- Parameters:
filepath – The location to save the data on disk.
- open() io.IOBase
Open the data as a binary file-like object.
This will return a file-like object that may be read from. It may be either on disk (i.e. an open file handle) or in memory (e.g. an
io.BytesIOwrapper).- Returns:
a binary file-like object.
- classmethod from_bytes(data: bytes) Self
Create a
Blobfrom a bytes object.This is the recommended way to create a
Blobfrom data that is held in memory. It should ideally be called on a subclass that has set themedia_type.
- classmethod from_temporary_directory(folder: tempfile.TemporaryDirectory, file: str) Self
Create a
Blobfrom a file in a temporary directory.This is the recommended way to create a
Blobfrom data that is saved to a file, when the file should not be retained. It should ideally be called on a subclass that has set themedia_type.The
tempfile.TemporaryDirectoryobject will persist as long as thisBlobdoes, which will prevent it from being cleaned up until the object is garbage collected. This means the file will stay on disk until it is no longer needed.- Parameters:
folder – a
tempfile.TemporaryDirectorywhere the file is saved.file – the path to the file, relative to the
folder.
- Returns:
a
Blobwrapping the file.
- classmethod from_file(file: str) Self
Create a
Blobfrom a regular file.This is the recommended way to create a
Blobfrom a file, if that file will persist on disk. It should ideally be called on a subclass ofBlobthat has setmedia_type.Note
The file should exist for at least as long as the
Blobdoes; this is assumed to be the case and nothing is done to ensure it’s not temporary. If you are using temporary files, consider creating yourBlobwithfrom_temporary_directoryinstead.- Parameters:
file – is the path to the file. This file must exist.
- Returns:
a
Blobobject referencing the specified file.
- classmethod from_url(href: str, client: httpx.Client | None = None) Self
Create a
Blobthat references data at a URL.This is the recommended way to create a
Blobthat references data held remotely. It should ideally be called on a subclass ofBlobthat has setmedia_type.- Parameters:
href – the URL where the data may be downloaded.
client – if supplied, this
httpx.Clientwill be used to download the data.
- Returns:
a
Blobobject referencing the specified URL.
- response() fastapi.responses.Response
Return a suitable response for serving the output.
This method is called by the
ThingServerto generate a response that returns the data over HTTP.- Returns:
an HTTP response that streams data from memory or file.
- Raises:
NotImplementedError – if the data is not local. It’s not currently possible to serve remote data via the
BlobManager.
- labthings_fastapi.outputs.blob.router
A FastAPI router for BlobData download endpoints.
- labthings_fastapi.outputs.blob.download_blob(blob_id: uuid.UUID) fastapi.responses.Response
Download a
Blob.This function returns a
fastapi.Responseallowing the data to be downloaded, using theLocalBlobData.responsemethod.- Parameters:
blob_id – the unique ID of the blob data.
- Returns:
a
fastapi.Responseobject that will send the content of the blob over HTTP.- Raises:
HTTPException – if the requested blob is not found.
- labthings_fastapi.outputs.blob.url_to_id(url: str) uuid.UUID | None
Extract the blob ID from a URL.
Currently, this checks for a UUID at the end of a URL. In the future, it might check if the URL refers to this server.
- Parameters:
url – a URL previously generated by
blobdata_to_url.- Returns:
the UUID blob ID extracted from the URL.