1+ from typing import Iterator , Dict , Type , Union
2+ import logging
3+ import json
4+ import os
5+ import hashlib
6+ import threading
7+ import weakref
8+
9+ from app .model import AssetAdministrationShellDescriptor , SubmodelDescriptor
10+ from basyx .aas import model
11+ from basyx .aas .model import provider as sdk_provider
12+
13+ from app .model .descriptor import Descriptor
14+ from app .adapter import jsonization
15+
16+
17+ logger = logging .getLogger (__name__ )
18+
19+
20+ # We need to resolve the Descriptor type in order to deserialize it again from JSON
21+ DESCRIPTOR_TYPE_TO_STRING : Dict [Type [Union [AssetAdministrationShellDescriptor , SubmodelDescriptor ]], str ] = {
22+ AssetAdministrationShellDescriptor : "AssetAdministrationShellDescriptor" ,
23+ SubmodelDescriptor : "SubmodelDescriptor" ,
24+ }
25+
26+
27+ class LocalFileDescriptorStore (sdk_provider .AbstractObjectStore [model .Identifier , Descriptor ]):
28+ """
29+ An ObjectStore implementation for :class:`~app.model.descriptor.Descriptor` BaSyx Python SDK objects backed
30+ by a local file based local backend
31+ """
32+ def __init__ (self , directory_path : str ):
33+ """
34+ Initializer of class LocalFileDescriptorStore
35+
36+ :param directory_path: Path to the local file backend (the path where you want to store your AAS JSON files)
37+ """
38+ self .directory_path : str = directory_path .rstrip ("/" )
39+
40+ # A dictionary of weak references to local replications of stored objects. Objects are kept in this cache as
41+ # long as there is any other reference in the Python application to them. We use this to make sure that only one
42+ # local replication of each object is kept in the application and retrieving an object from the store always
43+ # returns the **same** (not only equal) object. Still, objects are forgotten, when they are not referenced
44+ # anywhere else to save memory.
45+ self ._object_cache : weakref .WeakValueDictionary [model .Identifier , Descriptor ] \
46+ = weakref .WeakValueDictionary ()
47+ self ._object_cache_lock = threading .Lock ()
48+
49+ def check_directory (self , create = False ):
50+ """
51+ Check if the directory exists and created it if not (and requested to do so)
52+
53+ :param create: If True and the database does not exist, try to create it
54+ """
55+ if not os .path .exists (self .directory_path ):
56+ if not create :
57+ raise FileNotFoundError ("The given directory ({}) does not exist" .format (self .directory_path ))
58+ # Create directory
59+ os .mkdir (self .directory_path )
60+ logger .info ("Creating directory {}" .format (self .directory_path ))
61+
62+ def get_descriptor_by_hash (self , hash_ : str ) -> Descriptor :
63+ """
64+ Retrieve an AAS Descriptor object from the local file by its identifier hash
65+
66+ :raises KeyError: If the respective file could not be found
67+ """
68+ # Try to get the correct file
69+ try :
70+ with open ("{}/{}.json" .format (self .directory_path , hash_ ), "r" ) as file :
71+ obj = json .load (file , cls = jsonization .ServerAASFromJsonDecoder )
72+ except FileNotFoundError as e :
73+ raise KeyError ("No Descriptor with hash {} found in local file database" .format (hash_ )) from e
74+ # If we still have a local replication of that object (since it is referenced from anywhere else), update that
75+ # replication and return it.
76+ with self ._object_cache_lock :
77+ if obj .id in self ._object_cache :
78+ old_obj = self ._object_cache [obj .id ]
79+ old_obj .update_from (obj )
80+ return old_obj
81+ self ._object_cache [obj .id ] = obj
82+ return obj
83+
84+ def get_item (self , identifier : model .Identifier ) -> Descriptor :
85+ """
86+ Retrieve an AAS Descriptor object from the local file by its :class:`~basyx.aas.model.base.Identifier`
87+
88+ :raises KeyError: If the respective file could not be found
89+ """
90+ try :
91+ return self .get_descriptor_by_hash (self ._transform_id (identifier ))
92+ except KeyError as e :
93+ raise KeyError ("No Identifiable with id {} found in local file database" .format (identifier )) from e
94+
95+ def add (self , x : Descriptor ) -> None :
96+ """
97+ Add a Descriptor object to the store
98+
99+ :raises KeyError: If an object with the same id exists already in the object store
100+ """
101+ logger .debug ("Adding object %s to Local File Store ..." , repr (x ))
102+ if os .path .exists ("{}/{}.json" .format (self .directory_path , self ._transform_id (x .id ))):
103+ raise KeyError ("Descriptor with id {} already exists in local file database" .format (x .id ))
104+ with open ("{}/{}.json" .format (self .directory_path , self ._transform_id (x .id )), "w" ) as file :
105+ # Usually, we don't need to serialize the modelType, since during HTTP requests, we know exactly if this
106+ # is an AASDescriptor or SubmodelDescriptor. However, here we cannot distinguish them, so to deserialize
107+ # them successfully, we hack the `modelType` into the JSON.
108+ serialized = json .loads (
109+ json .dumps (x , cls = jsonization .ServerAASToJsonEncoder )
110+ )
111+ serialized ["modelType" ] = DESCRIPTOR_TYPE_TO_STRING [type (x )]
112+ json .dump (serialized , file , indent = 4 )
113+ with self ._object_cache_lock :
114+ self ._object_cache [x .id ] = x
115+
116+ def discard (self , x : Descriptor ) -> None :
117+ """
118+ Delete an :class:`~app.model.descriptor.Descriptor` AAS object from the local file store
119+
120+ :param x: The object to be deleted
121+ :raises KeyError: If the object does not exist in the database
122+ """
123+ logger .debug ("Deleting object %s from Local File Store database ..." , repr (x ))
124+ try :
125+ os .remove ("{}/{}.json" .format (self .directory_path , self ._transform_id (x .id )))
126+ except FileNotFoundError as e :
127+ raise KeyError ("No AAS Descriptor object with id {} exists in local file database" .format (x .id )) from e
128+ with self ._object_cache_lock :
129+ self ._object_cache .pop (x .id , None )
130+
131+ def __contains__ (self , x : object ) -> bool :
132+ """
133+ Check if an object with the given :class:`~basyx.aas.model.base.Identifier` or the same
134+ :class:`~basyx.aas.model.base.Identifier` as the given object is contained in the local file database
135+
136+ :param x: AAS object :class:`~basyx.aas.model.base.Identifier` or :class:`~app.model.descriptor.Descriptor`
137+ AAS object
138+ :return: ``True`` if such an object exists in the database, ``False`` otherwise
139+ """
140+ if isinstance (x , model .Identifier ):
141+ identifier = x
142+ elif isinstance (x , Descriptor ):
143+ identifier = x .id
144+ else :
145+ return False
146+ logger .debug ("Checking existence of Descriptor object with id %s in database ..." , repr (x ))
147+ return os .path .exists ("{}/{}.json" .format (self .directory_path , self ._transform_id (identifier )))
148+
149+ def __len__ (self ) -> int :
150+ """
151+ Retrieve the number of objects in the local file database
152+
153+ :return: The number of objects (determined from the number of documents)
154+ """
155+ logger .debug ("Fetching number of documents from database ..." )
156+ return len (os .listdir (self .directory_path ))
157+
158+ def __iter__ (self ) -> Iterator [Descriptor ]:
159+ """
160+ Iterate all :class:`~app.model.descriptor.Descriptor` objects in the local folder.
161+
162+ This method returns an iterator, containing only a list of all identifiers in the database and retrieving
163+ the identifiable objects on the fly.
164+ """
165+ logger .debug ("Iterating over objects in database ..." )
166+ for name in os .listdir (self .directory_path ):
167+ yield self .get_descriptor_by_hash (name .rstrip (".json" ))
168+
169+ @staticmethod
170+ def _transform_id (identifier : model .Identifier ) -> str :
171+ """
172+ Helper method to represent an ASS Identifier as a string to be used as Local file document id
173+ """
174+ return hashlib .sha256 (identifier .encode ("utf-8" )).hexdigest ()
0 commit comments