Coverage for filip/models/ngsi_ld/context.py: 91%

254 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-05 11:07 +0000

1""" 

2NGSI LD models for context broker interaction 

3""" 

4 

5import logging 

6from typing import Any, List, Dict, Union, Optional 

7import re 

8from geojson_pydantic import ( 

9 Point, 

10 MultiPoint, 

11 LineString, 

12 MultiLineString, 

13 Polygon, 

14 MultiPolygon, 

15) 

16from typing_extensions import Self 

17from aenum import Enum 

18from pydantic import field_validator, ConfigDict, BaseModel, Field, model_validator 

19from filip.models.ngsi_v2 import ContextEntity 

20from filip.utils.validators import ( 

21 FiwareRegex, 

22 validate_fiware_datatype_string_protect, 

23 validate_fiware_standard_regex, 

24) 

25from pydantic_core import ValidationError 

26 

27 

28class DataTypeLD(str, Enum): 

29 """ 

30 In NGSI-LD the data types on context entities are only divided into properties and relationships. 

31 """ 

32 

33 _init_ = "value __doc__" 

34 GEOPROPERTY = "GeoProperty", "A property that represents a geometry value" 

35 PROPERTY = "Property", "All attributes that do not represent a relationship" 

36 RELATIONSHIP = ( 

37 "Relationship", 

38 "Reference to another context entity, which can be identified with a URN.", 

39 ) 

40 

41 

42# NGSI-LD entity models 

43class ContextProperty(BaseModel): 

44 """ 

45 The model for a property is represented by a JSON object with the following syntax: 

46 

47 The attribute value is specified by the value, whose value can be any data type. This does not need to be 

48 specified further. 

49 

50 The NGSI type of the attribute is fixed and does not need to be specified. 

51 Example: 

52 

53 >>> data = {"value": <...>} 

54 

55 >>> attr = ContextProperty(**data) 

56 

57 """ 

58 

59 model_config = ConfigDict(extra="allow") # In order to allow nested properties 

60 type: Optional[str] = Field(default="Property", title="type", frozen=True) 

61 value: Optional[ 

62 Union[ 

63 Union[float, int, bool, str, List, Dict[str, Any]], 

64 List[Union[float, int, bool, str, List, Dict[str, Any]]], 

65 ] 

66 ] = Field(default=None, title="Property value", description="the actual data") 

67 observedAt: Optional[str] = Field( 

68 None, 

69 title="Timestamp", 

70 description="Representing a timestamp for the " 

71 "incoming value of the property.", 

72 max_length=256, 

73 min_length=1, 

74 ) 

75 field_validator("observedAt")(validate_fiware_datatype_string_protect) 

76 

77 createdAt: Optional[str] = Field( 

78 None, 

79 title="Timestamp", 

80 description="Representing a timestamp for the " 

81 "creation time of the property.", 

82 max_length=256, 

83 min_length=1, 

84 ) 

85 field_validator("createdAt")(validate_fiware_datatype_string_protect) 

86 

87 modifiedAt: Optional[str] = Field( 

88 None, 

89 title="Timestamp", 

90 description="Representing a timestamp for the " 

91 "last modification of the property.", 

92 max_length=256, 

93 min_length=1, 

94 ) 

95 field_validator("modifiedAt")(validate_fiware_datatype_string_protect) 

96 

97 unitCode: Optional[str] = Field( 

98 None, 

99 title="Unit Code", 

100 description="Representing the unit of the value. " 

101 "Should be part of the defined units " 

102 "by the UN/ECE Recommendation No. 21" 

103 "https://unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf ", 

104 max_length=256, 

105 min_length=1, 

106 ) 

107 field_validator("unitCode")(validate_fiware_datatype_string_protect) 

108 

109 datasetId: Optional[str] = Field( 

110 None, 

111 title="dataset Id", 

112 description="It allows identifying a set or group of property values", 

113 max_length=256, 

114 min_length=1, 

115 ) 

116 field_validator("datasetId")(validate_fiware_datatype_string_protect) 

117 

118 @classmethod 

119 def get_model_fields_set(cls): 

120 """ 

121 Get all names and aliases of the model fields. 

122 """ 

123 return set( 

124 [field.validation_alias for (_, field) in cls.model_fields.items()] 

125 + [field_name for field_name in cls.model_fields] 

126 ) 

127 

128 @field_validator("type") 

129 @classmethod 

