PhysicalAsset#

class rapidtools.core.PhysicalAsset(*, id: str, geometry: BaseGeometry, attributes: dict[str, ~typing.Any]=<factory>, image_assets: ImageCollection = <factory>)[source]#

Bases: object

Represents a tangible, real-world entity.

A PhysicalAsset serves as the digital twin for a real-world object (e.g., a utility pole, building, or road segment). It acts as a container combining:

  1. Spatial Data: Where it is located (Shapely geometry).

  2. Metadata: What describes it (Attributes dictionary).

  3. Media: Visual records of it (List of ImageAssets).

This distinguishes the physical object itself from the digital media (ImageAsset) used to document it.

Example

To create a simple PhysicalAsset for a utility pole:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>>
>>> my_asset = PhysicalAsset(
...     id='pole_123',
...     geometry=Point(10.0, 20.0),
...     attributes={'type': 'utility_pole', 'material': 'wood'}
... )

Methods

add_attributes

Add or update the asset’s attributes using values from a dictionary.

add_image_assets

Add one or more ImageAsset objects to the asset.

from_geojson_feature

Create a PhysicalAsset from a GeoJSON feature dictionary.

get_attributes

Retrieve one or more descriptive attributes from the asset.

get_image_assets

Retrieve specific image assets by their ID or filename.

remove_attributes

Remove one or more attributes from the asset by key.

remove_image_assets

Removes image assets identified by their unique string ID or filename.

summary

Print a human-readable summary of the asset information.

to_geojson_feature

Converts the asset into a GeoJSON feature dictionary.

property asset_type: str | None#

A convenience property to get the asset type from attributes.

It searches a predefined list of keys (e.g., 'asset_type', 'type') and returns the value of the first key found. Returns None if none of the keys are present.

Example

Create an asset using the key 'category' for its type:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>> asset = PhysicalAsset(
...     id='bldg_01',
...     geometry=Point(0, 0),
...     attributes={'category': 'commercial', 'floors': 5}
... )

The property automatically finds 'commercial' despite the key being 'category':

>>> print(asset.asset_type)
commercial
add_attributes(new_attributes: dict[str, Any], overwrite: bool = False) → None[source]#

Add or update the asset’s attributes using values from a dictionary.

By default, existing keys are not overwritten. Set overwrite=True to replace existing values.

Parameters:
  • new_attributes (dict[str, Any]) – A mapping of attribute names to values to add.

  • overwrite (bool) – If True, existing attributes with the same keys will be overwritten. If False, existing keys will be left unchanged and a log message will be emitted.

Raises:

TypeError – If new_attributes is not a dictionary.

Example

Initialize an asset with some base attributes:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>> asset = PhysicalAsset(
...     id='utility_pole_01',
...     geometry=Point(0, 0),
...     attributes={'material': 'wood', 'status': 'active'}
... )

Add new attributes that do not overlap with the existing ones:

>>> asset.add_attributes({'install_date': '2023-01-01'})
>>> print(asset.attributes['install_date'])
2023-01-01

Attempt to add attributes where some keys are already present in the existing set without specifying the overwrite argument (i.e., default behavior):

>>> asset.add_attributes({'material': 'metal', 'height': 12.0})
INFO: Attribute 'material' already exists in asset
'utility_pole_01'. Skipping attribute as 'overwrite is set to False.
>>> print(asset.attributes['material'])
wood
>>> print(asset.attributes['height'])
12.0

Force update existing attributes by setting the overwrite argument to True:

>>> asset.add_attributes({'status': 'retired'}, overwrite=True)
>>> print(asset.attributes['status'])
retired
add_image_assets(*args: Any) → None[source]#

Add one or more ImageAsset objects to the asset.

This method is highly flexible and accepts:

  • Individual ImageAsset objects.

  • Lists or Tuples of ImageAsset objects.

  • ImageCollection instances.

  • A mix of all the above.

