---
title: "Entities"
section: "Libraries: Python"
route: /pythonentityrequests
account: {accountId}
bruce_api: https://{accountId}.api.nextspace.host
guardian_api: https://guardian.nextspace.host
---
# Entities

Once you've configured your API instances, you can start making requests to retrieve and manipulate Entity records.

Here are a few examples on how to retrieve one or many Entity records.

```python
import bruce_models as BM

# Getting an Entity by ID.
entity = BM.Entity.get(bruce, "entity-id")

# Getting an Entity by ID with a specific scenario.
entity = BM.Entity.get(bruce, "entity-id", scenario="scenario-id-or-key")

# Getting a list of Entities under a specific Entity Type.
entities = BM.Entity.get_list(
    bruce,
    entity_type_id="entity-type-id",
    page_index=0,
    page_size=50
).get("Items")

# Getting a list of Entities under a specific Entity Type with a specific scenario.
entities = BM.Entity.get_list(
    bruce,
    entity_type_id="entity-type-id",
    scenario="scenario-id-or-key",
    page_index=0,
    page_size=50
).get("Items")
```

Here is how you can update or create Entity records.

```python
import bruce_models as BM

# If the ID inside the Entity record is not supplied (Bruce/ID), then a new record will be created.
# New records require an Entity Type ID to be specified (Bruce/EntityType.ID).

# Updating an Entity record.
updated = BM.Entity.update(bruce, entity)

# Updating an Entity with a specific scenario.
# Note that a scenario can be specified within the Entity JSON itself.
# The parameter takes precedence over the JSON.
updated = BM.Entity.update(bruce, entity, scenario="scenario-id-or-key")

# Updating a list of Entities.
updated = BM.Entity.update_list(bruce, entities).get("Items")

# Updating a list of Entities with a specific scenario.
# Note that a scenario can be specified within the Entity JSON itself.
# The parameter takes precedence over the JSON.
updated = BM.Entity.update_list(bruce, entities, scenario="scenario-id-or-key").get("Items")
```

API responses typically include trace, warnings, and errors alongside the returned data. For singular Entity requests, these are available under the `Bruce` property, for lists they are available at the root level as the Entity data is underneath the `Items` property.

```python
import bruce_models as BM

entity = BM.Entity.get(bruce, "entity-id")

# Please note that these properties are only available when there is something to report.
# Access them directly when it comes to lists of Entities. Eg: response.get("Error").

# Errors encountered that didn't stop the request.
# This usually includes errors related to propagating changes to external data sources.
errors = entity.get("Bruce").get("Error")

# Warnings encountered that didn't stop the request.
# An example is failing to create/update a Historic record because the date couldn't be resolved.
warnings = entity.get("Bruce").get("Warning")

# Trace information to see how long certain operations took.
# If you are experiencing slowness, this can give insight into where the bottleneck is.
# Eg: an external source update was slow.
trace = entity.get("Bruce").get("Trace")
```

## Reading and writing attribute values

Attribute paths are quoted and slash separated, which is what lets a single segment contain a dot or a space. `BM.PathUtils` converts between that form and a plain list, and the value helpers walk a record for you rather than making you index into nested dictionaries.

```python
import bruce_models as BM

entity = BM.Entity.get(bruce, "entity-id")

# Both forms are accepted.
name = BM.Entity.get_value(entity, '"Bruce"/"Name"')
name = BM.Entity.get_value(entity, [ "Bruce", "Name" ])

# Convert between the wire form and a list of segments.
segments = BM.PathUtils.parse('"location"/"latitude"')  # [ "location", "latitude" ]
path = BM.PathUtils.wrap([ "location", "latitude" ])    # '"location"/"latitude"'

# Writes create any missing intermediate objects.
# Note that this changes your local copy only, it does not save the record.
BM.Entity.set_value(entity, '"MyAttribute"', "my value")
BM.Entity.remove_value(entity, '"MyAttribute"')

# Save it when you are done.
updated = BM.Entity.update(bruce, entity)
```

A missing attribute reads as `None`. A stored `0`, empty string or `False` is returned as-is, so you can tell an empty value apart from an absent one.

## Where the data came from

When an Entity draws attributes from external sources, its internal data carries an `Outline` describing which source contributed what. It arrives on its own for a request by ID, or when an Entity Type is part of the filter, so there is nothing to enable.

The baseline entry is where the record itself came from, and the remaining entries list the attribute paths each source supplied. Sources that are not editable should not be offered for editing in your own tooling.

```python
import bruce_models as BM

entity = BM.Entity.get(bruce, "entity-id")
outline = entity.get("Bruce").get("Outline") or []

for source in outline:
    print(source.get("Source.Name"), source.get("Kind"))

    # The base record came from here, so no attribute list is given.
    if source.get("Baseline"):
        continue

    # Attribute paths are in the quoted form, so parse before using them.
    for raw_path in (source.get("Attribute") or []):
        path = BM.PathUtils.parse(raw_path)
        value = BM.Entity.get_value(entity, path)
        print("  ", path, "=", value, "editable:", source.get("Editable"))
```

## Historic data

Entity Types configured with historic data keep previous values against a date/time. Pass a point in time and the nearest matching historic record replaces the Entity contents. If nothing matches, the current record is returned unchanged.