130 def check_property_type(cls, value): 

131 """ 

132 Force property type to be "Property" 

133 Args: 

134 value: value field 

135 Returns: 

136 value 

137 """ 

138 valid_property_types = ["Property", "Relationship", "TemporalProperty"] 

139 if value not in valid_property_types: 

140 msg = ( 

141 f"NGSI_LD Properties must have type {valid_property_types}, " 

142 f'not "{value}"' 

143 ) 

144 logging.warning(msg=msg) 

145 raise ValueError(msg) 

146 return value 

147 

148 

149class NamedContextProperty(ContextProperty): 

150 """ 

151 Context properties are properties of context entities. For example, the current speed of a car could be modeled 

152 as the current_speed property of the car-104 entity. 

153 

154 In the NGSI-LD data model, properties have a name, the type "property" and a value. 

155 """ 

156 

157 name: str = Field( 

158 title="Property name", 

159 description="The property name describes what kind of property the " 

160 "attribute value represents of the entity, for example " 

161 "current_speed. Allowed characters " 

162 "are the ones in the plain ASCII set, except the following " 

163 "ones: control characters, whitespace, &, ?, / and #.", 

164 max_length=256, 

165 min_length=1, 

166 ) 

167 field_validator("name")(validate_fiware_datatype_string_protect) 

168 

169 

170class ContextGeoPropertyValue(BaseModel): 

171 """ 

172 The value for a Geo property is represented by a JSON object with the following syntax: 

173 

174 A type with value "Point" and the 

175 coordinates with a list containing the coordinates as value 

176 

177 Example: 

178 "value": { 

179 "type": "Point", 

180 "coordinates": [ 

181 -3.80356167695194, 

182 43.46296641666926 

183 ] 

184 } 

185 } 

186 

187 """ 

188 

189 type: Optional[str] = Field(default=None, title="type", frozen=True) 

190 model_config = ConfigDict(extra="allow") 

191 

192 @model_validator(mode="after") 

193 def check_geoproperty_value(self) -> Self: 

194 """ 

195 Check if the value is a valid GeoProperty 

196 """ 

197 if self.model_dump().get("type") == "Point": 

198 return Point(**self.model_dump()) 

199 elif self.model_dump().get("type") == "LineString": 

200 return LineString(**self.model_dump()) 

201 elif self.model_dump().get("type") == "Polygon": 

202 return Polygon(**self.model_dump()) 

203 elif self.model_dump().get("type") == "MultiPoint": 

204 return MultiPoint(**self.model_dump()) 

205 elif self.model_dump().get("type") == "MultiLineString": 

206 return MultiLineString(**self.model_dump()) 

207 elif self.model_dump().get("type") == "MultiPolygon": 

208 return MultiPolygon(**self.model_dump()) 

209 elif self.model_dump().get("type") == "GeometryCollection": 

210 raise ValueError("GeometryCollection is not supported") 

211 

212 

213class ContextGeoProperty(BaseModel): 

214 """ 

215 The model for a Geo property is represented by a JSON object with the following syntax: 

216 

217 The attribute value is a JSON object with two contents. 

218 

219 Example: 

220 

221 { 

222 "type": "GeoProperty", 

223 "value": { 

224 "type": "Point", 

225 "coordinates": [ 

226 -3.80356167695194, 

227 43.46296641666926 

228 ] 

229 } 

230 

231 """ 

232 

233 model_config = ConfigDict(extra="allow") 

234 type: Optional[str] = Field(default="GeoProperty", title="type", frozen=True) 

235 value: Optional[ 

236 Union[ 

237 ContextGeoPropertyValue, 

238 Point, 

239 LineString, 

240 Polygon, 

241 MultiPoint, 

242 MultiPolygon, 

243 MultiLineString, 

244 ] 

245 ] = Field(default=None, title="GeoProperty value", description="the actual data") 

246 observedAt: Optional[str] = Field( 

247 default=None, 

248 title="Timestamp", 

249 description="Representing a timestamp for the " 

250 "incoming value of the property.", 

251 max_length=256, 

252 min_length=1, 

253 ) 

254 field_validator("observedAt")(validate_fiware_datatype_string_protect) 

255 

256 datasetId: Optional[str] = Field( 

257 None, 

258 title="dataset Id", 

259 description="It allows identifying a set or group of property values", 

260 max_length=256, 

261 min_length=1, 

262 ) 