Any nested item that is not an ImageAsset is ignored and a warning is logged.

Parameters:

*args (Any) – Variable length argument list containing images or collections of images.

Examples

Initialize the main physical asset:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset, ImageAsset,             >>>     ImageCollection
>>>
>>> asset = PhysicalAsset(id='pole_01', geometry=Point(10, 20))

Initialize several ImageAsset and ImageCollection objects to use in the examples below. At minimum, an ImageAsset requires an id and a path. Because we will be using mock paths, we must set allow_missing_file=True; otherwise, ImageAsset will validate the path, detect that it does not exist, and raise an error. To avoid repeating the path and allow_missing_file arguments, we will define a small helper function, named make_img to construct ImageAsset instances consistently:

>>> def make_img(uid):
...     return ImageAsset(
...         id=uid,
...         path=f'/tmp/{uid}',
...         allow_missing_file=True
...     )
>>>
>>> img1 = make_img('img_1.jpg')
>>> img2 = make_img('img_2.jpg')
>>> img3 = make_img('img_3.jpg')
>>> img4 = make_img('img_4.jpg')
>>> img5 = make_img('img_5.jpg')
>>> img6 = make_img('img_6.jpg')
>>> img7 = make_img('img_7.jpg')
>>> collection = ImageCollection([img4, img5])

Add individual ImageAssets to the physical asset:

>>> asset.add_image_assets(img1, img2)
>>> len(asset.image_assets)
2

Add a list of images:

>>> asset.add_image_assets([img3])
>>> len(asset.image_assets)
3

Add a mix of a single ImageAsset, list, tuple, and ImageCollection in one call. Please note that ‘img5.jpg’ is skipped below as it is a duplicate asset:

>>> asset.add_image_assets(
...     img5,               # Single Object
...     [img6],             # List
...     (img7,),            # Tuple
...     collection          # ImageCollection (contains img4, img5)
... )
INFO: Skipping duplicate asset with ID: img_5.jpg
>>> 'img_7.jpg' in asset.image_assets.get_ids()
True
classmethod from_geojson_feature(geojson_feature: dict[str, Any], asset_id: str | None = None) → PhysicalAsset[source]#

Create a PhysicalAsset from a GeoJSON feature dictionary.

The method expects a GeoJSON object of type “Feature” with at least a “geometry” field containing a valid GeoJSON geometry.

If the feature’s properties contain an 'image_assets' list (as produced by to_geojson_feature), this method attempts to reconstruct the corresponding ImageAsset objects and populate the asset’s image collection.

If no id is found at the top level or in properties, a warning is logged and a random UUID is generated as a placeholder.

Parameters:
  • geojson_feature (dict[str, Any]) – A dictionary representing a GeoJSON feature.

  • asset_id (str or None, optional) – An explicit ID to assign to the asset. If provided, this overrides any ID found within the GeoJSON data. Defaults to None.

Returns:

PhysicalAsset – An initialized physical asset instance.

Raises:

ValueError – If geojson_feature is not a valid GeoJSON feature (i.e., its “type” is not “Feature”).

Examples

Create a PhysicalAsset from a GeoJSON feature:

>>> from rapidtools.core import PhysicalAsset
>>>
>>> feature = {
...     "type": "Feature",
...     "id": "pole_01",
...     "geometry": {"type": "Point", "coordinates": [10, 20]},
...     "properties": {
...         "material": "wood",
...         "image_assets": [
...             {"id": "img1", "path": "/tmp/a.jpg",
...              "allow_missing_file": True}
...         ]
...     }
... }
>>>
>>> asset = PhysicalAsset.from_geojson_feature(feature)

Display the asset’s ID and material properties. Note that image_assets is removed from the GeoJSON feature attributes and stored instead in the asset’s ImageCollection:

>>> print(asset.id)
pole_01
>>> print(asset.attributes['material'])
wood
>>> print('image_assets' in asset.attributes)
False
>>> print(len(asset.image_assets))
1
get_attributes(*keys: str) → dict[str, Any][source]#

