Module signals_notebook.entities.entity
Expand source code
import json
import logging
from datetime import datetime
from typing import Any, cast, ClassVar, Dict, Generator, Generic, List, Optional, Type, TypeVar, Union
from uuid import UUID
from pydantic import BaseModel, Field, PrivateAttr
from pydantic.generics import GenericModel
from signals_notebook.api import SignalsNotebookApi
from signals_notebook.common_types import (
EID,
EntityClass,
EntityCreationRequestPayload,
EntityShortDescription,
EntityType,
Response,
ResponseData,
)
from signals_notebook.jinja_env import env
from signals_notebook.utils import FSHandler
ChildClass = TypeVar('ChildClass', bound='Entity')
CellValueType = TypeVar('CellValueType')
log = logging.getLogger(__name__)
MAIN_PROPERTIES = ['Name', 'Description', 'createdAt', 'editedAt']
class Property(GenericModel, Generic[CellValueType]):
id: Optional[Union[UUID, str]]
type: Optional[str]
name: Optional[str]
description: Optional[str]
value: Optional[CellValueType]
values: Optional[List[CellValueType]]
_changed: bool = PrivateAttr(default=False)
def set_value(self, new_value: CellValueType) -> None:
"""Set new value
Args:
new_value: new value of Property value field
Returns:
"""
self.value = new_value
self._changed = True
@property
def is_changed(self) -> bool:
"""Checking if content of Cell has been modified
Returns:
bool
"""
return self._changed
@property
def representation_for_update(self) -> dict[str, dict]:
"""Get representation of body for update
Returns:
dict[str, dict]
"""
return {'attributes': self.dict(include={'name', 'value'})}
class PropertiesResponse(Response[Property]):
pass
class Entity(BaseModel):
type: str = Field(allow_mutation=False)
eid: EID = Field(allow_mutation=False)
digest: Optional[str] = Field(allow_mutation=False, default=None)
name: str = Field(title='Name')
description: Optional[str] = Field(title='Description', default=None)
created_at: datetime = Field(alias='createdAt', allow_mutation=False)
edited_at: datetime = Field(alias='editedAt', allow_mutation=False)
_template_name: ClassVar = 'entity.html'
_properties: List[Property] = PrivateAttr(default=[])
_properties_by_id: Dict[Union[str, UUID], Property] = PrivateAttr(default={})
class Config:
validate_assignment = True
allow_population_by_field_name = True
def __str__(self) -> str:
return f'<{self.__class__.__name__} eid={self.eid}>'
def __getitem__(self, index: Union[int, str, UUID, EID]) -> Any:
if not self._properties:
self._reload_properties()
if isinstance(index, int):
return self._properties[index]
if isinstance(index, str):
try:
return self._properties_by_id[UUID(index)]
except ValueError:
return self._properties_by_id[index]
if isinstance(index, UUID):
return self._properties_by_id[index]
raise IndexError('Invalid index')
def __iter__(self):
if not self._properties:
self._reload_properties()
return self._properties.__iter__()
@classmethod
def _get_entity_type(cls) -> EntityType:
raise NotImplementedError
@classmethod
def get_subclasses(cls) -> Generator[Type['Entity'], None, None]:
"""Get all Entity subclasses
Returns:
One of the Entity subclasses
"""
log.debug('Get subclasses for: %s', cls.__name__)
for subclass in cls.__subclasses__():
yield from subclass.get_subclasses()
yield subclass
@classmethod
def set_template_name(cls, template_name: str) -> None:
"""Set name of the template
Args:
template_name: template name
Returns:
"""
log.debug('Setting new template for: %s...', cls.__name__)
cls._template_name = template_name
log.debug('New template (%s) for %s was set', template_name, cls.__name__)
@classmethod
def get_template_name(cls) -> str:
"""Get Entity template name
Returns:
Template name
"""
return cls._template_name
@classmethod
def _get_endpoint(cls) -> str:
return 'entities'
@classmethod
def _get_list_params(cls) -> Dict[str, Any]:
return {
'include_types': [cls._get_entity_type()],
}
def _reload_properties(self) -> None:
log.debug('Reloading properties in Entity: %s...', self.eid)
self._properties = []
self._properties_by_id = {}
api = SignalsNotebookApi.get_default_api()
response = api.call(
method='GET',
path=(self._get_endpoint(), self.eid, 'properties'),
)
result = PropertiesResponse(**response.json())
properties = [cast(ResponseData, item).body for item in result.data]
for item in properties:
entity_property = cast(Property, item)
if entity_property.name in MAIN_PROPERTIES:
continue
assert entity_property.id
self._properties.append(entity_property)
self._properties_by_id[entity_property.id] = entity_property
log.debug('Properties in Entity: %s were reloaded', self.eid)
@classmethod
def get_list(cls) -> Generator['Entity', None, None]:
"""Get all entities
Returns:
list of entities
"""
from signals_notebook.entities.entity_store import EntityStore
return EntityStore.get_list(**cls._get_list_params())
def delete(self) -> None:
"""Delete entity
Returns:
"""
from signals_notebook.entities.entity_store import EntityStore
EntityStore.delete(self.eid)
log.debug('Entity: %s was deleted from EntityStore', self.eid)
@classmethod
def _create(cls, *, digest: str = None, force: bool = True, request: EntityCreationRequestPayload) -> EntityClass:
api = SignalsNotebookApi.get_default_api()
log.debug('Create Entity: %s...', cls.__name__)
response = api.call(
method='POST',
path=(cls._get_endpoint(),),
params={
'digest': digest,
'force': json.dumps(force),
},
json=request.dict(exclude_none=True, by_alias=True),
)
log.debug('Entity: %s was created.', cls.__name__)
result = Response[cls](**response.json()) # type: ignore
return cast(ResponseData, result.data).body
def refresh(self) -> None:
"""Refresh entity with new changes values
Returns:
"""
from signals_notebook.entities import EntityStore
EntityStore.refresh(self)
def _patch_properties(self, request_body, force: bool) -> None:
api = SignalsNotebookApi.get_default_api()
api.call(
method='PATCH',
path=(self._get_endpoint(), self.eid, 'properties'),
params={
'digest': None if force else self.digest,
'force': json.dumps(force),
},
json={
'data': request_body,
},
)
def save(self, force: bool = True) -> None:
"""Update attributes and properties of a specified entity.
Args:
force: Force to update attributes and properties without doing digest check.
Returns:
"""
log.debug('Save Entity: %s...', self.eid)
request_body = []
for field in self.__fields__.values():
if field.field_info.allow_mutation:
request_body.append(
{'attributes': {'name': field.field_info.title, 'value': getattr(self, field.name)}}
)
log.debug('Updating properties in Entity: %s...', self.eid)
if self._properties:
for item in self._properties:
if item.is_changed:
request_body.append(item.representation_for_update)
self._patch_properties(request_body=request_body, force=force)
self._reload_properties()
log.debug('Properties in Entity: %s were updated successfully', self.eid)
log.debug('Entity: %s was saved.', self.eid)
@property
def short_description(self) -> EntityShortDescription:
"""Return EntityShortDescription of Entity
Returns:
EntityShortDescription
"""
return EntityShortDescription(type=self.type, id=self.eid)
def get_html(self) -> str:
"""Get in HTML format
Returns:
Rendered HTML in string format
"""
data = {'name': self.name, 'edited_at': self.edited_at, 'type': self.type, 'description': self.description}
template = env.get_template(self._template_name)
log.info('Html template for %s:%s has been rendered.', self.__class__.__name__, self.eid)
return template.render(data=data)
def dump(self, base_path: str, fs_handler: FSHandler, alias: Optional[List[str]] = None) -> None:
log.error('Dumping is not implemented')
@classmethod
def _load(cls, path: str, fs_handler: FSHandler, parent: Any) -> None:
log.error('Loading is not implemented')
@classmethod
def dump_templates(cls, base_path: str, fs_handler: FSHandler) -> None:
"""Dump Entity templates
Args:
base_path: content path where create templates dump
fs_handler: FSHandler
Returns:
"""
from signals_notebook.entities import EntityStore
entity_type = cls._get_entity_type()
templates = EntityStore.get_list(
include_types=[entity_type], include_options=[EntityStore.IncludeOptions.TEMPLATE]
)
try:
for template in templates:
fs_handler.write(
fs_handler.join_path(base_path, 'templates', entity_type, f'metadata_{template.name}.json'),
json.dumps({k: v for k, v in template.dict().items() if k in ('name', 'description', 'eid')}),
base_alias=['Templates', entity_type, template.name],
)
except TypeError:
pass
Classes
class Entity (**data: Any)
-
Create a new model by parsing and validating input data from keyword arguments.
Raises ValidationError if the input data cannot be parsed to form a valid model.
Expand source code
class Entity(BaseModel): type: str = Field(allow_mutation=False) eid: EID = Field(allow_mutation=False) digest: Optional[str] = Field(allow_mutation=False, default=None) name: str = Field(title='Name') description: Optional[str] = Field(title='Description', default=None) created_at: datetime = Field(alias='createdAt', allow_mutation=False) edited_at: datetime = Field(alias='editedAt', allow_mutation=False) _template_name: ClassVar = 'entity.html' _properties: List[Property] = PrivateAttr(default=[]) _properties_by_id: Dict[Union[str, UUID], Property] = PrivateAttr(default={}) class Config: validate_assignment = True allow_population_by_field_name = True def __str__(self) -> str: return f'<{self.__class__.__name__} eid={self.eid}>' def __getitem__(self, index: Union[int, str, UUID, EID]) -> Any: if not self._properties: self._reload_properties() if isinstance(index, int): return self._properties[index] if isinstance(index, str): try: return self._properties_by_id[UUID(index)] except ValueError: return self._properties_by_id[index] if isinstance(index, UUID): return self._properties_by_id[index] raise IndexError('Invalid index') def __iter__(self): if not self._properties: self._reload_properties() return self._properties.__iter__() @classmethod def _get_entity_type(cls) -> EntityType: raise NotImplementedError @classmethod def get_subclasses(cls) -> Generator[Type['Entity'], None, None]: """Get all Entity subclasses Returns: One of the Entity subclasses """ log.debug('Get subclasses for: %s', cls.__name__) for subclass in cls.__subclasses__(): yield from subclass.get_subclasses() yield subclass @classmethod def set_template_name(cls, template_name: str) -> None: """Set name of the template Args: template_name: template name Returns: """ log.debug('Setting new template for: %s...', cls.__name__) cls._template_name = template_name log.debug('New template (%s) for %s was set', template_name, cls.__name__) @classmethod def get_template_name(cls) -> str: """Get Entity template name Returns: Template name """ return cls._template_name @classmethod def _get_endpoint(cls) -> str: return 'entities' @classmethod def _get_list_params(cls) -> Dict[str, Any]: return { 'include_types': [cls._get_entity_type()], } def _reload_properties(self) -> None: log.debug('Reloading properties in Entity: %s...', self.eid) self._properties = [] self._properties_by_id = {} api = SignalsNotebookApi.get_default_api() response = api.call( method='GET', path=(self._get_endpoint(), self.eid, 'properties'), ) result = PropertiesResponse(**response.json()) properties = [cast(ResponseData, item).body for item in result.data] for item in properties: entity_property = cast(Property, item) if entity_property.name in MAIN_PROPERTIES: continue assert entity_property.id self._properties.append(entity_property) self._properties_by_id[entity_property.id] = entity_property log.debug('Properties in Entity: %s were reloaded', self.eid) @classmethod def get_list(cls) -> Generator['Entity', None, None]: """Get all entities Returns: list of entities """ from signals_notebook.entities.entity_store import EntityStore return EntityStore.get_list(**cls._get_list_params()) def delete(self) -> None: """Delete entity Returns: """ from signals_notebook.entities.entity_store import EntityStore EntityStore.delete(self.eid) log.debug('Entity: %s was deleted from EntityStore', self.eid) @classmethod def _create(cls, *, digest: str = None, force: bool = True, request: EntityCreationRequestPayload) -> EntityClass: api = SignalsNotebookApi.get_default_api() log.debug('Create Entity: %s...', cls.__name__) response = api.call( method='POST', path=(cls._get_endpoint(),), params={ 'digest': digest, 'force': json.dumps(force), }, json=request.dict(exclude_none=True, by_alias=True), ) log.debug('Entity: %s was created.', cls.__name__) result = Response[cls](**response.json()) # type: ignore return cast(ResponseData, result.data).body def refresh(self) -> None: """Refresh entity with new changes values Returns: """ from signals_notebook.entities import EntityStore EntityStore.refresh(self) def _patch_properties(self, request_body, force: bool) -> None: api = SignalsNotebookApi.get_default_api() api.call( method='PATCH', path=(self._get_endpoint(), self.eid, 'properties'), params={ 'digest': None if force else self.digest, 'force': json.dumps(force), }, json={ 'data': request_body, }, ) def save(self, force: bool = True) -> None: """Update attributes and properties of a specified entity. Args: force: Force to update attributes and properties without doing digest check. Returns: """ log.debug('Save Entity: %s...', self.eid) request_body = [] for field in self.__fields__.values(): if field.field_info.allow_mutation: request_body.append( {'attributes': {'name': field.field_info.title, 'value': getattr(self, field.name)}} ) log.debug('Updating properties in Entity: %s...', self.eid) if self._properties: for item in self._properties: if item.is_changed: request_body.append(item.representation_for_update) self._patch_properties(request_body=request_body, force=force) self._reload_properties() log.debug('Properties in Entity: %s were updated successfully', self.eid) log.debug('Entity: %s was saved.', self.eid) @property def short_description(self) -> EntityShortDescription: """Return EntityShortDescription of Entity Returns: EntityShortDescription """ return EntityShortDescription(type=self.type, id=self.eid) def get_html(self) -> str: """Get in HTML format Returns: Rendered HTML in string format """ data = {'name': self.name, 'edited_at': self.edited_at, 'type': self.type, 'description': self.description} template = env.get_template(self._template_name) log.info('Html template for %s:%s has been rendered.', self.__class__.__name__, self.eid) return template.render(data=data) def dump(self, base_path: str, fs_handler: FSHandler, alias: Optional[List[str]] = None) -> None: log.error('Dumping is not implemented') @classmethod def _load(cls, path: str, fs_handler: FSHandler, parent: Any) -> None: log.error('Loading is not implemented') @classmethod def dump_templates(cls, base_path: str, fs_handler: FSHandler) -> None: """Dump Entity templates Args: base_path: content path where create templates dump fs_handler: FSHandler Returns: """ from signals_notebook.entities import EntityStore entity_type = cls._get_entity_type() templates = EntityStore.get_list( include_types=[entity_type], include_options=[EntityStore.IncludeOptions.TEMPLATE] ) try: for template in templates: fs_handler.write( fs_handler.join_path(base_path, 'templates', entity_type, f'metadata_{template.name}.json'), json.dumps({k: v for k, v in template.dict().items() if k in ('name', 'description', 'eid')}), base_alias=['Templates', entity_type, template.name], ) except TypeError: pass
Ancestors
- pydantic.main.BaseModel
- pydantic.utils.Representation
Subclasses
Class variables
var Config
var created_at : datetime.datetime
var description : Optional[str]
var digest : Optional[str]
var edited_at : datetime.datetime
var eid : EID
var name : str
var type : str
Static methods
def dump_templates(base_path: str, fs_handler: FSHandler) ‑> None
-
Dump Entity templates
Args
base_path
- content path where create templates dump
fs_handler
- FSHandler
Returns:
Expand source code
@classmethod def dump_templates(cls, base_path: str, fs_handler: FSHandler) -> None: """Dump Entity templates Args: base_path: content path where create templates dump fs_handler: FSHandler Returns: """ from signals_notebook.entities import EntityStore entity_type = cls._get_entity_type() templates = EntityStore.get_list( include_types=[entity_type], include_options=[EntityStore.IncludeOptions.TEMPLATE] ) try: for template in templates: fs_handler.write( fs_handler.join_path(base_path, 'templates', entity_type, f'metadata_{template.name}.json'), json.dumps({k: v for k, v in template.dict().items() if k in ('name', 'description', 'eid')}), base_alias=['Templates', entity_type, template.name], ) except TypeError: pass
def get_list() ‑> Generator[Entity, None, None]
-
Get all entities
Returns
list of entities
Expand source code
@classmethod def get_list(cls) -> Generator['Entity', None, None]: """Get all entities Returns: list of entities """ from signals_notebook.entities.entity_store import EntityStore return EntityStore.get_list(**cls._get_list_params())
def get_subclasses() ‑> Generator[Type[Entity], None, None]
-
Get all Entity subclasses
Returns
One of the Entity subclasses
Expand source code
@classmethod def get_subclasses(cls) -> Generator[Type['Entity'], None, None]: """Get all Entity subclasses Returns: One of the Entity subclasses """ log.debug('Get subclasses for: %s', cls.__name__) for subclass in cls.__subclasses__(): yield from subclass.get_subclasses() yield subclass
def get_template_name() ‑> str
-
Get Entity template name
Returns
Template name
Expand source code
@classmethod def get_template_name(cls) -> str: """Get Entity template name Returns: Template name """ return cls._template_name
def set_template_name(template_name: str) ‑> None
-
Set name of the template
Args
template_name
- template name
Returns:
Expand source code
@classmethod def set_template_name(cls, template_name: str) -> None: """Set name of the template Args: template_name: template name Returns: """ log.debug('Setting new template for: %s...', cls.__name__) cls._template_name = template_name log.debug('New template (%s) for %s was set', template_name, cls.__name__)
Instance variables
var short_description : EntityShortDescription
-
Return EntityShortDescription of Entity
Returns
EntityShortDescription
Expand source code
@property def short_description(self) -> EntityShortDescription: """Return EntityShortDescription of Entity Returns: EntityShortDescription """ return EntityShortDescription(type=self.type, id=self.eid)
Methods
def delete(self) ‑> None
-
Delete entity
Returns:
Expand source code
def delete(self) -> None: """Delete entity Returns: """ from signals_notebook.entities.entity_store import EntityStore EntityStore.delete(self.eid) log.debug('Entity: %s was deleted from EntityStore', self.eid)
def dump(self, base_path: str, fs_handler: FSHandler, alias: Optional[List[str]] = None) ‑> None
-
Expand source code
def dump(self, base_path: str, fs_handler: FSHandler, alias: Optional[List[str]] = None) -> None: log.error('Dumping is not implemented')
def get_html(self) ‑> str
-
Get in HTML format
Returns
Rendered HTML in string format
Expand source code
def get_html(self) -> str: """Get in HTML format Returns: Rendered HTML in string format """ data = {'name': self.name, 'edited_at': self.edited_at, 'type': self.type, 'description': self.description} template = env.get_template(self._template_name) log.info('Html template for %s:%s has been rendered.', self.__class__.__name__, self.eid) return template.render(data=data)
def refresh(self) ‑> None
-
Refresh entity with new changes values
Returns:
Expand source code
def refresh(self) -> None: """Refresh entity with new changes values Returns: """ from signals_notebook.entities import EntityStore EntityStore.refresh(self)
def save(self, force: bool = True) ‑> None
-
Update attributes and properties of a specified entity.
Args
force
- Force to update attributes and properties without doing digest check.
Returns:
Expand source code
def save(self, force: bool = True) -> None: """Update attributes and properties of a specified entity. Args: force: Force to update attributes and properties without doing digest check. Returns: """ log.debug('Save Entity: %s...', self.eid) request_body = [] for field in self.__fields__.values(): if field.field_info.allow_mutation: request_body.append( {'attributes': {'name': field.field_info.title, 'value': getattr(self, field.name)}} ) log.debug('Updating properties in Entity: %s...', self.eid) if self._properties: for item in self._properties: if item.is_changed: request_body.append(item.representation_for_update) self._patch_properties(request_body=request_body, force=force) self._reload_properties() log.debug('Properties in Entity: %s were updated successfully', self.eid) log.debug('Entity: %s was saved.', self.eid)
class PropertiesResponse (**kwargs)
-
Create a new model by parsing and validating input data from keyword arguments.
Raises ValidationError if the input data cannot be parsed to form a valid model.
Expand source code
class PropertiesResponse(Response[Property]): pass
Ancestors
- Response[Property]
- Response
- pydantic.generics.GenericModel
- pydantic.main.BaseModel
- pydantic.utils.Representation
- typing.Generic
Class variables
var data : Union[pydantic.generics.ResponseData[Property], List[pydantic.generics.ResponseData[Property]]]
var links : Optional[Links]
class Property (**data: Any)
-
Create a new model by parsing and validating input data from keyword arguments.
Raises ValidationError if the input data cannot be parsed to form a valid model.
Expand source code
class Property(GenericModel, Generic[CellValueType]): id: Optional[Union[UUID, str]] type: Optional[str] name: Optional[str] description: Optional[str] value: Optional[CellValueType] values: Optional[List[CellValueType]] _changed: bool = PrivateAttr(default=False) def set_value(self, new_value: CellValueType) -> None: """Set new value Args: new_value: new value of Property value field Returns: """ self.value = new_value self._changed = True @property def is_changed(self) -> bool: """Checking if content of Cell has been modified Returns: bool """ return self._changed @property def representation_for_update(self) -> dict[str, dict]: """Get representation of body for update Returns: dict[str, dict] """ return {'attributes': self.dict(include={'name', 'value'})}
Ancestors
- pydantic.generics.GenericModel
- pydantic.main.BaseModel
- pydantic.utils.Representation
- typing.Generic
Class variables
var description : Optional[str]
var id : Union[uuid.UUID, str, ForwardRef(None)]
var name : Optional[str]
var type : Optional[str]
var value : Optional[~CellValueType]
var values : Optional[List[~CellValueType]]
Instance variables
var is_changed : bool
-
Checking if content of Cell has been modified
Returns
bool
Expand source code
@property def is_changed(self) -> bool: """Checking if content of Cell has been modified Returns: bool """ return self._changed
var representation_for_update : dict
-
Get representation of body for update
Returns
dict[str, dict]
Expand source code
@property def representation_for_update(self) -> dict[str, dict]: """Get representation of body for update Returns: dict[str, dict] """ return {'attributes': self.dict(include={'name', 'value'})}
Methods
def set_value(self, new_value: ~CellValueType) ‑> None
-
Set new value
Args
new_value
- new value of Property value field
Returns:
Expand source code
def set_value(self, new_value: CellValueType) -> None: """Set new value Args: new_value: new value of Property value field Returns: """ self.value = new_value self._changed = True
class Response[Property] (**kwargs)
-
Create a new model by parsing and validating input data from keyword arguments.
Raises ValidationError if the input data cannot be parsed to form a valid model.
Ancestors
- Response
- pydantic.generics.GenericModel
- pydantic.main.BaseModel
- pydantic.utils.Representation
- typing.Generic
Subclasses
Class variables
var Config
var data : Union[pydantic.generics.ResponseData[Property], List[pydantic.generics.ResponseData[Property]]]
var links : Optional[Links]