263 field_validator("datasetId")(validate_fiware_datatype_string_protect) 

264 

265 

266class NamedContextGeoProperty(ContextGeoProperty): 

267 """ 

268 Context GeoProperties are geo properties of context entities. For example, the coordinates of a building . 

269 

270 In the NGSI-LD data model, properties have a name, the type "Geoproperty" and a value. 

271 """ 

272 

273 name: str = Field( 

274 title="Property name", 

275 description="The property name describes what kind of property the " 

276 "attribute value represents of the entity, for example " 

277 "current_speed. Allowed characters " 

278 "are the ones in the plain ASCII set, except the following " 

279 "ones: control characters, whitespace, &, ?, / and #.", 

280 max_length=256, 

281 min_length=1, 

282 ) 

283 field_validator("name")(validate_fiware_datatype_string_protect) 

284 

285 

286class ContextRelationship(BaseModel): 

287 """ 

288 The model for a relationship is represented by a JSON object with the following syntax: 

289 

290 The attribute value is specified by the object, whose value can be a reference to another context entity. This 

291 should be specified as the URN. The existence of this entity is not assumed. 

292 

293 The NGSI type of the attribute is fixed and does not need to be specified. 

294 

295 Example: 

296 

297 >>> data = {"object": <...>} 

298 

299 >>> attr = ContextRelationship(**data) 

300 

301 """ 

302 

303 model_config = ConfigDict(extra="allow") # In order to allow nested relationships 

304 type: Optional[str] = Field(default="Relationship", title="type", frozen=True) 

305 object: Optional[ 

306 Union[ 

307 Union[float, int, bool, str, List, Dict[str, Any]], 

308 List[Union[float, int, bool, str, List, Dict[str, Any]]], 

309 ] 

310 ] = Field( 

311 default=None, title="Realtionship object", description="the actual object id" 

312 ) 

313 

314 datasetId: Optional[str] = Field( 

315 None, 

316 title="dataset Id", 

317 description="It allows identifying a set or group of property values", 

318 max_length=256, 

319 min_length=1, 

320 ) 

321 field_validator("datasetId")(validate_fiware_datatype_string_protect) 

322 

323 observedAt: Optional[str] = Field( 

324 None, 

325 titel="Timestamp", 

326 description="Representing a timestamp for the " 

327 "incoming value of the property.", 

328 max_length=256, 

329 min_length=1, 

330 ) 

331 field_validator("observedAt")(validate_fiware_datatype_string_protect) 

332 

333 @field_validator("type") 

334 @classmethod 

335 def check_relationship_type(cls, value): 

336 """ 

337 Force property type to be "Relationship" 

338 Args: 

339 value: value field 

340 Returns: 

341 value 

342 """ 

343 if not value == "Relationship": 

344 logging.warning(msg='NGSI_LD relationships must have type "Relationship"') 

345 value = "Relationship" 

346 return value 

347 

348 

349class NamedContextRelationship(ContextRelationship): 

350 """ 

351 Context Relationship are relations of context entities to each other. 

352 For example, the current_speed of the entity car-104 could be modeled. 

353 The location could be modeled as located_in the entity Room-001. 

354 

355 In the NGSI-LD data model, relationships have a name, the type "relationship" and an object. 

356 """ 

357 

358 name: str = Field( 

359 title="Attribute name", 

360 description="The attribute name describes what kind of property the " 

361 "attribute value represents of the entity, for example " 

362 "current_speed. Allowed characters " 

363 "are the ones in the plain ASCII set, except the following " 

364 "ones: control characters, whitespace, &, ?, / and #.", 

365 max_length=256, 

366 min_length=1, 

367 # pattern=FiwareRegex.string_protect.value, 

368 # Make it FIWARE-Safe 

369 ) 

370 field_validator("name")(validate_fiware_datatype_string_protect) 

371 

372 

373class ContextLDEntityBase(BaseModel): 

374 """ 

375 Base Model for an entity is represented by a JSON object with the following 

376 syntax. 

377 

378 The entity id is specified by the object's id property, whose value 

379 is a string containing the entity id. 

380 

381 The entity type is specified by the object's type property, whose value 

382 is a string containing the entity's type name. 

383 """ 

384 

385 model_config = ConfigDict( 

386 extra="allow", validate_default=True, validate_assignment=True 

387 ) 

