Simple Object Archive for Python - Providing a single decorator to persist object data and relations
Posted by Samnono@reddit | Python | View on Reddit | 10 comments
Simple Object Archive for Python (SOAP)
What my project does
This library provides a single @entity
decorator for object persistence. Decorated classes will store their instances under ./__data__/
For each class variable that is annotated, a property
will be provided with the same name.
Class variables whose annotation is also a decorated object, set or list thereof are stored as a string of their UUID and will be resolved when their get()
method is first called.
Target audience
People...
- wanting to quickly prototype a database;
- creating simple applications with not that many objects (<10.000 or so)
Comparison
SQLAlchemy
SOAP doens't require database setup, but isn't as extensive.
Pickle
Pickled objects aren't transparent or queriable.
Dataclass
SOAP was inspired by the u/entity
decorator, adding query and persistence functionality.
Example
@entity
class MyClassA:
name: str
health: int = 100
my_path: Path = None
inventory: set['MyClassB'] = set() # One-to-many
This creates an __init__
-function with the default arguments of the class variables.
@entity
class MyClassB:
daddy: MyClassA # One-to-one relation
other_items: list
timestamp: datetime
problems: random.randint(0, 99)
MyClassA
and MyClassB
now reference each other. We create the objects like we would any other, just keep in mind to use all keyword arguments.
a1 = MyClassA(name="Benjamin")
a2 = MyClassA(name="Steve")
b1 = MyClassB(daddy=a1,
timestamp=datetime.now(),
other_items=['Some cheese', 'Bud light'])
b2 = MyClassB(daddy=a2,
timestamp=b1.timestamp,
other_items=[b1])
Because MyClassA.inventory
is annotated with set['MyClassB']
, the getattr
function returns a EntitySet
type. This is basically a set
with filter()
and exlude()
methods to perform queries. Additionally, operations like append
and remove
are wrapped to save the object afterwards.
a1.inventory.append(b1)
a2.inventory.append(b2)
steve_not_my_daddy = MyClassB.exclude(daddy=lambda x: x.name.startswith('Steve'))
cheese_i_have = a1.inventory.filter(other_items=lambda x: "Some cheese" in x)
print(steve_not_my_daddy) # {b1}
print(cheese_i_have) # {b1}
print(type(steve_not_my_daddy)) # <class 'src.entity.entity.<locals>.Entity'>
print(type(a1.inventory)) # <class 'src.entity.entity.<locals>.Entity'>
Limitations
- All objects are kept in memory.
- When an object is deleted, it is not directly removed from memory because other objects may still have a reference to it.
- Currently, only
datetime
andPath
objects are transcoded besides the builtins.
Next steps
- Explicit archiving, adding items to a (
.zip
) archive (to partially address limitation #1); - Option to disable implicit saving;
- Combine with a
rollback
function to facilitate transactions; - Custom transcoders (to address limitation #2);
- Typechecking for getters and setters;
- Derive date created from file metadata;
- Custom assignment of data folder;
- Allow creaton/modification/deletion of objects from files using watchdog to monitor the data directory for changes;
- This may allow this framework to function as a synchronized database when combined with something like
portalocker
; - CSV file writing of all objects;
- Optional integrations:
- NiceGUI to have some kind of admin page;
- Saving asynchronously;
- Use a profiler to identify bottlenecks;
- Find a more fitting name.
Issues
- Deleting an object and then terminating the program may cause invalid refereces, which in turn may cause errors.
- Invalid files cause errors.
yesvee@reddit
Have you looked at ZODB?
Samnono@reddit (OP)
No, but I'll take a closer look because it does look similar. The only thing I noticed right away is that it requires more configuration and package-specific instructions to set up. I tried leveraging the Python native class definition to derive the entity attributes and methods.
LibreCobra@reddit
It does not. You can persist any python object in zodb. Be careful with updates of the class definitions.
Samnono@reddit (OP)
You need to specify where to store the data like so:
Also, I see that the database is not as transparant as when saving to json.
I know they are minor things, but they are what bothered me with other methods.
Morazma@reddit
This is cool. I'm not sure I have a use-case but I think it's a neat idea and I bet implementation was fun. Thanks for sharing!
Adrewmc@reddit
I don’t see the purpose of this, creating a db is not that hard and has significant advantages. Creating classes from those queries is fairly simple.
But generally I’m scratching my head on whenever I would want to permanently save a whole class object, or reference of it, that I couldn’t do with a simple dictionary/JSON of its attributes…then makes a from_dict/json() class method. That doesn’t make more sense as your run of the mill database, or even CSV file.
I think you need to explain the problem you are solving here.
Samnono@reddit (OP)
Thanks. I agree that the use cases are probably limited. What you are describing in the second part of your comment is basically what is automated with this package, with the addition of maintaining relations between the objects. What I'm trying to accomplish is inferring the setup of a relational database system by solely using regular Python syntax.
This is also why I am asking for feedback on how to make it more useful.
InvaderToast348@reddit
Interesting, I'll have a play tomorrow if I get some time.
Samnono@reddit (OP)
Thanks! Let me know what you think
Samnono@reddit (OP)
Feedback on how to improve the quality and usefulness is appreciated!