Exemple #1
0
class ModelConfig(FrigateBaseModel):
    path: Optional[str] = Field(title="Custom Object detection model path.")
    labelmap_path: Optional[str] = Field(
        title="Label map for custom object detector.")
    width: int = Field(default=320,
                       title="Object detection model input width.")
    height: int = Field(default=320,
                        title="Object detection model input height.")
    labelmap: Dict[int, str] = Field(default_factory=dict,
                                     title="Labelmap customization.")
    _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
    _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()

    @property
    def merged_labelmap(self) -> Dict[int, str]:
        return self._merged_labelmap

    @property
    def colormap(self) -> Dict[int, Tuple[int, int, int]]:
        return self._colormap

    def __init__(self, **config):
        super().__init__(**config)

        self._merged_labelmap = {
            **load_labels(config.get("labelmap_path", "/labelmap.txt")),
            **config.get("labelmap", {}),
        }

        cmap = plt.cm.get_cmap("tab10", len(self._merged_labelmap.keys()))

        self._colormap = {}
        for key, val in self._merged_labelmap.items():
            self._colormap[val] = tuple(
                int(round(255 * c)) for c in cmap(key)[:3])
Exemple #2
0
class StorageDistribution(ProtectBaseObject):
    recording_type_distributions: List[RecordingTypeDistribution]
    resolution_distributions: List[ResolutionDistribution]

    _recording_type_dict: Optional[Dict[
        RecordingType, RecordingTypeDistribution]] = PrivateAttr(None)
    _resolution_dict: Optional[Dict[
        ResolutionStorageType, ResolutionDistribution]] = PrivateAttr(None)

    def _get_recording_type_dict(
            self) -> Dict[RecordingType, RecordingTypeDistribution]:
        if self._recording_type_dict is None:
            self._recording_type_dict = {}
            for recording_type in self.recording_type_distributions:
                self._recording_type_dict[
                    recording_type.recording_type] = recording_type

        return self._recording_type_dict

    def _get_resolution_dict(
            self) -> Dict[ResolutionStorageType, ResolutionDistribution]:
        if self._resolution_dict is None:
            self._resolution_dict = {}
            for resolution in self.resolution_distributions:
                self._resolution_dict[resolution.resolution] = resolution

        return self._resolution_dict

    @property
    def timelapse_recordings(self) -> Optional[RecordingTypeDistribution]:
        return self._get_recording_type_dict().get(RecordingType.TIMELAPSE)

    @property
    def continuous_recordings(self) -> Optional[RecordingTypeDistribution]:
        return self._get_recording_type_dict().get(RecordingType.CONTINUOUS)

    @property
    def detections_recordings(self) -> Optional[RecordingTypeDistribution]:
        return self._get_recording_type_dict().get(RecordingType.DETECTIONS)

    @property
    def uhd_usage(self) -> Optional[ResolutionDistribution]:
        return self._get_resolution_dict().get(ResolutionStorageType.UHD)

    @property
    def hd_usage(self) -> Optional[ResolutionDistribution]:
        return self._get_resolution_dict().get(ResolutionStorageType.HD)

    @property
    def free(self) -> Optional[ResolutionDistribution]:
        return self._get_resolution_dict().get(ResolutionStorageType.FREE)

    def update_from_dict(self, data: Dict[str, Any]) -> StorageDistribution:
        # reset internal look ups when data changes
        self._recording_type_dict = None
        self._resolution_dict = None

        return super().update_from_dict(data)
Exemple #3
0
class MeshPoint(BaseModel):
    """Mesh Point."""

    # Vertex Index.
    vidx: int
    # 3D Coordinate of point.
    point: Tuple[float, float, float]
    # Point normal.
    normal: Optional[Tuple[float, float, float]]
    # Parent Mesh Data.
    _parent: Optional["MeshData"] = PrivateAttr(default=None)

    class Config:
        keep_untouched = (cached_property,)

    @property
    def faces(self) -> List["MeshFace"]:
        return [f for f in self._parent.faces if self.vidx in f.vertex_indices]

    @property
    def as_euclid(self) -> Point3:
        return Point3(*self.point)

    @cached_property
    def as_sympy(self) -> S.Point3D:
        return S.Point3D(*self.point)

    @property
    def as_vector(self) -> Vector3:
        return Vector3(*self.point)

    @property
    def normal_vector(self) -> Vector3:
        return Vector3(*self.normal)
Exemple #4
0
class User(ProtectModelWithId):
    permissions: List[str]
    last_login_ip: Optional[str]
    last_login_time: Optional[datetime]
    is_owner: bool
    enable_notifications: bool
    has_accepted_invite: bool
    all_permissions: List[str]
    scopes: Optional[List[str]] = None
    location: Optional[UserLocation]
    name: str
    first_name: str
    last_name: str
    email: str
    local_username: str
    group_ids: List[str]
    cloud_account: Optional[CloudAccount]
    feature_flags: UserFeatureFlags

    # TODO:
    # settings
    # alertRules
    # notificationsV2

    _groups: Optional[List[Group]] = PrivateAttr(None)

    @classmethod
    def _get_unifi_remaps(cls) -> Dict[str, str]:
        return {**super()._get_unifi_remaps(), "groups": "groupIds"}

    def unifi_dict(self,
                   data: Optional[Dict[str, Any]] = None,
                   exclude: Optional[Set[str]] = None) -> Dict[str, Any]:
        data = super().unifi_dict(data=data, exclude=exclude)

        if "location" in data and data["location"] is None:
            del data["location"]

        return data

    @property
    def groups(self) -> List[Group]:
        """
        Groups the user is in

        Will always be empty if the user only has read only access.
        """

        if self._groups is not None:
            return self._groups

        self._groups = [
            self.api.bootstrap.groups[g] for g in self.group_ids
            if g in self.api.bootstrap.groups
        ]
        return self._groups