388 id: str = Field( 

389 ..., 

390 title="Entity Id", 

391 description="Id of an entity in an NGSI context broker. Allowed " 

392 "characters are the ones in the plain ASCII set, except " 

393 "the following ones: control characters, " 

394 "whitespace, &, ?, / and #." 

395 "the id should be structured according to the urn naming scheme.", 

396 json_schema_extra={"example": "urn:ngsi-ld:Room:001"}, 

397 max_length=256, 

398 min_length=1, 

399 # pattern=FiwareRegex.standard.value, # Make it FIWARE-Safe 

400 frozen=True, 

401 ) 

402 field_validator("id")(validate_fiware_standard_regex) 

403 type: str = Field( 

404 ..., 

405 title="Entity Type", 

406 description="Id of an entity in an NGSI context broker. " 

407 "Allowed characters are the ones in the plain ASCII set, " 

408 "except the following ones: control characters, " 

409 "whitespace, &, ?, / and #.", 

410 json_schema_extra={"example": "Room"}, 

411 max_length=256, 

412 min_length=1, 

413 # pattern=FiwareRegex.standard.value, # Make it FIWARE-Safe 

414 frozen=True, 

415 ) 

416 field_validator("type")(validate_fiware_standard_regex) 

417 context: Optional[Union[str, List[str], Dict]] = Field( 

418 title="@context", 

419 default=None, 

420 description="The @context in JSON-LD is used to expand terms, provided as short " 

421 "hand strings, to concepts, specified as URIs, and vice versa, " 

422 "to compact URIs into terms " 

423 "The main implication of NGSI-LD API is that if the @context is " 

424 "a compound one, i.e. an @context which references multiple " 

425 "individual @context, served by resources behind different URIs, " 

426 "then a wrapper @context has to be created and hosted.", 

427 examples=["https://n5geh.github.io/n5geh.test-context.io/context_saref.jsonld"], 

428 alias="@context", 

429 validation_alias="@context", 

430 frozen=False, 

431 ) 

432 

433 

434class ContextLDEntityKeyValues(ContextLDEntityBase): 

435 """ 

436 Base Model for an entity is represented by a JSON object with the following 

437 syntax. 

438 

439 The entity id is specified by the object's id property, whose value 

440 is a string containing the entity id. 

441 

442 The entity type is specified by the object's type property, whose value 

443 is a string containing the entity's type name. 

444 

445 """ 

446 

447 model_config = ConfigDict( 

448 extra="allow", validate_default=True, validate_assignment=True 

449 ) 

450 

451 def to_entity(self): 

452 """ 

453 Convert the entity to a normalized representation. 

454 """ 

455 return ContextLDEntity( 

456 **{ 

457 "id": self.id, 

458 "type": self.type, 

459 "context": self.context if self.context else None, 

460 **{ 

461 key: { 

462 "type": "Property", 

463 "value": value, 

464 } 

465 for key, value in self.model_dump().items() 

466 if key not in ["id", "type", "context"] 

467 }, 

468 } 

469 ) 

470 

471 

472class PropertyFormat(str, Enum): 

473 """ 

474 Format to decide if properties of ContextEntity class are returned as 

475 List of NamedContextAttributes or as Dict of ContextAttributes. 

476 """ 

477 

478 LIST = "list" 

479 DICT = "dict" 

480 

481 

482class ContextLDEntity(ContextLDEntityBase): 

483 """ 

484 Context LD entities, or simply entities, are the center of gravity in the 

485 FIWARE NGSI-LD information model. An entity represents a thing, i.e., any 

486 physical or logical object (e.g., a sensor, a person, a room, an issue in 

487 a ticketing system, etc.). Each entity has an entity id. 

488 Furthermore, the type system of FIWARE NGSI enables entities to have an 

489 entity type. Entity types are semantic types; they are intended to describe 

490 the type of thing represented by the entity. For example, a context 

491 entity #with id sensor-365 could have the type temperatureSensor. 

492 

493 Each entity is uniquely identified by its id. 

494 

495 The entity id is specified by the object's id property, whose value 

496 is a string containing the entity id. 

497 

498 The entity type is specified by the object's type property, whose value 

499 is a string containing the entity's type name. 

500 

501 Entity attributes are specified by additional properties and relationships, whose names are 

502 the name of the attribute and whose representation is described in the 

503 "ContextProperty"/"ContextRelationship"-model. Obviously, id and type are 

504 not allowed to be used as attribute names. 

505 

506 Example: 

507 

508 >>> data = {'id': 'MyId', 

509 'type': 'MyType', 

510 'my_attr': {'value': 20}} 

511 

512 >>> entity = ContextLDEntity(**data) 

513 

514 """ 

