Skip to main content

A Django extension for implementing the OGC SensorThings API

Project description

Django OGC SensorThings

A Django extension that adds OGC SensorThings API v1.1 support to your project. It provides a fully compliant REST API for managing sensor data, built on top of Django Ninja and Pydantic.

Features

  • Full SensorThings API v1.1 compliance (Things, Locations, Datastreams, Observations, and more)
  • Built-in Django ORM backend
  • Optional extensions: Data Array, MultiDatastream
  • Extensible backend adapter interface for custom storage layers

Installation

pip install django-ogc-sensorthings

Quick Start

1. Add the apps to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "sensorthings.versions.v1_1",
    "sensorthings.versions.v1_1.backends.django",
]

To enable optional extensions, add them as well:

    "sensorthings.versions.v1_1.extensions.dataarray",

2. Configure the library in your settings:

from uuid import UUID

SENSORTHINGS_V1_1_SERVICE_URL = "http://localhost:8000/sensorthings"
SENSORTHINGS_V1_1_BACKEND_ADAPTER = "sensorthings.versions.v1_1.backends.django.adapter.DjangoBackendAdapter"
SENSORTHINGS_V1_1_ID_TYPE = UUID
SENSORTHINGS_V1_1_ID_DELIMITER = "'"

3. Include the URL patterns:

from django.urls import path, include

urlpatterns = [
    ...
    path("sensorthings/", include("sensorthings.versions.v1_1.urls")),
]

4. Run migrations:

python manage.py migrate

The API will be available at http://localhost:8000/sensorthings/v1.1/.

Interactive API docs (Swagger UI) are served at http://localhost:8000/sensorthings/v1.1/docs.

Configuration Reference