Retrieve one or more descriptive attributes from the asset.

If a requested key does not exist, a warning is logged and the key is omitted from the returned dictionary.

Parameters:

*keys (str) – A variable number of keys to retrieve.

Returns:

dict[str, Any] – A dictionary containing the keys that were found and their values.

Examples

Initialize the main physical asset:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>>
>>> asset = PhysicalAsset(
...     id='bldg_001',
...     geometry=Point(0,0),
...     attributes={'year_built': 1990, 'material': 'concrete'}
... )

Retrieve a single attribute:

>>> asset.get_attributes('year_built')
{'year_built': 1990}

Request multiple attributes. Please note that 'roof_type' is missing, so it is omitted in the result:

>>> asset.get_attributes('year_built', 'roof_type', 'material')
{'year_built': 1990, 'material': 'concrete'}
get_image_assets(*identifiers: str) → list[ImageAsset][source]#

Retrieve specific image assets by their ID or filename.

This method searches the asset’s image collection. It matches against the 'id' attribute or the 'filename' (explicit or derived from path).

Parameters:

*identifiers (str) – A variable number of string IDs or filenames to retrieve.

Returns:

list[ImageAsset] – A list of the found ImageAsset objects. If a requested identifier is not found, it is skipped and a warning is logged.

Examples

Setup asset with images:

>>> from rapidtools.core import PhysicalAsset, ImageAsset
>>> from shapely.geometry import Point
>>>
>>> asset = PhysicalAsset(id='pole_01', geometry=Point(0,0))
>>> img1 = ImageAsset(id='img_A', path='/tmp/photo_A.jpg',
...                   allow_missing_file=True)
>>> img2 = ImageAsset(id='img_B', path='/tmp/photo_B.jpg',
...                   allow_missing_file=True)
>>> asset.add_image_assets(img1, img2)

Get by ID:

>>> found = asset.get_image_assets('img_A')
>>> found[0].path
PosixPath('/tmp/photo_A.jpg')

Get by filename:

>>> found = asset.get_image_assets('photo_B.jpg')
>>> found[0].id
'img_B'

Get multiple (mixed ID and filename):

>>> found = asset.get_image_assets('img_A', 'photo_B.jpg')
>>> len(found)
2

Attempt to get non-existent image:

>>> asset.get_image_assets('missing.jpg')
WARNING: Image asset 'missing.jpg' not found in asset 'pole_01'.
[]
remove_attributes(*keys: str) → None[source]#

Remove one or more attributes from the asset by key.

For each provided key:

  • If the key exists, it is removed.

  • If the key does not exist, a warning is logged to indicate a potential typo or logic error in the caller.

Parameters:

*keys (str) – The names of the attributes to remove.

Examples

Setup a physical asset with initial attributes:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>>
>>> asset = PhysicalAsset(
...     id='bldg_123',
...     geometry=Point(0, 0),
...     attributes={'color': 'red', 'height': 50, 'year': 1990}
... )

Remove a single existing attribute:

>>> asset.remove_attributes('color')
>>> 'color' in asset.attributes
False

Attempt to remove a non-existent attribute:

>>> asset.remove_attributes('width')
WARNING: Attempted to remove non-existent attribute 'width' from
asset 'bldg_123'.
>>> print(asset.attributes)
{'height': 50, 'year': 1990}

Attempt to remove multiple (mixed existing and missing) attributes at once:

>>> asset.remove_attributes('height', 'invalid_key')
WARNING: Attempted to remove non-existent attribute 'invalid_key'
from asset 'bldg_123'.
>>> print(asset.attributes)
{'year': 1990}
remove_image_assets(*image_ids: str) → list[ImageAsset][source]#

Removes image assets identified by their unique string ID or filename.

This method searches the asset’s image collection for matches. It checks both the 'id' and 'file_name' fields of the image assets.