515 

516 model_config = ConfigDict( 

517 extra="allow", 

518 validate_default=True, 

519 validate_assignment=True, 

520 populate_by_name=True, 

521 ) 

522 

523 observationSpace: Optional[ContextGeoProperty] = Field( 

524 default=None, 

525 title="Observation Space", 

526 description="The geospatial Property representing " 

527 "the geographic location that is being " 

528 "observed, e.g. by a sensor. " 

529 "For example, in the case of a camera, " 

530 "the location of the camera and the " 

531 "observationspace are different and " 

532 "can be disjoint. ", 

533 ) 

534 

535 @field_validator("context") 

536 @classmethod 

537 def return_context(cls, context): 

538 return context 

539 

540 operationSpace: Optional[ContextGeoProperty] = Field( 

541 default=None, 

542 title="Operation Space", 

543 description="The geospatial Property representing " 

544 "the geographic location in which an " 

545 "Entity,e.g. an actuator is active. " 

546 "For example, a crane can have a " 

547 "certain operation space.", 

548 ) 

549 

550 createdAt: Optional[str] = Field( 

551 None, 

552 title="Timestamp", 

553 description="Representing a timestamp for the " 

554 "creation time of the property.", 

555 max_length=256, 

556 min_length=1, 

557 ) 

558 field_validator("createdAt")(validate_fiware_datatype_string_protect) 

559 

560 modifiedAt: Optional[str] = Field( 

561 None, 

562 title="Timestamp", 

563 description="Representing a timestamp for the " 

564 "last modification of the property.", 

565 max_length=256, 

566 min_length=1, 

567 ) 

568 field_validator("modifiedAt")(validate_fiware_datatype_string_protect) 

569 

570 def __init__(self, **data): 

571 # There is currently no validation for extra fields 

572 data.update(self._validate_attributes(data)) 

573 super().__init__(**data) 

574 

575 @classmethod 

576 def get_model_fields_set(cls): 

577 """ 

578 Get all names and aliases of the model fields. 

579 """ 

580 return set( 

581 [field.validation_alias for (_, field) in cls.model_fields.items()] 

582 + [field_name for field_name in cls.model_fields] 

583 ) 

584 

585 @classmethod 

586 def _validate_single_property( 

587 cls, attr 

588 ) -> Union[ContextProperty, ContextRelationship, ContextGeoProperty]: 

589 # skip validation if pre-defined model is already used 

590 if type(attr) in [ContextProperty, ContextRelationship, ContextGeoProperty]: 

591 return attr 

592 property_fields = ContextProperty.get_model_fields_set() 

593 property_fields.remove(None) 

594 # subattrs = {} 

595 if attr.get("type") == "Relationship": 

596 attr_instance = ContextRelationship.model_validate(attr) 

597 elif attr.get("type") == "GeoProperty": 

598 try: 

599 attr_instance = ContextGeoProperty.model_validate(attr) 

600 except Exception as e: 

601 pass 

602 elif attr.get("type") == "Property" or attr.get("type") is None: 

603 attr_instance = ContextProperty.model_validate(attr) 

604 else: 

605 raise ValueError(f"Attribute {attr.get('type')} " "is not a valid type") 

606 for subkey, subattr in attr.items(): 

607 if isinstance(subattr, dict) and subkey not in property_fields: 

608 attr_instance.model_extra.update( 

609 {subkey: cls._validate_single_property(attr=subattr)} 

610 ) 

611 return attr_instance 

612 

613 @classmethod 

614 def _validate_attributes(cls, data: Dict): 

615 entity_fields = cls.get_model_fields_set() 

616 entity_fields.remove(None) 

617 # Initialize the attribute dictionary 

618 attrs = {} 

619 # Iterate through the data 

620 for key, attr in data.items(): 

621 # Check if the keyword is not already present in the fields 

622 if key not in entity_fields: 

623 attrs[key] = cls._validate_single_property(attr=attr) 

624 return attrs 

625 

626 def model_dump(self, *args, by_alias: bool = True, **kwargs): 

627 return super().model_dump(*args, by_alias=by_alias, **kwargs) 

628 

629 @field_validator("id") 

630 @classmethod 