Setting Description
SENSORTHINGS_V1_1_SERVICE_URL Base URL of the service, used when building self links and navigation links
SENSORTHINGS_V1_1_BACKEND_ADAPTER Dotted path to the backend adapter class
SENSORTHINGS_V1_1_ID_TYPE Python type used for entity IDs (e.g. UUID, int)
SENSORTHINGS_V1_1_ID_DELIMITER Delimiter wrapping IDs in URLs (e.g. ' for Things('id'))
SENSORTHINGS_V1_1_ID_EXAMPLE Example ID value shown in Swagger UI (auto-detected from ID_TYPE if not set)
SENSORTHINGS_V1_1_MAX_TOP Maximum number of entities returned in a single collection response (default: 100)
SENSORTHINGS_V1_1_RENDERER Custom Django Ninja renderer instance (subclass of ninja.renderers.BaseRenderer); defaults to JSONRenderer
SENSORTHINGS_V1_1_DOCS_ENABLED Whether to expose the Swagger UI and OpenAPI schema endpoints (default: True)
SENSORTHINGS_V1_1_DEFAULT_AUTH_HANDLER See Authentication and Authorization
SENSORTHINGS_V1_1_AUTH_HANDLERS See Authentication and Authorization
SENSORTHINGS_V1_1_PROPERTIES_SCHEMAS See Custom Properties Schemas

Authentication and Authorization

Authentication and authorization are handled at two separate layers.

Authentication

Authentication is configured via Django settings using Django Ninja's auth system. Define one or more auth classes and assign them to the DEFAULT_AUTH_HANDLER setting to protect all endpoints globally:

from ninja.security import HttpBearer

class BearerAuth(HttpBearer):
    def authenticate(self, request, token):
        user = validate_token(token)
        if user:
            return user

SENSORTHINGS_V1_1_DEFAULT_AUTH_HANDLER = [BearerAuth()]

To require different auth on specific operations — for example, public reads with authenticated writes — use AUTH_HANDLERS with the operation name as the key:

SENSORTHINGS_V1_1_AUTH_HANDLERS = {
    "create_thing_entity": [BearerAuth()],
    "update_thing_entity": [BearerAuth()],
    "delete_thing_entity": [BearerAuth()],
}

Operation names follow the pattern {verb}_{entity}_{collection|entity}, for example get_thing_collection, create_observation_entity, delete_datastream_entity. Any operation not listed in AUTH_HANDLERS falls back to DEFAULT_AUTH_HANDLER.

Authorization

Row-level access control — filtering results by owner, organization, or any other data-level constraint — belongs in the backend adapter via the context parameter, which is the Django HttpRequest passed to every adapter method:

async def get_things(self, ..., context=None):
    return query_things(owner=context.user)

This keeps identity verification at the view layer and data scoping at the data layer.

Backend Adapters

The backend adapter is the bridge between the SensorThings API layer and your data. The built-in Django ORM backend is suitable for new projects, but the main use case for this library is layering a compliant SensorThings API over an existing environmental sensor database — in which case you implement a custom adapter that maps your existing schema to the SensorThings data model.

Implementing a Custom Adapter

Subclass the BaseBackendAdapter for the API version you are targeting and implement its abstract methods:

from sensorthings.versions.v1_1.backends.base import BaseBackendAdapter
from sensorthings.versions.v1_1.dto import (
    EntityResultSetDTO, ThingDTO, ObservationDTO, ...
)

class MyBackendAdapter(BaseBackendAdapter):
    ...

Each entity type has four operations: get_*, create_*, update_*, and delete_*. You only need to implement the ones your deployment supports — raise NotImplementedError for any you intentionally omit. Point the BACKEND_ADAPTER setting to your class using a dotted path:

SENSORTHINGS_V1_1_BACKEND_ADAPTER = "myapp.adapters.MyBackendAdapter"

Operation Signatures

get_* — fetch a collection of entities:

async def get_things(
    self,
    filters=None,       # parsed OData $filter AST node, or None
    orderby=None,       # list of OrderByField, or None
    group_by=None,      # tuple of (field_name, [ids]), or None
    select=None,        # list of field names to fetch, or None (fetch all)
    top=100,            # maximum number of results
    skip=0,             # offset
    count=False,        # whether to include total count
    context=None,       # the Django HttpRequest — use for auth, tenancy, etc.
) -> EntityResultSetDTO[ThingDTO]:
    ...

create_* — create one or more entities, return their IDs:

async def create_things(
    self,
    payload: list[ThingDTO],
    context=None,
) -> list[app_settings.ID_TYPE]:
    ...

update_* — apply partial updates:

async def update_things(
    self,
    payload: dict[app_settings.ID_TYPE, ThingDTO],
    context=None,
) -> None:
    ...

delete_* — delete by ID:

async def delete_things(
    self,
    entity_ids: list[app_settings.ID_TYPE],
    context=None,
) -> None:
    ...

Both sync and async implementations are supported. The service layer automatically wraps synchronous methods in sync_to_async.

Returning Results from get_*

get_* methods return an EntityResultSetDTO, which separates entity objects from collection membership to avoid duplication when grouping.

For a standard (ungrouped) query, use the "__UNGROUPED__" key:

from sensorthings.versions.v1_1.dto import EntityResultSetDTO, CollectionDTO, ThingDTO

return EntityResultSetDTO(
    collections={
        "__UNGROUPED__": CollectionDTO(
            entity_count=total if count else None,
            entity_ids=[t.id for t in results],
        )
    },
    entities={t.id: ThingDTO(id=t.id, name=t.name, description=t.description) for t in results},
)

When group_by=(field_name, ids) is provided — used internally for $expand and nested resource paths — key each collection by the parent entity ID:

return EntityResultSetDTO(
    collections={
        parent_id: CollectionDTO(entity_ids=[...])
        for parent_id in requested_ids
    },
    entities={obs.id: ObservationDTO(...) for obs in results},
)

Use Absent (from sensorthings.types) rather than None for DTO fields that were not requested via $select, so they are omitted from the response rather than serialized as null.

Why Absent and not None? Python has no built-in concept of "not provided but also not null". Using None as a sentinel would type fields as nullable, which is incorrect — most SensorThings fields are non-nullable but conditionally omittable (e.g. omitted from a $select response, or not set in a PATCH body). Absent allows fields to carry their correct non-nullable type while still being excluded from serialization when not present.

Transactions

Write operations (create_entity, update_entity, delete_entity) are automatically wrapped in a transaction using the transaction() context manager on your adapter. The built-in Django adapter implements this with transaction.atomic(), making deep inserts atomic by default.

Custom adapters inherit a no-op by default. Override transaction() to add transaction support for your storage layer:

from contextlib import asynccontextmanager

class MyAdapter(BaseBackendAdapter):

    @asynccontextmanager
    async def transaction(self):
        async with self.session.begin():
            yield

For sync adapters, wrap a sync context manager inside @asynccontextmanager:

@asynccontextmanager
async def transaction(self):
    with my_sync_transaction():
        yield

Customizing Server Settings

Override get_server_settings() to control the conformance list advertised at the service root — for example if your adapter does not support write operations:

def get_server_settings(self) -> dict:
    settings = super().get_server_settings()
    settings["conformance"] = [
        uri for uri in settings["conformance"]
        if "create-update-delete" not in uri
    ]
    return settings

Custom Properties Schemas

By default, the properties field on each entity (and the parameters field on Observation) is typed as a plain dict. Use SENSORTHINGS_V1_1_PROPERTIES_SCHEMAS to replace this with a typed Pydantic model for any entity, which will be enforced on both request validation and response serialization:

from pydantic import BaseModel

class ThingProperties(BaseModel):
    deployment_id: str
    site_code: str
    active: bool = True

SENSORTHINGS_V1_1_PROPERTIES_SCHEMAS = {
    "Things": ThingProperties,
}

The valid keys, one per entity type, are:

Key Entity
"Things" Thing
"Locations" Location
"Datastreams" Datastream
"Sensors" Sensor
"ObservedProperties" ObservedProperty
"Observations" Observation (parameters field)
"FeaturesOfInterest" FeatureOfInterest
"MultiDatastreams" MultiDatastream (requires MultiDatastream extension)

Any entity not listed in the dict keeps its default dict type.

Note: PROPERTIES_SCHEMAS values are compiled into the API schemas at app load time. The assigned value must be present in Django settings before app startup — it cannot be changed at runtime.

Customizing Field Types

Observation Result Type

By default, the result field on Observation is typed as float. Use OBSERVATION_TYPE_SCHEMA to change the accepted Python type:

from typing import Union

SENSORTHINGS_V1_1_OBSERVATION_TYPE_SCHEMA = Union[float, str, bool]

Use OBSERVATION_TYPE_VALUE_LITERAL to restrict the set of allowed observationType URIs on Datastream:

from typing import Literal

SENSORTHINGS_V1_1_OBSERVATION_TYPE_VALUE_LITERAL = Literal[
    "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement",
]

Encoding Types

Three entity types carry an encodingType field alongside an associated data field (location, metadata, or feature). Each has a pair of settings: one for the Python type of the data field and one for the allowed encodingType values.

Setting Affects Default
SENSORTHINGS_V1_1_LOCATION_ENCODING_TYPE_SCHEMA Location location field type dict
SENSORTHINGS_V1_1_LOCATION_ENCODING_TYPE_VALUE_LITERAL Allowed Location encodingType values "application/geo+json"
SENSORTHINGS_V1_1_SENSOR_METADATA_ENCODING_TYPE_SCHEMA Sensor metadata field type str
SENSORTHINGS_V1_1_SENSOR_METADATA_ENCODING_TYPE_VALUE_LITERAL Allowed Sensor encodingType values "application/pdf", SensorML 2.0, "text/html"
SENSORTHINGS_V1_1_FEATURE_OF_INTEREST_ENCODING_TYPE_SCHEMA FeatureOfInterest feature field type dict
SENSORTHINGS_V1_1_FEATURE_OF_INTEREST_ENCODING_TYPE_VALUE_LITERAL Allowed FeatureOfInterest encodingType values "application/geo+json"

Example — enforce a typed GeoJSON structure on FeatureOfInterest:

from typing import Literal
from pydantic import BaseModel

class GeoJSONFeature(BaseModel):
    type: str
    geometry: dict
    properties: dict

SENSORTHINGS_V1_1_FEATURE_OF_INTEREST_ENCODING_TYPE_SCHEMA = GeoJSONFeature
SENSORTHINGS_V1_1_FEATURE_OF_INTEREST_ENCODING_TYPE_VALUE_LITERAL = Literal["application/geo+json"]

Note: Like PROPERTIES_SCHEMAS, these values must be present in Django settings before app startup and cannot be changed at runtime.

Extensions

Data Array

Adds the $resultFormat=dataArray query parameter to the Observations endpoint, which returns observations grouped by Datastream in a compact array format.

INSTALLED_APPS = [
    ...
    "sensorthings.versions.v1_1.extensions.dataarray",
]

MultiDatastream

Adds support for the MultiDatastream entity type, which associates a single Datastream with multiple observed properties.

INSTALLED_APPS = [
    ...
    "sensorthings.versions.v1_1.extensions.multidatastream",
]

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_ogc_sensorthings-1.0.0.tar.gz (52.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_ogc_sensorthings-1.0.0-py3-none-any.whl (84.5 kB view details)

Uploaded Python 3

File details

Details for the file django_ogc_sensorthings-1.0.0.tar.gz.

File metadata

  • Download URL: django_ogc_sensorthings-1.0.0.tar.gz
  • Upload date:
  • Size: 52.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for django_ogc_sensorthings-1.0.0.tar.gz
Algorithm Hash digest
SHA256 86904147624e796da5a5e51a4f3ccce1b6b3c0f37cdfb945aa0fcba38c4435e0
MD5 e07d4b3adda7733d463718db1d1137ac
BLAKE2b-256 152f29324f2d64855561d3162ed6f92c959f17293c0a432e9b5afa6b638537c7

See more details on using hashes here.

File details

Details for the file django_ogc_sensorthings-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_ogc_sensorthings-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8943a0a655cb738927cdc2523f7b29692872d378571c09db7d7cc5f05dbf8d75
MD5 597d3683e59d6d109656713cb06419d1
BLAKE2b-256 16d1bc1792db273b101ea4db474b80610bf17958fae17bc5ce83831148c0bc0a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page