A historic read reports itself in the `Outline` described above, as another source among any others. That entry carries the `DateTime` the values came from, and a `ForeignKey` naming the attribute the date/time was read from.

```python
import bruce_models as BM

# A single Entity as it was at that point in time.
entity = BM.Entity.get(
    bruce,
    "entity-id",
    historic_point="2026-01-01T00:00:00Z"
)

# The same for a list.
entities = BM.Entity.get_list(
    bruce,
    entity_type_id="entity-type-id",
    historic_point="2026-01-01T00:00:00Z"
).get("Items")

# A historic source reports itself in the Outline, alongside any other source.
# Source.Name is "Historic data", or "Historic Scenario data" under a scenario.
for source in (entity.get("Bruce").get("Outline") or []):
    if not source.get("DateTime"):
        continue
    print("Values from", source.get("DateTime"))
    # ForeignKey is the attribute holding the date/time for this record.
    print("  keyed on", BM.PathUtils.parse(source.get("ForeignKey")))
    for raw_path in (source.get("Attribute") or []):
        path = BM.PathUtils.parse(raw_path)
        print("  ", path, "=", BM.Entity.get_value(entity, path))
```

## Historic records directly

Overlaying a point in time answers "what did this Entity look like then". To work with the records themselves, reading a series or writing one, use `BM.EntityHistoricData`. It covers the whole surface, so there is no need to pick between endpoints.

A record is shaped like the Entity it belongs to: your attribute values sit at the root of `Entity`, and platform managed values such as the location sit under its `Bruce` key. That means the same object comes back from a read as goes into a write.

```python
import bruce_models as BM

ATTR_KEY = ["reading_taken"]

# Reading a page of records. Paths can be passed as segments or as a quoted string,
# and an attribute nested in a Structure is just more segments.
page = BM.EntityHistoricData.get_list(
    bruce,
    ["entity-id", "other-entity-id"],
    attr_key=ATTR_KEY,
    date_time_from="2026-01-01T00:00:00Z",
    date_time_to="2026-01-31T23:59:59Z",
    order_by="DateTime",
    order_dir="ASC",
    page_size=1000
)
for record in (page.get("Items") or []):
    values = record.get("Entity") or {}
    print(record.get("DateTime"), values.get("temperature"))

# Walking every match instead of one page, which pages for you.
records = list(BM.EntityHistoricData.iterate(
    bruce,
    ["entity-id"],
    attr_key=ATTR_KEY
))

# Records arrive interleaved when more than one Entity was asked for.
by_entity = BM.EntityHistoricData.group_by_entity_id(records)
for entity_id, entity_records in by_entity.items():
    print(entity_id, len(entity_records), "records")
```

Before paging through a long range, ask where records actually exist. `get_analysis` returns counts across the matched range rather than the records themselves.

```python
analysis = BM.EntityHistoricData.get_analysis(
    bruce,
    ["entity-id"],
    attr_key=ATTR_KEY
)
for bucket in (analysis.get("Items") or []):
    print(bucket)
```

Writing a record is how you add history the platform did not capture itself, such as a backfill from another system. `build_record` assembles one, wrapping the attribute path and nesting any platform managed values.

Note that the date/time attribute named by `attr_key` should also be present in the values, since that is the attribute the record is keyed on.

```python
records = [
    BM.EntityHistoricData.build_record(
        "entity-id",
        ATTR_KEY,
        "2026-01-01T09:00:00Z",
        {"reading_taken": "2026-01-01T09:00:00Z", "temperature": 21.4}
    ),
    # A record can carry where the Entity was, not only what it held.
    BM.EntityHistoricData.build_record(
        "entity-id",
        ATTR_KEY,
        "2026-01-02T09:00:00Z",
        {"reading_taken": "2026-01-02T09:00:00Z", "temperature": 19.1},
        internal={"Location": {"latitude": -36.8485, "longitude": 174.7633, "altitude": 0}}
    )
]

res = BM.EntityHistoricData.update(bruce, records)
print("wrote", len(res.get("Items") or []), "records")

# A record that could not be written is reported rather than failing the request,
# so check this instead of assuming every record landed.
for warning in (res.get("Warning") or []):
    print(warning)
```

There are two ways to delete. Hand back the records you read to remove exactly those, or describe a range with the same filter a read takes.

```python
# Naming the records. Only the fields identifying each one are sent, so the
# values that came back from the read are dropped rather than shipped again.
BM.EntityHistoricData.delete_records(bruce, records)

# Or a whole range. Outside a Scenario the attribute and both ends of the range
# are required, and this raises before sending anything if one is missing, which
# is what stops an accidental unbounded delete.
BM.EntityHistoricData.delete(
    bruce,
    ["entity-id"],
    attr_key=ATTR_KEY,
    date_time_from="2026-01-01T00:00:00Z",
    date_time_to="2026-01-31T23:59:59Z"
)
```

A record is identified by its Entity, its attribute and its date/time together, which is why a record read back is enough to delete it.

Every one of these accepts a `scenario`, so a series can be read, written or cleared inside a Scenario without touching the real world records. A Scenario identifies its own records, so writing into one makes the date/time and attribute optional.

---

Urls on this page are resolved for account `{accountId}`.
Site index: https://docs.nextspace.host/llms.txt · whole site in one file: https://docs.nextspace.host/llms-full.txt
Human-readable version of this page: https://docs.nextspace.host/pythonentityrequests