631 def _validate_id(cls, id: str): 

632 if not id.startswith("urn:ngsi-ld:"): 

633 logging.warning( 

634 msg="It is recommended that the entity id to be a URN," 

635 'starting with the namespace "urn:ngsi-ld:"' 

636 ) 

637 return id 

638 

639 def get_properties( 

640 self, response_format: Union[str, PropertyFormat] = PropertyFormat.LIST 

641 ) -> Union[List[NamedContextProperty], Dict[str, ContextProperty]]: 

642 """ 

643 Get all properties of the entity. 

644 Args: 

645 response_format: 

646 

647 Returns: 

648 

649 """ 

650 response_format = PropertyFormat(response_format) 

651 # response format dict: 

652 if response_format == PropertyFormat.DICT: 

653 final_dict = {} 

654 for key, value in self.model_dump(exclude_unset=True).items(): 

655 if key not in ContextLDEntity.get_model_fields_set(): 

656 if value.get("type") != DataTypeLD.RELATIONSHIP: 

657 if value.get("type") == DataTypeLD.GEOPROPERTY: 

658 final_dict[key] = ContextGeoProperty(**value) 

659 elif value.get("type") == DataTypeLD.PROPERTY: 

660 final_dict[key] = ContextProperty(**value) 

661 else: # named context property by default 

662 final_dict[key] = ContextProperty(**value) 

663 return final_dict 

664 # response format list: 

665 final_list = [] 

666 for key, value in self.model_dump(exclude_unset=True).items(): 

667 if key not in ContextLDEntity.get_model_fields_set(): 

668 if value.get("type") != DataTypeLD.RELATIONSHIP: 

669 if value.get("type") == DataTypeLD.GEOPROPERTY: 

670 final_list.append(NamedContextGeoProperty(name=key, **value)) 

671 elif value.get("type") == DataTypeLD.PROPERTY: 

672 final_list.append(NamedContextProperty(name=key, **value)) 

673 else: # named context property by default 

674 final_list.append(NamedContextProperty(name=key, **value)) 

675 return final_list 

676 

677 def delete_relationships(self, relationships: List[str]): 

678 """ 

679 Delete the given relationships from the entity 

680 

681 Args: 

682 relationships: List of relationship names 

683 

684 Returns: 

685 

686 """ 

687 all_relationships = self.get_relationships(response_format="dict") 

688 for relationship in relationships: 

689 # check they are relationships 

690 if relationship not in all_relationships: 

691 raise ValueError(f"Relationship {relationship} does not exist") 

692 delattr(self, relationship) 

693 

694 def delete_properties( 

695 self, 

696 props: Union[Dict[str, ContextProperty], List[NamedContextProperty], List[str]], 

697 ): 

698 """ 

699 Delete the given properties from the entity 

700 

701 Args: 

702 props: can be given in multiple forms 

703 1) Dict: {"<property_name>": ContextProperty, ...} 

704 2) List: [NamedContextProperty, ...] 

705 3) List: ["<property_name>", ...] 

706 

707 Returns: 

708 

709 """ 

710 names: List[str] = [] 

711 if isinstance(props, list): 

712 for entry in props: 

713 if isinstance(entry, str): 

714 names.append(entry) 

715 elif isinstance(entry, NamedContextProperty): 

716 names.append(entry.name) 

717 else: 

718 names.extend(list(props.keys())) 

719 

720 # check there are no relationships 

721 relationship_names = [rel.name for rel in self.get_relationships()] 

722 for name in names: 

723 if name in relationship_names: 

724 raise TypeError(f"{name} is a relationship") 

725 

726 for name in names: 

727 delattr(self, name) 

728 

729 def add_geo_properties( 

730 self, attrs: Union[Dict[str, ContextGeoProperty], List[NamedContextGeoProperty]] 

731 ) -> None: 

732 """ 

733 Add property to entity 

734 Args: 

735 attrs: 

736 Returns: 

737 None 

738 """ 

739 if isinstance(attrs, list): 

740 attrs = { 

741 attr.name: ContextGeoProperty( 

742 **attr.model_dump(exclude={"name"}, exclude_unset=True) 

743 ) 

744 for attr in attrs 

745 } 

746 for key, attr in attrs.items(): 

747 self.__setattr__(name=key, value=attr) 

748 

749 def add_properties( 

750 self, attrs: Union[Dict[str, ContextProperty], List[NamedContextProperty]] 

751 ) -> None: 