Exemple #5
0
class ZoneConfig(BaseModel):
    filters: Dict[str, FilterConfig] = Field(
        default_factory=dict, title="Zone filters."
    )
    coordinates: Union[str, List[str]] = Field(
        title="Coordinates polygon for the defined zone."
    )
    objects: List[str] = Field(
        default_factory=list,
        title="List of objects that can trigger the zone.",
    )
    _color: Optional[Tuple[int, int, int]] = PrivateAttr()
    _contour: np.ndarray = PrivateAttr()

    @property
    def color(self) -> Tuple[int, int, int]:
        return self._color

    @property
    def contour(self) -> np.ndarray:
        return self._contour

    def __init__(self, **config):
        super().__init__(**config)

        self._color = config.get("color", (0, 0, 0))
        coordinates = config["coordinates"]

        if isinstance(coordinates, list):
            self._contour = np.array(
                [[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
            )
        elif isinstance(coordinates, str):
            points = coordinates.split(",")
            self._contour = np.array(
                [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
            )
        else:
            self._contour = np.array([])
Exemple #6
0
class FileModel(BaseModel, ABC):
    """Base class to represent models with a file representation.

    It therefore always has a `filepath` and if it is given on
    initilization, it will parse that file. The filepath can be
    relative, in which case the paths are expected to be resolved
    relative to some root model. If a path is absolute, this path
    will always be used, regardless of a root parent.

    When saving a model, if the current filepath is relative, the
    last resolved absolute path will be used. If the model has just
    been read, the

    This class extends the `validate` option of Pydantic,
    so when when a Path is given to a field with type `FileModel`,
    it doesn't error, but actually initializes the `FileModel`.

    Attributes:
        filepath (Optional[Path]):
            The path of this FileModel. This path can be either absolute or relative.
            If it is a relative path, it is assumed to be resolved from some root
            model.
        save_location (Path):
            A readonly property corresponding with the (current) save location of this
            FileModel. If read from a file or after saving recursively or
            after calling synchronize_filepath, this value will be updated to its new
            state. If made from memory and filepath is not set, it will correspond with
            cwd / filename.extension
    """

    __slots__ = ["__weakref__"]
    # Use WeakValueDictionary to keep track of file paths with their respective parsed file models.
    _file_models_cache: WeakValueDictionary = WeakValueDictionary()
    filepath: Optional[Path] = None
    # Absolute anchor is used to resolve the save location when the filepath is relative.
    _absolute_anchor_path: Path = PrivateAttr(default_factory=Path.cwd)

    def __new__(cls, filepath: Optional[Path] = None, *args, **kwargs):
        """Creates a new model.
        If the file at the provided file path was already parsed, this instance is returned.

        Args:
            filepath (Optional[Path], optional): The file path to the file. Defaults to None.

        Returns:
            FileModel: A file model.
        """
        with file_load_context() as context:
            if (file_model := context.retrieve_model(filepath)) is not None:
                return file_model
            else:
Exemple #7
0
class LiveviewSlot(ProtectBaseObject):
    camera_ids: List[str]
    cycle_mode: str
    cycle_interval: int

    _cameras: Optional[List[Camera]] = PrivateAttr(None)

    @classmethod
    def _get_unifi_remaps(cls) -> Dict[str, str]:
        return {**super()._get_unifi_remaps(), "cameras": "cameraIds"}

    @property
    def cameras(self) -> List[Camera]:
        if self._cameras is not None:
            return self._cameras

        # user may not have permission to see the cameras in the liveview
        self._cameras = [self.api.bootstrap.cameras[g] for g in self.camera_ids if g in self.api.bootstrap.cameras]
        return self._cameras
Exemple #8
0
class AEMOTableSchema(BaseModel):
    name: str
    namespace: str
    fieldnames: List[str]
    records: List[Union[Dict, BaseModel]] = []

    # optionally it has a schema
    _record_schema: Optional[BaseModel] = PrivateAttr()

    # the url this table was taken from if any
    url_source: Optional[str]

    # the original content ?!?!
    # @NOTE does this make sense ..
    content_source: Optional[str]

    @property
    def full_name(self) -> str:
        return "{}_{}".format(self.namespace, self.name)

    @validator("name")
    def validate_name(cls, table_name: str) -> str:
        _table_name = table_name.strip().upper()

        return _table_name

    @validator("namespace")
    def validate_namespace(cls, namespace_name: str) -> str:
        _namespace_name = namespace_name.strip().upper()

        return _namespace_name

    @validator("fieldnames")
    def validate_fieldnames(cls, fieldnames: List[str]) -> List[str]:
        if not isinstance(fieldnames, list):
            return []

        _fieldnames = [i.strip() for i in fieldnames]

        return _fieldnames

    def set_schema(self, schema: BaseModel) -> bool:
        self._record_schema = schema

        return True

    def get_records(self) -> Any:
        return self.records

    def add_record(self, record: Union[Dict, BaseModel]) -> bool:
        if self._record_schema:
            _record = None

            try:
                _record = self._record_schema(**record)  # type: ignore
            except ValidationError as e:
                val_errors = e.errors()

                for ve in val_errors:
                    ve_fieldname = ve["loc"][0]
                    ve_val = ""

                    if (record and isinstance(record, dict)
                            and ve_fieldname in record):
                        ve_val = record[ve_fieldname]

                    logger.error("{} has error: {} '{}'".format(
                        ve_fieldname, ve["msg"], ve_val))
                return False

            self.records.append(_record)
        else:
            self.records.append(record)

        return True

    class Config:
        underscore_attrs_are_private = True
Exemple #9
0
class AEMOTableSchema(BaseModel):
    name: str
    namespace: str
    fieldnames: List[str]
    _records: List[Union[Dict, BaseModel]] = []

    # optionally it has a schema
    _record_schema: Optional[BaseModel] = PrivateAttr()

    # the url this table was taken from if any
    url_source: Optional[str]

    # the original content ?!?!
    # @NOTE does this make sense .. (it doesnt because it can be super large)
    content_source: Optional[str]

    @property
    def full_name(self) -> str:
        return "{}_{}".format(self.namespace, self.name)

    @validator("name")
    def validate_name(cls, table_name: str) -> str:
        _table_name = table_name.strip().upper()

        return _table_name

    @validator("namespace")
    def validate_namespace(cls, namespace_name: str) -> str:
        _namespace_name = namespace_name.strip().upper()

        return _namespace_name

    @validator("fieldnames")
    def validate_fieldnames(cls, fieldnames: List[str]) -> List[str]:
        if not isinstance(fieldnames, list):
            return []

        _fieldnames = [i.strip() for i in fieldnames]

        return _fieldnames

    def set_schema(self, schema: BaseModel) -> bool:
        self._record_schema = schema

        return True

    @property
    def records(self) -> Any:
        _records = []

        for _r in self._records:
            if isinstance(_r, MMSBase):
                _records.append(_r.dict())
            if isinstance(_r, Dict):
                _records.append(_r)

        return _records

    def add_record(self, record: Union[Dict, BaseModel]) -> bool:
        if hasattr(self, "_record_schema") and self._record_schema:
            _record = None

            try:
                _record = self._record_schema(**record)  # type: ignore
            except ValidationError as e:
                val_errors = e.errors()

                for ve in val_errors:
                    ve_fieldname = ve["loc"][0]
                    ve_val = ""

                    if record and isinstance(record, dict) and ve_fieldname in record:
                        ve_val = record[ve_fieldname]

                    logger.error("{} has error: {} '{}'".format(ve_fieldname, ve["msg"], ve_val))
                return False

            self._records.append(_record)
        else:
            self._records.append(record)

        return True

    def to_frame(self) -> Any:
        """ Return a pandas dataframe for the table """
        if not _HAVE_PANDAS:
            return None

        _index_keys = []

        if hasattr(self, "_record_schema") and self._record_schema:
            if hasattr(self._record_schema, "_primary_keys"):
                _index_keys = self._record_schema._primary_keys  # type: ignore

        _df = pd.DataFrame(self.records)

        if len(_index_keys) > 0:
            logger.debug("Setting index to {}".format(_index_keys))
            _df = _df.set_index(_index_keys)

        return _df

    class Config:
        underscore_attrs_are_private = True
Exemple #10
0
class Bootstrap(ProtectBaseObject):
    auth_user_id: str
    access_key: str
    cameras: Dict[str, Camera]
    users: Dict[str, User]
    groups: Dict[str, Group]
    liveviews: Dict[str, Liveview]
    nvr: NVR
    viewers: Dict[str, Viewer]
    lights: Dict[str, Light]
    bridges: Dict[str, Bridge]
    sensors: Dict[str, Sensor]
    doorlocks: Dict[str, Doorlock]
    last_update_id: UUID

    # TODO:
    # legacyUFVs
    # displays
    # chimes
    # schedules

    # not directly from Unifi
    events: Dict[str, Event] = FixSizeOrderedDict()
    capture_ws_stats: bool = False
    _ws_stats: List[WSStat] = PrivateAttr([])

    @classmethod
    def unifi_dict_to_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
        api = cls._get_api(data.get("api"))
        for model_type in ModelType.bootstrap_models():
            key = model_type + "s"
            items: Dict[str, ProtectModel] = {}
            for item in data[key]:
                if api is not None and api.ignore_unadopted and not item.get(
                        "isAdopted", True):
                    continue

                items[item["id"]] = item
            data[key] = items

        return super().unifi_dict_to_dict(data)

    def unifi_dict(self,
                   data: Optional[Dict[str, Any]] = None,
                   exclude: Optional[Set[str]] = None) -> Dict[str, Any]:
        data = super().unifi_dict(data=data, exclude=exclude)

        if "events" in data:
            del data["events"]
        if "captureWsStats" in data:
            del data["captureWsStats"]

        for model_type in ModelType.bootstrap_models():
            attr = model_type + "s"
            if attr in data and isinstance(data[attr], dict):
                data[attr] = list(data[attr].values())

        return data

    @property
    def ws_stats(self) -> List[WSStat]:
        return self._ws_stats

    def clear_ws_stats(self) -> None:
        self._ws_stats = []

    @property
    def auth_user(self) -> User:
        user: User = self.api.bootstrap.users[self.auth_user_id]
        return user

    def process_event(self, event: Event) -> None:
        if event.type in CAMERA_EVENT_ATTR_MAP and event.camera is not None:
            _process_camera_event(event)
        elif event.type == EventType.MOTION_LIGHT and event.light is not None:
            _process_light_event(event)
        elif event.type == EventType.MOTION_SENSOR and event.sensor is not None:
            _process_sensor_event(event)

        self.events[event.id] = event

    def _create_stat(self, packet: WSPacket, keys_set: List[str],
                     filtered: bool) -> None:
        if self.capture_ws_stats:
            self._ws_stats.append(
                WSStat(
                    model=packet.action_frame.data["modelKey"],
                    action=packet.action_frame.data["action"],
                    keys=list(packet.data_frame.data.keys()),
                    keys_set=keys_set,
                    size=len(packet.raw),
                    filtered=filtered,
                ))

    def _get_frame_data(
            self, packet: WSPacket) -> Tuple[Dict[str, Any], Dict[str, Any]]:
        if self.capture_ws_stats:
            return deepcopy(packet.action_frame.data), deepcopy(
                packet.data_frame.data)
        return packet.action_frame.data, packet.data_frame.data

    def _process_add_packet(
            self, packet: WSPacket,
            data: Dict[str, Any]) -> Optional[WSSubscriptionMessage]:
        obj = create_from_unifi_dict(data, api=self._api)

        if isinstance(obj, Event):
            self.process_event(obj)
        elif isinstance(obj, NVR):
            self.nvr = obj
        elif (isinstance(obj, ProtectModelWithId) and obj.model is not None
              and obj.model.value in ModelType.bootstrap_models()):
            key = obj.model.value + "s"
            getattr(self, key)[obj.id] = obj
        else:
            _LOGGER.debug("Unexpected bootstrap model type for add: %s",
                          obj.model)
            return None

        updated = obj.dict()
        self._create_stat(packet, list(updated.keys()), False)
        return WSSubscriptionMessage(action=WSAction.ADD,
                                     new_update_id=self.last_update_id,
                                     changed_data=updated,
                                     new_obj=obj)

    def _process_nvr_update(
            self, packet: WSPacket, data: Dict[str, Any],
            ignore_stats: bool) -> Optional[WSSubscriptionMessage]:
        data = _remove_stats_keys(data, ignore_stats)
        # nothing left to process
        if len(data) == 0:
            self._create_stat(packet, [], True)
            return None

        data = self.nvr.unifi_dict_to_dict(data)
        old_nvr = self.nvr.copy()
        self.nvr = self.nvr.update_from_dict(deepcopy(data))

        self._create_stat(packet, list(data.keys()), False)
        return WSSubscriptionMessage(
            action=WSAction.UPDATE,
            new_update_id=self.last_update_id,
            changed_data=data,
            new_obj=self.nvr,
            old_obj=old_nvr,
        )

    def _process_device_update(
            self, packet: WSPacket, action: Dict[str, Any], data: Dict[str,
                                                                       Any],
            ignore_stats: bool) -> Optional[WSSubscriptionMessage]:
        model_type = action["modelKey"]

        data = _remove_stats_keys(data, ignore_stats)
        # nothing left to process
        if len(data) == 0:
            self._create_stat(packet, [], True)
            return None

        key = model_type + "s"
        devices = getattr(self, key)
        if action["id"] in devices:
            obj: ProtectModelWithId = devices[action["id"]]
            data = obj.unifi_dict_to_dict(data)
            old_obj = obj.copy()
            obj = obj.update_from_dict(deepcopy(data))
            now = utc_now()

            if isinstance(obj, Event):
                self.process_event(obj)
            elif isinstance(obj, Camera):
                if "last_ring" in data and obj.last_ring:
                    is_recent = obj.last_ring + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
                    if is_recent:
                        obj.set_ring_timeout()
            elif isinstance(obj, Sensor):
                if "alarm_triggered_at" in data and obj.alarm_triggered_at:
                    is_recent = obj.alarm_triggered_at + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id,
                                  is_recent)
                    if is_recent:
                        obj.set_alarm_timeout()
                elif "tampering_detected_at" in data and obj.tampering_detected_at:
                    is_recent = obj.tampering_detected_at + RECENT_EVENT_MAX >= now
                    _LOGGER.debug("tampering_detected_at for %s (%s)", obj.id,
                                  is_recent)
                    if is_recent:
                        obj.set_tampering_timeout()

            devices[action["id"]] = obj

            self._create_stat(packet, list(data.keys()), False)
            return WSSubscriptionMessage(
                action=WSAction.UPDATE,
                new_update_id=self.last_update_id,
                changed_data=data,
                new_obj=obj,
                old_obj=old_obj,
            )

        # ignore updates to events that phase out
        if model_type != ModelType.EVENT.value:
            _LOGGER.debug("Unexpected %s: %s", key, action["id"])
        return None

    def process_ws_packet(
            self,
            packet: WSPacket,
            models: Optional[Set[ModelType]] = None,
            ignore_stats: bool = False) -> Optional[WSSubscriptionMessage]:
        if models is None:
            models = set()

        if not isinstance(packet.action_frame, WSJSONPacketFrame):
            _LOGGER.debug("Unexpected action frame format: %s",
                          packet.action_frame.payload_format)

        if not isinstance(packet.data_frame, WSJSONPacketFrame):
            _LOGGER.debug("Unexpected data frame format: %s",
                          packet.data_frame.payload_format)

        action, data = self._get_frame_data(packet)
        if action["newUpdateId"] is not None:
            self.last_update_id = UUID(action["newUpdateId"])

        if action["modelKey"] not in ModelType.values():
            _LOGGER.debug("Unknown model type: %s", action["modelKey"])
            self._create_stat(packet, [], True)
            return None

        if len(models) > 0 and ModelType(
                action["modelKey"]) not in models or len(data) == 0:
            self._create_stat(packet, [], True)
            return None

        if action["action"] == "add":
            return self._process_add_packet(packet, data)

        if action["action"] == "update":
            if action["modelKey"] == ModelType.NVR.value:
                return self._process_nvr_update(packet, data, ignore_stats)
            if action["modelKey"] in ModelType.bootstrap_models(
            ) or action["modelKey"] == ModelType.EVENT.value:
                return self._process_device_update(packet, action, data,
                                                   ignore_stats)
            _LOGGER.debug("Unexpected bootstrap model type for update: %s",
                          action["modelKey"])

        self._create_stat(packet, [], True)
        return None
Exemple #11
0
class MyPrivateAttr(BaseModel):
    _private_field: str = PrivateAttr()
Exemple #12
0
class Event(ProtectModelWithId):
    type: EventType
    start: datetime
    end: Optional[datetime]
    score: int
    heatmap_id: Optional[str]
    camera_id: Optional[str]
    smart_detect_types: List[SmartDetectObjectType]
    smart_detect_event_ids: List[str]
    thumbnail_id: Optional[str]
    user_id: Optional[str]
    timestamp: Optional[datetime]
    metadata: Optional[EventMetadata]

    # TODO:
    # partition

    _smart_detect_events: Optional[List[Event]] = PrivateAttr(None)
    _smart_detect_track: Optional[SmartDetectTrack] = PrivateAttr(None)
    _smart_detect_zones: Optional[Dict[int, CameraZone]] = PrivateAttr(None)

    @classmethod
    def _get_unifi_remaps(cls) -> Dict[str, str]:
        return {
            **super()._get_unifi_remaps(),
            "camera": "cameraId",
            "heatmap": "heatmapId",
            "user": "******",
            "thumbnail": "thumbnailId",
            "smartDetectEvents": "smartDetectEventIds",
        }

    @classmethod
    def unifi_dict_to_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
        for key in {"start", "end", "timestamp"}.intersection(data.keys()):
            data[key] = process_datetime(data, key)

        return super().unifi_dict_to_dict(data)

    @property
    def camera(self) -> Optional[Camera]:
        if self.camera_id is None:
            return None

        return self.api.bootstrap.cameras.get(self.camera_id)

    @property
    def light(self) -> Optional[Light]:
        if self.metadata is None or self.metadata.light_id is None:
            return None

        return self.api.bootstrap.lights.get(self.metadata.light_id)

    @property
    def sensor(self) -> Optional[Sensor]:
        if self.metadata is None or self.metadata.sensor_id is None:
            return None

        return self.api.bootstrap.sensors.get(self.metadata.sensor_id)

    @property
    def user(self) -> Optional[User]:
        if self.user_id is None:
            return None

        return self.api.bootstrap.users.get(self.user_id)

    @property
    def smart_detect_events(self) -> List[Event]:
        if self._smart_detect_events is not None:
            return self._smart_detect_events

        self._smart_detect_events = [
            self.api.bootstrap.events[g] for g in self.smart_detect_event_ids
            if g in self.api.bootstrap.events
        ]
        return self._smart_detect_events

    async def get_thumbnail(self,
                            width: Optional[int] = None,
                            height: Optional[int] = None) -> Optional[bytes]:
        """Gets thumbnail for event"""

        if self.thumbnail_id is None:
            return None
        return await self.api.get_event_thumbnail(self.thumbnail_id, width,
                                                  height)

    async def get_heatmap(self) -> Optional[bytes]:
        """Gets heatmap for event"""

        if self.heatmap_id is None:
            return None
        return await self.api.get_event_heatmap(self.heatmap_id)

    async def get_video(self, channel_index: int = 0) -> Optional[bytes]:
        """Get the MP4 video clip for this given event

        Args:

        * `channel_index`: index of `CameraChannel` on the camera to use to retrieve video from

        Will raise an exception if event does not have a camera, end time or the channel index is wrong.
        """

        if self.camera is None:
            raise BadRequest("Event does not have a camera")
        if self.end is None:
            raise BadRequest("Event is ongoing")

        return await self.api.get_camera_video(self.camera.id, self.start,
                                               self.end, channel_index)

    async def get_smart_detect_track(self) -> SmartDetectTrack:
        """
        Gets smart detect track for given smart detect event.

        If event is not a smart detect event, it will raise a `BadRequest`
        """

        if self.type != EventType.SMART_DETECT:
            raise BadRequest("Not a smart detect event")

        if self._smart_detect_track is None:
            self._smart_detect_track = await self.api.get_event_smart_detect_track(
                self.id)

        return self._smart_detect_track

    async def get_smart_detect_zones(self) -> Dict[int, CameraZone]:
        """Gets the triggering zones for the smart detection"""

        if self.camera is None:
            raise BadRequest("No camera on event")

        if self._smart_detect_zones is None:
            smart_track = await self.get_smart_detect_track()

            ids: Set[int] = set()
            for item in smart_track.payload:
                ids = ids | set(item.zone_ids)

            self._smart_detect_zones = {
                z.id: z
                for z in self.camera.smart_detect_zones if z.id in ids
            }

        return self._smart_detect_zones
Exemple #13
0
class ProtectBaseObject(BaseModel):
    """
    Base class for building Python objects from Unifi Protect JSON.

    * Provides `.unifi_dict_to_dict` to convert UFP JSON to a more Pythonic formatted dict (camel case to snake case)
    * Add attrs with matching Pyhonic name and they will automatically be populated from the UFP JSON if passed in to the constructer
    * Provides `.unifi_dict` to convert object back into UFP JSON
    """

    _api: Optional[ProtectApiClient] = PrivateAttr(None)
    _initial_data: Dict[str, Any] = PrivateAttr()

    _protect_objs: ClassVar[Optional[Dict[str,
                                          Type[ProtectBaseObject]]]] = None
    _protect_lists: ClassVar[Optional[Dict[str,
                                           Type[ProtectBaseObject]]]] = None
    _protect_dicts: ClassVar[Optional[Dict[str,
                                           Type[ProtectBaseObject]]]] = None

    class Config:
        arbitrary_types_allowed = True
        validate_assignment = True

    def __init__(self,
                 api: Optional[ProtectApiClient] = None,
                 **data: Any) -> None:
        """
        Base class for creating Python objects from UFP JSON data.

        Use the static method `.from_unifi_dict()` to create objects from UFP JSON data from then the main class constructor.
        """
        super().__init__(**data)

        self._initial_data = self.dict(
            exclude=self._get_excluded_changed_fields())
        self._api = api

    @classmethod
    def from_unifi_dict(cls,
                        api: Optional[ProtectApiClient] = None,
                        **data: Any) -> ProtectObject:
        """
        Main constructor for `ProtectBaseObject`

        Args:

        * `api`: Optional reference to the ProtectAPIClient that created generated the UFP JSON
        * `data`: decoded UFP JSON

        `api` is is expected as a `@property`. If it is `None` and accessed, a `BadRequest` will be raised.

        API can be used for saving updates for the Protect object or fetching references to other objects
        (cameras, users, etc.)
        """

        data["api"] = api
        data = cls.unifi_dict_to_dict(data)

        if is_debug():
            data.pop("api", None)
            return cls(api=api, **data)  # type: ignore

        obj = cls.construct(**data)
        return obj  # type: ignore

    @classmethod
    def construct(cls,
                  _fields_set: Optional[Set[str]] = None,
                  **values: Any) -> ProtectObject:
        api = values.pop("api", None)
        for key, klass in cls._get_protect_objs().items():
            if key in values and isinstance(values[key], dict):
                values[key] = klass.construct(**values[key])

        for key, klass in cls._get_protect_lists().items():
            if key in values and isinstance(values[key], list):
                values[key] = [
                    klass.construct(**v) if isinstance(v, dict) else v
                    for v in values[key]
                ]

        for key, klass in cls._get_protect_dicts().items():
            if key in values and isinstance(values[key], dict):
                values[key] = {
                    k: klass.construct(**v) if isinstance(v, dict) else v
                    for k, v in values[key].items()
                }

        obj = super().construct(_fields_set=_fields_set, **values)
        obj._initial_data = obj.dict(
            exclude=cls._get_excluded_changed_fields())  # pylint: disable=protected-access
        obj._api = api  # pylint: disable=protected-access

        return obj  # type: ignore

    @classmethod
    def _get_excluded_changed_fields(cls) -> Set[str]:
        """
        Helper method for override in child classes for fields that excluded from calculating "changed" state for a
        model (`.initial_data` and `.get_changed()`)
        """
        return set()

    @classmethod
    def _get_unifi_remaps(cls) -> Dict[str, str]:
        """
        Helper method for overriding in child classes for remapping UFP JSON keys to Python ones that do not fit the
        simple camel case to snake case formula.

        Return format is
        {
            "ufpJsonName": "python_name"
        }
        """

        return {}

    @classmethod
    def _set_protect_subtypes(cls) -> None:
        """Helper method to detect attrs of current class that are UFP Objects themselves"""

        cls._protect_objs = {}
        cls._protect_lists = {}
        cls._protect_dicts = {}

        for name, field in cls.__fields__.items():
            try:
                if issubclass(field.type_, ProtectBaseObject):
                    if field.shape == SHAPE_LIST:
                        cls._protect_lists[name] = field.type_
                    if field.shape == SHAPE_DICT:
                        cls._protect_dicts[name] = field.type_
                    else:
                        cls._protect_objs[name] = field.type_
            except TypeError:
                pass

    @classmethod
    def _get_protect_objs(cls) -> Dict[str, Type[ProtectBaseObject]]:
        """Helper method to get all child UFP objects"""
        if cls._protect_objs is not None:
            return cls._protect_objs

        cls._set_protect_subtypes()
        return cls._protect_objs  # type: ignore

    @classmethod
    def _get_protect_lists(cls) -> Dict[str, Type[ProtectBaseObject]]:
        """Helper method to get all child of UFP objects (lists)"""
        if cls._protect_lists is not None:
            return cls._protect_lists

        cls._set_protect_subtypes()
        return cls._protect_lists  # type: ignore

    @classmethod
    def _get_protect_dicts(cls) -> Dict[str, Type[ProtectBaseObject]]:
        """Helper method to get all child of UFP objects (dicts)"""
        if cls._protect_dicts is not None:
            return cls._protect_dicts

        cls._set_protect_subtypes()
        return cls._protect_dicts  # type: ignore

    @classmethod
    def _get_api(
            cls,
            api: Optional[ProtectApiClient]) -> Optional[ProtectApiClient]:
        """Helper method to try to find and the current ProjtectAPIClient instance from given data"""
        if api is None and isinstance(cls, ProtectBaseObject) and hasattr(
                cls, "_api"):
            api = cls._api

        return api

    @classmethod
    def _clean_protect_obj(cls, data: Any, klass: Type[ProtectBaseObject],
                           api: Optional[ProtectApiClient]) -> Any:
        if isinstance(data, dict):
            if api is not None:
                data["api"] = api
            return klass.unifi_dict_to_dict(data=data)
        return data

    @classmethod
    def _clean_protect_obj_list(cls, items: List[Any],
                                klass: Type[ProtectBaseObject],
                                api: Optional[ProtectApiClient]) -> List[Any]:
        cleaned_items: List[Any] = []
        for item in items:
            cleaned_items.append(cls._clean_protect_obj(item, klass, api))
        return cleaned_items

    @classmethod
    def _clean_protect_obj_dict(
            cls, items: Dict[Any, Any], klass: Type[ProtectBaseObject],
            api: Optional[ProtectApiClient]) -> Dict[Any, Any]:
        cleaned_items: Dict[Any, Any] = {}
        for key, value in items.items():
            cleaned_items[key] = cls._clean_protect_obj(value, klass, api)
        return cleaned_items

    @classmethod
    def unifi_dict_to_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
        """
        Takes a decoded UFP JSON dict and converts it into a Python dict

        * Remaps items from `._get_unifi_remaps()`
        * Converts camelCase keys to snake_case keys
        * Injects ProtectAPIClient into any child UFP object Dicts
        * Runs `.unifi_dict_to_dict` for any child UFP objects

        Args:

        * `data`: decoded UFP JSON dict
        """

        # get the API client instance
        api = cls._get_api(data.get("api", None))

        # remap keys that will not be converted correctly by snake_case convert
        for from_key, to_key in cls._get_unifi_remaps().items():
            if from_key in data:
                data[to_key] = data.pop(from_key)

        # convert to snake_case
        for key in list(data.keys()):
            data[to_snake_case(key)] = data.pop(key)

        # remove extra fields
        for key, value in list(data.items()):
            if key == "api":
                continue

            if key not in cls.__fields__:
                del data[key]
                continue
            data[key] = convert_unifi_data(value, cls.__fields__[key])

        # clean child UFP objs
        for key, klass in cls._get_protect_objs().items():
            if key in data:
                data[key] = cls._clean_protect_obj(data[key], klass, api)

        for key, klass in cls._get_protect_lists().items():
            if key in data and isinstance(data[key], list):
                data[key] = cls._clean_protect_obj_list(data[key], klass, api)

        for key, klass in cls._get_protect_dicts().items():
            if key in data and isinstance(data[key], dict):
                data[key] = cls._clean_protect_obj_dict(data[key], klass, api)

        return data

    def _unifi_dict_protect_obj(self, data: Dict[str, Any], key: str,
                                use_obj: bool) -> Any:
        value: Optional[Any] = data.get(key)
        if use_obj:
            value = getattr(self, key)

        if isinstance(value, ProtectBaseObject):
            value = value.unifi_dict()

        return value

    def _unifi_dict_protect_obj_list(self, data: Dict[str, Any], key: str,
                                     use_obj: bool) -> Any:
        value: Optional[Any] = data.get(key)
        if use_obj:
            value = getattr(self, key)

        if not isinstance(value, list):
            return value

        items: List[Any] = []
        for item in value:
            if isinstance(item, ProtectBaseObject):
                item = item.unifi_dict()
            items.append(item)

        return items

    def _unifi_dict_protect_obj_dict(self, data: Dict[str, Any], key: str,
                                     use_obj: bool) -> Any:
        value: Optional[Any] = data.get(key)
        if use_obj:
            value = getattr(self, key)

        if not isinstance(value, dict):
            return value

        items: Dict[Any, Any] = {}
        for obj_key, obj in value.items():
            if isinstance(obj, ProtectBaseObject):
                obj = obj.unifi_dict()
            items[obj_key] = obj

        return items

    def unifi_dict(self,
                   data: Optional[Dict[str, Any]] = None,
                   exclude: Optional[Set[str]] = None) -> Dict[str, Any]:
        """
        Can either convert current Python object into UFP JSON dict or take the output of a `.dict()` call and convert it.

        * Remaps items from `._get_unifi_remaps()` in reverse
        * Converts snake_case to camelCase
        * Automatically removes any ProtectApiClient instances that might still be in the data
        * Automaitcally calls `.unifi_dict()` for any UFP Python objects that are detected

        Args:

        `data`: Optional output of `.dict()` for the Python object. If `None`, will call `.dict` first
        `exclude`: Optional set of fields to exclude from convert. Useful for subclassing and having custom
            processing for dumping to UFP JSON data.
        """

        use_obj = False
        if data is None:
            excluded_fields = set(self._get_protect_objs().keys()) | set(
                self._get_protect_lists().keys())
            if exclude is not None:
                excluded_fields = excluded_fields | exclude
            data = self.dict(exclude=excluded_fields)
            use_obj = True

        for key in self._get_protect_objs().keys():
            if use_obj or key in data:
                data[key] = self._unifi_dict_protect_obj(data, key, use_obj)

        for key in self._get_protect_lists().keys():
            if use_obj or key in data:
                data[key] = self._unifi_dict_protect_obj_list(
                    data, key, use_obj)

        for key in self._get_protect_dicts().keys():
            if use_obj or key in data:
                data[key] = self._unifi_dict_protect_obj_dict(
                    data, key, use_obj)

        data: Dict[str, Any] = serialize_unifi_obj(data)
        for to_key, from_key in self._get_unifi_remaps().items():
            if from_key in data:
                data[to_key] = data.pop(from_key)

        if "api" in data:
            del data["api"]

        return data

    def _inject_api(self, data: Dict[str, Any],
                    api: Optional[ProtectApiClient]) -> Dict[str, Any]:
        data["api"] = api

        for key in self._get_protect_objs().keys():
            if key in data:
                unifi_obj: Optional[Any] = getattr(self, key)
                if unifi_obj is not None and isinstance(unifi_obj, dict):
                    unifi_obj["api"] = api

        for key in self._get_protect_lists().keys():
            if key in data:
                new_items = []
                for item in data[key]:
                    if isinstance(item, dict):
                        item["api"] = api
                    new_items.append(item)
                data[key] = new_items

        for key in self._get_protect_dicts().keys():
            if key in data:
                for item_key, item in data[key].items():
                    if isinstance(item, dict):
                        item["api"] = api
                    data[key][item_key] = item

        return data

    def update_from_dict(self: ProtectObject,
                         data: Dict[str, Any]) -> ProtectObject:
        """Updates current object from a cleaned UFP JSON dict"""
        for key in self._get_protect_objs().keys():
            if key in data:
                unifi_obj: Optional[Any] = getattr(self, key)
                if unifi_obj is not None and isinstance(
                        unifi_obj, ProtectBaseObject):
                    setattr(self, key,
                            unifi_obj.update_from_dict(data.pop(key)))

        if "api" in data:
            del data["api"]

        if is_debug():
            return self.copy(update=data)

        new_data = self.dict()
        new_data.update(data)
        new_data = self._inject_api(new_data, self._api)
        return self.construct(**new_data)  # type: ignore

    def get_changed(self: ProtectObject) -> Dict[str, Any]:
        return dict_diff(self._initial_data, self.dict())

    @property
    def api(self) -> ProtectApiClient:
        """
        ProtectApiClient that the UFP object was created with. If no API Client was passed in time of
        creation, will raise `BadRequest`
        """
        if self._api is None:
            raise BadRequest("API Client not initialized")

        return self._api
Exemple #14
0
class MeshFace(BaseModel):
    """Mesh Face."""

    # Face Index.
    fidx: int
    # Vertices that makeup face.
    vertex_indices: List[int]
    # Face normal.
    normal: Optional[Tuple[float, float, float]]
    # Face area.
    area: float
    # Vector representing face centroids.
    centroid: Optional[Tuple[float, float, float]]
    # Parent Mesh Data.
    _parent: Optional["MeshData"] = PrivateAttr(default=None)

    class Config:
        keep_untouched = (cached_property,)

    @property
    def vertices(self) -> List[MeshPoint]:
        return [v for v in self._parent.vertices if v.vidx in self.vertex_indices]

    @cached_property
    def sympy_vertices(self) -> List[S.Point3D]:
        return [p.as_sympy for p in self.vertices]

    @property
    def euclid_vertices(self) -> List[Point3]:
        return [p.as_euclid for p in self.vertices]

    @cached_property
    def as_sympy_plane(self) -> S.Plane:
        return S.Plane(*self.sympy_vertices, normal_vector=self.normal)

    @cached_property
    def missing_rect_vertex(self) -> S.Point3D:
        points_perms = itertools.permutations(self.sympy_vertices, 3)
        least_dist = None
        least_dist_point = None
        for perm in points_perms:
            midp = find_missing_rect_vertex(*perm)
            dist = self.centroid_point.distance(midp)
            if least_dist is None or least_dist > dist:
                least_dist = dist
                least_dist_point = midp
        return least_dist_point

    @cached_property
    def midpoint_by_canberra(self) -> S.Point3D:
        fp_1 = self.vertices[0].as_sympy
        fp_2 = max(self.sympy_vertices, key=lambda p: fp_1.canberra_distance(p))
        return fp_1.midpoint(fp_2)

    @property
    def normal_vector(self) -> Vector3:
        return Vector3(*self.normal)

    @cached_property
    def centroid_point(self) -> S.Point3D:
        return S.Point3D(*self.centroid)
Exemple #15
0
class CameraConfig(FrigateBaseModel):
    name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
    ffmpeg: CameraFfmpegConfig = Field(
        title="FFmpeg configuration for the camera.")
    best_image_timeout: int = Field(
        default=60,
        title=
        "How long to wait for the image with the highest confidence score.",
    )
    zones: Dict[str, ZoneConfig] = Field(default_factory=dict,
                                         title="Zone configuration.")
    record: RecordConfig = Field(default_factory=RecordConfig,
                                 title="Record configuration.")
    rtmp: RtmpConfig = Field(default_factory=RtmpConfig,
                             title="RTMP restreaming configuration.")
    live: CameraLiveConfig = Field(default_factory=CameraLiveConfig,
                                   title="Live playback settings.")
    snapshots: SnapshotsConfig = Field(default_factory=SnapshotsConfig,
                                       title="Snapshot configuration.")
    mqtt: CameraMqttConfig = Field(default_factory=CameraMqttConfig,
                                   title="MQTT configuration.")
    objects: ObjectConfig = Field(default_factory=ObjectConfig,
                                  title="Object configuration.")
    motion: Optional[MotionConfig] = Field(
        title="Motion detection configuration.")
    detect: DetectConfig = Field(default_factory=DetectConfig,
                                 title="Object detection configuration.")
    timestamp_style: TimestampStyleConfig = Field(
        default_factory=TimestampStyleConfig,
        title="Timestamp style configuration.")
    _ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()

    def __init__(self, **config):
        # Set zone colors
        if "zones" in config:
            colors = plt.cm.get_cmap("tab10", len(config["zones"]))
            config["zones"] = {
                name: {
                    **z, "color":
                    tuple(round(255 * c) for c in colors(idx)[:3])
                }
                for idx, (name, z) in enumerate(config["zones"].items())
            }

        # add roles to the input if there is only one
        if len(config["ffmpeg"]["inputs"]) == 1:
            config["ffmpeg"]["inputs"][0]["roles"] = [
                "record", "rtmp", "detect"
            ]

        super().__init__(**config)

    @property
    def frame_shape(self) -> Tuple[int, int]:
        return self.detect.height, self.detect.width

    @property
    def frame_shape_yuv(self) -> Tuple[int, int]:
        return self.detect.height * 3 // 2, self.detect.width

    @property
    def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
        return self._ffmpeg_cmds

    def create_ffmpeg_cmds(self):
        if "_ffmpeg_cmds" in self:
            return
        ffmpeg_cmds = []
        for ffmpeg_input in self.ffmpeg.inputs:
            ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
            if ffmpeg_cmd is None:
                continue

            ffmpeg_cmds.append({
                "roles": ffmpeg_input.roles,
                "cmd": ffmpeg_cmd
            })
        self._ffmpeg_cmds = ffmpeg_cmds

    def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
        ffmpeg_output_args = []
        if "detect" in ffmpeg_input.roles:
            detect_args = (self.ffmpeg.output_args.detect if isinstance(
                self.ffmpeg.output_args.detect, list) else
                           self.ffmpeg.output_args.detect.split(" "))
            ffmpeg_output_args = ([
                "-r",
                str(self.detect.fps),
                "-s",
                f"{self.detect.width}x{self.detect.height}",
            ] + detect_args + ffmpeg_output_args + ["pipe:"])
        if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
            rtmp_args = (self.ffmpeg.output_args.rtmp if isinstance(
                self.ffmpeg.output_args.rtmp, list) else
                         self.ffmpeg.output_args.rtmp.split(" "))
            ffmpeg_output_args = (rtmp_args +
                                  [f"rtmp://127.0.0.1/live/{self.name}"] +
                                  ffmpeg_output_args)
        if "record" in ffmpeg_input.roles and self.record.enabled:
            record_args = (self.ffmpeg.output_args.record if isinstance(
                self.ffmpeg.output_args.record, list) else
                           self.ffmpeg.output_args.record.split(" "))

            ffmpeg_output_args = (
                record_args +
                [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"] +
                ffmpeg_output_args)

        # if there arent any outputs enabled for this input
        if len(ffmpeg_output_args) == 0:
            return None

        global_args = ffmpeg_input.global_args or self.ffmpeg.global_args
        hwaccel_args = ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args
        input_args = ffmpeg_input.input_args or self.ffmpeg.input_args

        global_args = (global_args if isinstance(global_args, list) else
                       global_args.split(" "))
        hwaccel_args = (hwaccel_args if isinstance(hwaccel_args, list) else
                        hwaccel_args.split(" "))
        input_args = (input_args if isinstance(input_args, list) else
                      input_args.split(" "))

        cmd = (["ffmpeg"] + global_args + hwaccel_args + input_args +
               ["-i", ffmpeg_input.path] + ffmpeg_output_args)

        return [part for part in cmd if part != ""]
Exemple #16
0
class AEMOTableSchema(BaseConfig):
    name: str
    namespace: str
    fieldnames: List[str]
    records: List[Dict[str, Any] | list] = Field(default_factory=list)

    # optionally it has a schema
    _record_schema: Optional[MMSBaseClass] = PrivateAttr()

    # the url this table was taken from if any
    url_source: Optional[str]

    # the original content ?!?!
    # @NOTE does this make sense .. (it doesnt because it can be super large)
    content_source: Optional[str]

    @property
    def full_name(self) -> str:
        return "{}_{}".format(self.namespace, self.name)

    @property
    def primary_key(self) -> Optional[List[str]]:
        if (
            hasattr(self, "_record_schema")
            and isinstance(self._record_schema, MMSBaseClass)
            and hasattr(self._record_schema, "_primary_keys")
        ):
            return self._record_schema._primary_keys

        return None

    @validator("name")
    def validate_name(cls, table_name: str) -> str:
        _table_name = table_name.strip().lower()

        return _table_name

    @validator("namespace")
    def validate_namespace(cls, namespace_name: str) -> str:
        _namespace_name = namespace_name.strip().lower()

        return _namespace_name

    @validator("fieldnames")
    def validate_fieldnames(cls, fieldnames: List[str]) -> List[str]:
        if not isinstance(fieldnames, list):
            return []

        _fieldnames = [i.lower() for i in fieldnames]

        return _fieldnames

    def set_schema(self, schema: MMSBaseClass) -> bool:
        self._record_schema = schema

        return True

    def add_record(self, record: Union[Dict, MMSBaseClass], values_only: bool = False) -> bool:
        if isinstance(record, dict) and hasattr(self, "_record_schema") and self._record_schema:
            _record = None

            if not isinstance(record, dict):
                raise Exception("Could not add record, not a dict: {}".format(record))

            try:
                _record = self._record_schema(**record)  # type: ignore
            except ValidationError as e:
                val_errors = e.errors()
                logger.debug(record)

                for ve in val_errors:
                    ve_fieldname = ve["loc"][0]
                    ve_val = ""

                    if record and isinstance(record, dict) and ve_fieldname in record:
                        ve_val = record[ve_fieldname]

                return False
            except Exception as e:
                logger.error("Record error: {}".format(e))
                return False

            if values_only:
                self.records.append(list(_record.values()))
            else:
                self.records.append(_record)

        else:
            if values_only and isinstance(record, dict):
                self.records.append(list(record.values()))
            elif values_only and isinstance(record, list):
                self.records.append(record)
            else:
                self.records.append(record)

        return True

    def to_frame(self) -> Any:
        """Return a pandas dataframe for the table"""
        if not _HAVE_PANDAS:
            return None

        _index_keys = []

        _df = pd.DataFrame(self.records)

        if hasattr(self, "_record_schema") and self._record_schema:
            if hasattr(self._record_schema, "_primary_keys"):
                _index_keys = self._record_schema._primary_keys  # type: ignore

        if len(_index_keys) > 0:
            logger.debug("Setting index to {}".format(_index_keys))
            try:
                _df = _df.set_index(_index_keys)
            except KeyError:
                logger.warn("Could not set index with columns: {}".format(", ".join(_index_keys)))

        return _df

    def to_csv(self, filename: str) -> None:
        logger.info(f"Writing table {self.full_name} with {len(self.records)} records")

        with open(filename, "w") as fh:
            csvwriter = csv.DictWriter(fh, fieldnames=self.fieldnames)
            csvwriter.writeheader()

            for record in self.records:
                csvwriter.writerow(dict(zip(self.fieldnames, record)))

        logger.info(f"Wrote records to {self.full_name}")

    class Config:
        underscore_attrs_are_private = True
Exemple #17
0
class NameServerConfiguration(BaseSettings, PyroConfigMixin, YAMLMixin):
    """
    The NameServer Settings class. 
    
    Contains all applicable configuration parameters for running a nameserver.

    Parameters
    ----------
    host : str, optional
        The hostname of the nameserver. Defaults to "localhost" for security.
        Can be set to "public", which is dynamically translated to the 
        machine's ip address when the nameserver is started.
    ns_port : int, optional
        The port of the nameserver. Defaults to 9090.
    broadcast : bool, optional
        Whether to launch a broadcast server. Defaults to False.
    ns_bchost : str, optional
        The hostname of the broadcast server. Defaults to None.
    ns_bcport : int, optional
        The port of the broadcast server. Defaults to 9091.
    ns_autoclean : float, optional
        The interval in seconds at which the nameserver will ping registered
        objects and clean up unresponsive ones. Default is 0.0 (off).
    storage : str, optional
        A Pyro5-style storage string. You have several options:

        * ``memory``: Fast, volatile in-memory database. This is the default.  
        * ``dbm[:dbfile]``: Persistent database using dbm. Optionally provide 
          the filename to use (ignore for PyroLab to create automatically). This 
          storage type does not support metadata.  
        * ``sql[:dbfile]``: Persistent database using sqlite. Optionally 
          provide the filename to use (ignore for PyroLab to create 
          automatically).

    Examples
    --------
    The following are examples of valid YAML configurations for nameservers.
    Keys not defined assume the default values.

    .. code-block:: yaml

        host: localhost
        ns_port: 9090
        ns_autoclean: 0.0
        storage: memory
    
    .. code-block:: yaml

        host: public
        ns_port: 9100
        broadcast: false
        ns_bchost: null
        ns_bcport: 9091
        ns_autoclean: 15.0
        storage: sql
    """
    host: str = "localhost"
    ns_port: int = 9090
    broadcast: bool = False
    ns_bchost: Optional[bool] = None
    ns_bcport: int = 9091
    ns_autoclean: float = 0.0
    storage: str = "memory"
    _name: str = PrivateAttr("")

    @validator('storage')
    def valid_memory_format(cls, v: str):
        if v == "memory":
            return v
        elif any(v.startswith(storage) for storage in ["dbm", "sql"]):
            return v
        else:
            raise ValueError(f"Invalid storage specification: {v}")

    @property
    def name(self) -> str:
        return self._name

    def set_name(self, name: str) -> None:
        self._name = name

    def get_storage_location(self) -> Path:
        """
        Returns the storage location for the given name.

        Returns
        -------
        Path
            The path to the storage location.
        """
        if self.storage in ["sql", "dbm"]:
            return f"{self.storage}:" + str(
                NAMESERVER_STORAGE / f"ns_{self.name}.{self.storage}")
        return self.storage

    def update_pyro_config(self) -> Dict[str, Any]:
        """
        Sets all key-value attributes that are Pyro5 configuration options.

        Pyro5 attributes that this function automatically translates:
        | * HOST: "public" is translated to the machine's ip address
        | * NS_HOST: "public" is translated to the machine's ip address
        | * NS_BCHOST: "public" is translated to the machine's ip address

        One side effect of this function is that it also updates NS_HOST to the 
        same value as HOST. This is usually desirable.

        Parameters
        ----------
        values : dict, optional
            A dictionary of key-value pairs to update the configuration. If not
            provided, the model's attributes will be used.
        
        Returns
        -------
        dict
            A dictionary of Pyro5 key-value pairs that were updated, for 
            debugging or informational purposes.
        """
        values = self.dict()
        values['ns_host'] = values['host']
        return super().update_pyro_config(values=values)
Exemple #18
0
class _Config(BaseSettings):
    # General Config.
    RENDERS_DIR: Path = Path("renders")

    # OpenSCAD Poly Segments.
    SEGMENTS: int = 48
    # OpenSCAD Libraries.
    TMP_DIR: Path = Path("/tmp/") / "threedframe"
    LIB_DIR: Path = ROOT / "lib"
    MCAD_DIR: Path = LIB_DIR / "MCAD"
    DOTSCAD_DIR: Path = LIB_DIR / "dotSCAD" / "src"

    _mcad: Optional[Any] = PrivateAttr(None)
    _dotSCAD: Optional[Any] = PrivateAttr(None)

    @validator("LIB_DIR", "MCAD_DIR", "DOTSCAD_DIR")
    def validate_libs(cls, v: Path) -> Path:
        if not v.exists():
            raise ValueError(f"Missing library: {v}")
        return v

    @validator("TMP_DIR", pre=True)
    def validate_tmp_dir(cls, v: Path) -> Path:
        v.mkdir(exist_ok=True)
        return v

    def create_lib_dir(self, lib_dir: Path):
        """Create library directory.

        Copies given lib to /tmp/ directory to
        simplify previewing generated models on host.

        """
        rel_dir = lib_dir.relative_to(self.LIB_DIR)
        target_dir = self.TMP_DIR / rel_dir
        if target_dir.exists():
            return target_dir
        target_dir.mkdir(parents=True, exist_ok=True)
        shutil.copytree(lib_dir, target_dir, dirs_exist_ok=True)
        return target_dir

    def setup_libs(self):
        mcad = self.create_lib_dir(self.MCAD_DIR)
        dotscad = self.create_lib_dir(self.DOTSCAD_DIR)
        with quiet_solid():
            self._mcad = solid.import_scad(str(mcad))
            self._dotSCAD = solid.import_scad(str(dotscad))

    @property
    def mcad(self):
        if not self._mcad:
            self.setup_libs()
        return self._mcad

    @property
    def dotSCAD(self):
        if not self._dotSCAD:
            self.setup_libs()
        return self._dotSCAD

    # Model Params
    GAP: float = 0.02  # 3dPrinting fudge factor.
    CORE_SIZE: float = 1.4 * Constants.INCH
    SUPPORT_SIZE: float = 0.69 * Constants.INCH
    FIXTURE_WALL_THICKNESS: float = 6.0
    FIXTURE_HOLE_SIZE: float = SUPPORT_SIZE + GAP
    FIXTURE_SIZE: float = FIXTURE_HOLE_SIZE + FIXTURE_WALL_THICKNESS