Parameters:

*image_ids – A variable number of string IDs (or filenames) identifying the images to remove.

Returns:

list[ImageAsset] – A list containing the actual ImageAsset objects that were successfully found and removed.

Examples

Setup asset with images:

>>> from rapidtools.core import PhysicalAsset, ImageAsset
>>> from shapely.geometry import Point
>>>
>>> asset = PhysicalAsset(id='pole_01', geometry=Point(0,0))
>>> img1 = ImageAsset(id='img_A', path='/tmp/photo_A.jpg',
... allow_missing_file=True)
>>> img2 = ImageAsset(id='img_B', path='/tmp/photo_B.jpg',
... allow_missing_file=True)
>>> img3 = ImageAsset(id='img_C', path='/tmp/photo_C.jpg',
... allow_missing_file=True)
>>> asset.add_image_assets(img1, img2, img3)

Remove by ID:

>>> removed = asset.remove_image_assets('img_A')
>>> len(asset.image_assets)
2
>>> removed[0].id
'img_A'

Remove by filename (assuming file_name is derived from path):

>>> removed = asset.remove_image_assets('photo_B.jpg')
>>> len(asset.image_assets)
1

Attempt to remove non-existent image:

>>> asset.remove_image_assets('ghost_image.jpg')
WARNING: Attempted to remove image 'ghost_image.jpg' from asset
'pole_01', but no matching asset was found.
[]
summary() → None[source]#

Print a human-readable summary of the asset information.

The summary includes:

  • Identity (ID and inferred type).

  • Spatial data (WKT geometry, truncated if excessively long).

  • Metadata (Attributes pretty-printed as JSON).

  • Media (List of associated ImageAsset representations).

Example

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset
>>>
>>> asset = PhysicalAsset(
...     id='pole_99',
...     geometry=Point(10, 20),
...     attributes={'material': 'wood', 'install_year': 2021}
... )
>>>
>>> asset.summary()
--- PhysicalAsset Summary ---
ID:          pole_99
Asset Type:  N/A
Geometry:    POINT (10 20)
Attributes (2):
{
    "material": "wood",
    "install_year": 2021
}
Image Assets (0):
  (No image assets)
-----------------------------
to_geojson_feature(ignore_properties: list[str] | None = None) → dict[str, Any][source]#

Converts the asset into a GeoJSON feature dictionary.

The asset’s 'id' becomes the feature’s 'id'. The asset’s ‘attributes’ are used as the base for the feature’s ‘properties’. The list of 'image_assets' is serialized and added to the ‘properties’ by default.

Specific properties can be excluded from the output using the ignore_properties argument, which is useful for reducing file size when large nested objects are not required.

Parameters:

ignore_properties (list[str] or None, optional) – A list of property keys to exclude from the GeoJSON properties dictionary. Commonly used to exclude 'image_assets'. Defaults to None.

Returns:

dict[str, Any] – A dictionary representing a valid GeoJSON Feature.

Examples

Create asset with an image:

>>> from shapely.geometry import Point
>>> from rapidtools.core import PhysicalAsset, ImageAsset
>>>
>>> asset = PhysicalAsset(
...     id='pole_99',
...     geometry=Point(10, 20),
...     attributes={'material': 'wood'}
... )
>>> img = ImageAsset(id='img1', path='p1.jpg',
... allow_missing_file=True)
>>> asset.add_image_assets(img)

Convert to GeoJSON:

>>> feature = asset.to_geojson_feature()
>>> print(feature)
{'type': 'Feature', 'id': 'pole_99', 'geometry':
{'type': 'Point', 'coordinates': (10.0, 20.0)},
'properties': {'material': 'wood', 'image_assets':
[{'id': 'img1', 'path': '/home/bacetiner/p1.jpg',
'properties': {}, 'semantic_map': None, 'instance_map': None,
'allow_missing_file': True}]}}