752 """ 

753 Add property to entity 

754 Args: 

755 attrs: 

756 Returns: 

757 None 

758 """ 

759 if isinstance(attrs, list): 

760 attrs = { 

761 attr.name: ContextProperty( 

762 **attr.model_dump(exclude={"name"}, exclude_unset=True) 

763 ) 

764 for attr in attrs 

765 } 

766 for key, attr in attrs.items(): 

767 self.__setattr__(name=key, value=attr) 

768 

769 def add_relationships( 

770 self, 

771 relationships: Union[ 

772 Dict[str, ContextRelationship], List[NamedContextRelationship] 

773 ], 

774 ) -> None: 

775 """ 

776 Add relationship to entity 

777 Args: 

778 relationships: 

779 Returns: 

780 None 

781 """ 

782 if isinstance(relationships, list): 

783 relationships = { 

784 attr.name: ContextRelationship(**attr.dict(exclude={"name"})) 

785 for attr in relationships 

786 } 

787 for key, attr in relationships.items(): 

788 self.__setattr__(name=key, value=attr) 

789 

790 def get_relationships( 

791 self, response_format: Union[str, PropertyFormat] = PropertyFormat.LIST 

792 ) -> Union[List[NamedContextRelationship], Dict[str, ContextRelationship]]: 

793 """ 

794 Get all relationships of the context entity 

795 

796 Args: 

797 response_format: 

798 

799 Returns: 

800 

801 """ 

802 response_format = PropertyFormat(response_format) 

803 # response format dict: 

804 if response_format == PropertyFormat.DICT: 

805 final_dict = {} 

806 for key, value in self.model_dump(exclude_unset=True).items(): 

807 if key not in ContextLDEntity.get_model_fields_set(): 

808 try: 

809 if value.get("type") == DataTypeLD.RELATIONSHIP: 

810 final_dict[key] = ContextRelationship(**value) 

811 except AttributeError: # if context attribute 

812 if isinstance(value, list): 

813 pass 

814 return final_dict 

815 # response format list: 

816 final_list = [] 

817 for key, value in self.model_dump(exclude_unset=True).items(): 

818 if key not in ContextLDEntity.get_model_fields_set(): 

819 if value.get("type") == DataTypeLD.RELATIONSHIP: 

820 final_list.append(NamedContextRelationship(name=key, **value)) 

821 return final_list 

822 

823 def get_context(self): 

824 """ 

825 Args: 

826 response_format: 

827 

828 Returns: context of the entity as list 

829 

830 """ 

831 _, context = self.model_dump(include={"context"}).popitem() 

832 if not context: 

833 logging.warning("No context in entity") 

834 return None 

835 else: 

836 return context 

837 

838 def to_keyvalues(self) -> ContextLDEntityKeyValues: 

839 props = self.get_properties() 

840 rels = self.get_relationships() 

841 result = dict[str, Any]() 

842 for prop in props: 

843 result[prop.name] = prop.value 

844 for rel in rels: 

845 result[rel.name] = rel.object 

846 return ContextLDEntityKeyValues(id=self.id, type=self.type, **result) 

847 

848 

849class ActionTypeLD(str, Enum): 

850 """ 

851 Options for queries 

852 """ 

853 

854 CREATE = "create" 

855 UPSERT = "upsert" 

856 UPDATE = "update" 

857 DELETE = "delete" 

858 

859 

860class UpdateLD(BaseModel): 

861 """ 

862 Model for update action 

863 """ 

864 

865 entities: List[Union[ContextLDEntity, ContextLDEntityKeyValues]] = Field( 

866 description="an array of entities, each entity specified using the " 

867 "JSON entity representation format " 

868 ) 

869 

870 

871class MessageLD(BaseModel): 

872 """ 

873 Model for a notification message, when sent to other NGSIv2-APIs 

874 """ 

875 

876 subscriptionId: Optional[str] = Field( 

877 default=None, 

878 description="Id of the subscription the notification comes from", 

879 ) 

880 data: List[ContextLDEntity] = Field( 

881 description="is an array with the notification data itself which " 

882 "includes the entity and all concerned attributes. Each " 

883 "element in the array corresponds to a different entity. " 

884 "By default, the entities are represented in normalized " 

885 "mode. However, using the attrsFormat modifier, a " 

886 "simplified representation mode can be requested." 

887 )