From 5c8153ed19d04921f6a0df1d24eca00308ffbcdc Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Tue, 17 Feb 2026 13:54:02 -0500 Subject: [PATCH] Changes to be committed: deleted: .create_tables.sql.swp new file: ca_core/__init__.py new file: ca_core/__pycache__/__init__.cpython-313.pyc new file: ca_core/__pycache__/entity.cpython-313.pyc new file: ca_core/__pycache__/group_member.cpython-313.pyc new file: ca_core/__pycache__/metadata.cpython-313.pyc new file: ca_core/__pycache__/property.cpython-313.pyc new file: ca_core/entity.py new file: ca_core/group_member.py new file: ca_core/metadata.py new file: ca_core/property.py modified: create_tables.sql deleted: tests/.create_testdata.sql.swp new file: tests/__pycache__/test_entity.cpython-313.pyc new file: tests/__pycache__/test_group.cpython-313.pyc new file: tests/__pycache__/test_metadata.cpython-313.pyc new file: tests/__pycache__/test_property.cpython-313.pyc new file: tests/test_entity.py new file: tests/test_group.py new file: tests/test_metadata.py new file: tests/test_property.py --- .create_tables.sql.swp | Bin 12288 -> 0 bytes ca_core/__init__.py | 0 ca_core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 131 bytes ca_core/__pycache__/entity.cpython-313.pyc | Bin 0 -> 6667 bytes .../__pycache__/group_member.cpython-313.pyc | Bin 0 -> 1372 bytes ca_core/__pycache__/metadata.cpython-313.pyc | Bin 0 -> 2009 bytes ca_core/__pycache__/property.cpython-313.pyc | Bin 0 -> 1506 bytes ca_core/entity.py | 170 ++++++++++++++++++ ca_core/group_member.py | 28 +++ ca_core/metadata.py | 35 ++++ ca_core/property.py | 33 ++++ create_tables.sql | 23 ++- tests/.create_testdata.sql.swp | Bin 12288 -> 0 bytes tests/__pycache__/test_entity.cpython-313.pyc | Bin 0 -> 7338 bytes tests/__pycache__/test_group.cpython-313.pyc | Bin 0 -> 6557 bytes .../__pycache__/test_metadata.cpython-313.pyc | Bin 0 -> 4907 bytes .../__pycache__/test_property.cpython-313.pyc | Bin 0 -> 5763 bytes tests/test_entity.py | 97 ++++++++++ tests/test_group.py | 89 +++++++++ tests/test_metadata.py | 76 ++++++++ tests/test_property.py | 90 ++++++++++ 21 files changed, 636 insertions(+), 5 deletions(-) delete mode 100644 .create_tables.sql.swp create mode 100644 ca_core/__init__.py create mode 100644 ca_core/__pycache__/__init__.cpython-313.pyc create mode 100644 ca_core/__pycache__/entity.cpython-313.pyc create mode 100644 ca_core/__pycache__/group_member.cpython-313.pyc create mode 100644 ca_core/__pycache__/metadata.cpython-313.pyc create mode 100644 ca_core/__pycache__/property.cpython-313.pyc create mode 100644 ca_core/entity.py create mode 100644 ca_core/group_member.py create mode 100644 ca_core/metadata.py create mode 100644 ca_core/property.py delete mode 100644 tests/.create_testdata.sql.swp create mode 100644 tests/__pycache__/test_entity.cpython-313.pyc create mode 100644 tests/__pycache__/test_group.cpython-313.pyc create mode 100644 tests/__pycache__/test_metadata.cpython-313.pyc create mode 100644 tests/__pycache__/test_property.cpython-313.pyc create mode 100644 tests/test_entity.py create mode 100644 tests/test_group.py create mode 100755 tests/test_metadata.py create mode 100644 tests/test_property.py diff --git a/.create_tables.sql.swp b/.create_tables.sql.swp deleted file mode 100644 index 113b6c445463a5fd77b0a839a94d154a948ea25e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2J#W)M7{{M>V@N@;R|iERkF~w?LO5zlS#W=~ewen(fc3QH41m6QB z82JJq0RqGqU}9$l*3LY(lPC@nMe0=Pf6`Bv+{^Pk_q!>|wQ{>gS=?I76SoUQZ*D$w zex6&TFRzG(zEt7xN$tp0-ILaY^H0Kn>t5GcZ!2jB(hBTWPx@>Aqu&2)#~K)bfm0bs z(Z=|6801UtY z48Q;kzyJ)u01UtY4E${%Qy?LPr(92}U}O#Kz7)G!?e<-*md@vLnQ+$W2%}<(N|kAp zN?D5EUyJDd^>lBVsKa6AMhir7ik+HK)@n_$tv5w_U%5liaynVg>bnE}6eUt_Z#H`- z8`U;h5*87SOm@cuiDKfZzU;T8N)|Lb6Bk8cbotnGlpQ$kKom7oj}i`BJ*RDT XX_!qhb3bzzEC+!TjFL9vu>$=81q#;a diff --git a/ca_core/__init__.py b/ca_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ca_core/__pycache__/__init__.cpython-313.pyc b/ca_core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bc6456ce7fffc9669d3440af4c97a277fcb53a2 GIT binary patch literal 131 zcmey&%ge<81QiWaGC}lX5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i7enx(7s(xv4 zYLR|HcBX!EVtjIbQL27?d}dx|NqoFsLFFwDo80`A(wtPgB37Ulkdeh8#z$sGM#ds$ GAPWG!ZyZnn literal 0 HcmV?d00001 diff --git a/ca_core/__pycache__/entity.cpython-313.pyc b/ca_core/__pycache__/entity.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae62474f16d8d7dac3505b88121ca4d323aecc9c GIT binary patch literal 6667 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 literal 0 HcmV?d00001 diff --git a/ca_core/__pycache__/group_member.cpython-313.pyc b/ca_core/__pycache__/group_member.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d44604235ea3d9aa35a75f0f1f2aab99fd830f8 GIT binary patch literal 1372 zcmc(f%}*0S6u@Ucx@>`}fFIF7G6`Z>)7XRMh7ux+#o&^3TlGNF;C3k0mThKQqn`96 zdhsHOM~`sx=+Qrc#7nat{Rf&9?!GtEEd_%fouu!*nfEg9z2CfTHBbNBzX;tQ_I($j2DfpF{djBB^Bc6 z-s{|o=ezj=UGWNQROPF)`cuZ~Y@9t!``ObJvpe+B;%d?L>FkV1Q}uK9wP=XFWI?OR z95b?l78J}mX5&~K$BG3C3M;yi*$V3{(-Mql8qcelV51a-oMi?pxYc+SUDzm>OM{0T zp8+ovS3$fXZFBgY_ttCM`F3*bbNRrYK1vt~s|!vNU|hTsU`R?g!T1}ctbUr&!jik{ z7F@cVUwe3x(c_$(cY8TQJPA6Q;H)0LU?+P` zfd{?f3W$MTSD@DedrEf7N5B{KUKLD25VGu4jpJGKP}H*8aiJ8x0B;LqlftF9h;1fe_VLK@MC21z?}RKX!gH8NxsrkURVjM`~1dfY{ zLw%E?6mBXqd;-EQW9XkW#&$-|q~SSMWK_4Cks+E~eoGvOOv06S?h{E(4W&I_56i6@ zyUTF^@n1ze0MRitP3u^uW_=MI0_v+$-=`08*BJ$|H!VHi;-iE%wdWraP@QvV-#$WH G=K2k_&mx!r literal 0 HcmV?d00001 diff --git a/ca_core/__pycache__/metadata.cpython-313.pyc b/ca_core/__pycache__/metadata.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cf6b62fcb368a93aeff2ad0a504da087b89d00a GIT binary patch literal 2009 zcmd5-%}*0i5PxrX`@vQ~1&snV53GSA(5RqMfk>c6B2>fD#2YTl69}|R-fj_yCU_D~ zUgXXZZXEqHN-PQ4gBSk+(#E?p+g&Nu>cvD~va|DM=6%fXH#=LZuU82SW$fc(w?fEQ zocM!!N~sT&9iovIB8WyoQcVIWX);JzQ$Q+O0AxT5f(*`w;$g#Ijho2cQ+f%?4oQ=# zN-x-OXmr4{GK|1ARb#-LXK9*tdAc4TBn|h07m2|gVHhPSzIJmmKGC+K4X}IC;XlJR@R;HJhx|XV-bNxn2yy0Vv`&z>ig8YRKDqMLAiIV zp8t^8PJC8dH%Gs;M7Qp2t!_WeE1kRjAW9HV3_Q~xz>AAusT-8q=%r~t9q`sMGEd@C zY9pF{J~lF)Jk4H9dY}xk8`eG*2;06S!tinetNL<|d+7?S3ny&(`CNc$5FV=uL`6Qg z57Gxuj+7yB4($Jy^q5DwVP-NsYuDgCNxiR;_Ld8K=I4n!G^zeFPY6^VY5$fIb$R2qLu1q8)PW9qqzf{qwq`17%qiWo?*sN(UurhB<&(1L~9|pBxG24W@FL0r5 zJXM)Dsy>Y$cIp70MX(`r`)e6F1tNYT!Gt*R)hodfI}+!AYMXT1GDRo zg0e|Y!XyyE&VEAV=I}|7L@wp)I=+SEK+J_F6%V<;0)f>hu%8G7TVO#7gx=k7pSZ!^ yS%VpKF1O5w1p~kP4~c_vB2!9>l1#gbDy754piFO~HbPPDE2=V0l++L{pZguF+<~3| literal 0 HcmV?d00001 diff --git a/ca_core/__pycache__/property.cpython-313.pyc b/ca_core/__pycache__/property.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..872b54a171a6c6f299808780c9ce42dca681d331 GIT binary patch literal 1506 zcma)+&u`O66vxLu(mD=JrM40*u+q>a5=J38&+0-5LmGKB}ioNS6`(Ic6PrgRojiTme7Qx1J{Go_6M zY0~jse|z0^f{9J)Z4wHTj@2eM?2%XsMtmOSUqv+`i6sM5xHmN?&Wl3_Mom$Zdb+yc zI1F1jPL3NM#r_7t!;K5L(!j2dtrjJgvyI7?%Y2r{jz<{oc>adlX%^Jpj*26z7^YsU zL$LFgRD6=@>dPKST%00R2J*_gzxcE6&uz1a>ck6 zrZO}k7$;lA?)qd;3dZa%Wgeycwv;~&;|-iKm@RI2ZBpzqLW`S^-J)%++a4vw_yG!= z+rcSD{PoyD)9@vKPxd1;&(J_Ysmya@#~2`aMuAyorl0=lC@rUQ2e1I0Oy(g(UZC$I z1FIrRJYQ_4`13u4tPDc;Iq%R+X+9!Ngr=;edaG)Uw7n&`5Ajt zccQxG^#bM9?Kp-eQGSO&q26ycc>F*e9fyuVDCR{KzHZVXE#i|AlS%NxOCWX~8epPE zOx`Lt-lnJe%IslEN{s;=CHws8&eOjx?2n&&mp#+};mUrt&{qlr2`T3(SFHWzQzt^& zg2=aVtqOC(NJxN-7uQ!dAI+!Hn#jZ=V&XJf9!JQVHATM79q~34Rt8~+bi|9$4o3|H zvb+u${VwfrWwt 0: + raise ValueError("Creator cannot be deleted because it has created other entities") + cursor.execute("DELETE FROM entity WHERE id=%s AND creator IS NULL", (creator_id,)) + if cursor.rowcount == 0: + raise ValueError("Creator not found or already deleted") + + +# ------------------------ +# Getters +# ------------------------ +def get_entity(cursor, entity_id): + cursor.execute("SELECT * FROM entity WHERE id=%s", (entity_id,)) + row = cursor.fetchone() + if not row: + raise ValueError("Entity not found") + return row + + +def get_entity_id(cursor, name): + cursor.execute("SELECT id FROM entity WHERE name=%s", (name,)) + row = cursor.fetchone() + if not row: + raise ValueError("Entity not found") + return row["id"] + + +def get_entity_public_key(cursor, entity_id): + cursor.execute("SELECT public_key FROM entity WHERE id=%s", (entity_id,)) + row = cursor.fetchone() + if not row: + raise ValueError("Entity not found") + return row["public_key"] + + +def get_entity_name(cursor, entity_id): + cursor.execute("SELECT name FROM entity WHERE id=%s", (entity_id,)) + row = cursor.fetchone() + if not row: + raise ValueError("Entity not found") + return row["name"] + + +# ------------------------ +# Setters +# ------------------------ +def set_entity_name(cursor, entity_id, new_name, requesting_creator_id): + _verify_ownership(cursor, entity_id, requesting_creator_id) + cursor.execute("UPDATE entity SET name=%s WHERE id=%s", (new_name, entity_id)) + + +def set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id): + _verify_ownership(cursor, entity_id, requesting_creator_id) + cursor.execute("UPDATE entity SET public_key=%s WHERE id=%s", (public_key, entity_id)) + + +def set_entity_keys(cursor, entity_id, public_key, requesting_creator_id): + # only public_key for current schema + set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id) + diff --git a/ca_core/group_member.py b/ca_core/group_member.py new file mode 100644 index 0000000..36216db --- /dev/null +++ b/ca_core/group_member.py @@ -0,0 +1,28 @@ +# ca_core/group_member.py + +def add_group_member(cursor, group_id: int, person_id: int, role: str): + cursor.execute( + "INSERT INTO group_member (group_id, person_id, role) VALUES (%s, %s, %s)", + (group_id, person_id, role) + ) + +def remove_group_member(cursor, group_id: int, person_id: int): + cursor.execute( + "DELETE FROM group_member WHERE group_id = %s AND person_id = %s", + (group_id, person_id) + ) + +def get_groups_for_person(cursor, person_id: int): + cursor.execute( + "SELECT group_id, role FROM group_member WHERE person_id = %s", + (person_id,) + ) + return cursor.fetchall() + +def get_members_of_group(cursor, group_id: int): + cursor.execute( + "SELECT person_id, role FROM group_member WHERE group_id = %s", + (group_id,) + ) + return cursor.fetchall() + diff --git a/ca_core/metadata.py b/ca_core/metadata.py new file mode 100644 index 0000000..dc1cdb3 --- /dev/null +++ b/ca_core/metadata.py @@ -0,0 +1,35 @@ +def get_name(cursor): + cursor.execute("SELECT name FROM metadata LIMIT 1") + row = cursor.fetchone() + return row['name'] if row else None + +def set_name(cursor, value): + cursor.execute("UPDATE metadata SET name = %s", (value,)) + +def get_comment(cursor): + cursor.execute("SELECT comment FROM metadata LIMIT 1") + row = cursor.fetchone() + return row['comment'] if row else None + +def set_comment(cursor, value): + cursor.execute("UPDATE metadata SET comment = %s", (value,)) + +def get_public_key(cursor): + cursor.execute("SELECT public_key FROM metadata LIMIT 1") + row = cursor.fetchone() + return row['public_key'] if row else None + +def get_private_key(cursor): + cursor.execute("SELECT private_key FROM metadata LIMIT 1") + row = cursor.fetchone() + return row['private_key'] if row else None + +def set_keys(cursor, public_key, private_key): + """ + Sets both public and private keys together + """ + cursor.execute(""" + UPDATE metadata + SET public_key = %s, private_key = %s + """, (public_key, private_key)) + diff --git a/ca_core/property.py b/ca_core/property.py new file mode 100644 index 0000000..ff30d4d --- /dev/null +++ b/ca_core/property.py @@ -0,0 +1,33 @@ +def set_property(cursor, entity_id: int, property_name: str): + """ + Adds a property for the entity. If it already exists, does nothing. + """ + cursor.execute(""" + INSERT INTO property (id, property_name) + VALUES (%s, %s) + ON CONFLICT (id, property_name) DO NOTHING + """, (entity_id, property_name)) + +def delete_property(cursor, entity_id, property_name): + """ + Remove a property from an entity. + Raises ValueError if the property does not exist. + """ + cursor.execute( + "DELETE FROM property WHERE id=%s AND property_name=%s", + (entity_id, property_name) + ) + if cursor.rowcount == 0: + raise ValueError("Property not found") + +def get_properties(cursor, entity_id: int): + """ + Returns a list of property names for the given entity. + """ + cursor.execute(""" + SELECT property_name + FROM property + WHERE id = %s + """, (entity_id,)) + return [row['property_name'] for row in cursor.fetchall()] + diff --git a/create_tables.sql b/create_tables.sql index 6d6cf5d..581ccf4 100644 --- a/create_tables.sql +++ b/create_tables.sql @@ -1,8 +1,20 @@ -drop table entity; +drop table metadata; + +create table metadata( + name varchar(50), + comment varchar(200), + private_key varchar(500), + public_key varchar(500) +); + +insert into metadata default vALUES; + +drop table entity cascade; create table entity( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), + creator INT REFERENCES entity(id), name varchar(100) NOT NULL, group_p BOOLEAN NOT NULL, geo_offset BIGINT, @@ -16,8 +28,8 @@ create table entity( drop table group_member; create table group_member( - group_id INT, - person_id INT, + group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + person_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, role VARCHAR(10), PRIMARY KEY (group_id,person_id) ); @@ -25,6 +37,7 @@ create table group_member( drop table property; create table property( - id INT NOT NULL, - property_name VARCHAR(100) + id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + property_name VARCHAR(100), + PRIMARY KEY (id, property_name) ); diff --git a/tests/.create_testdata.sql.swp b/tests/.create_testdata.sql.swp deleted file mode 100644 index 527fd5f1b17ce81fe6e4ef7a9219bcefb35fcb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&KX21O7zXgS-57upzJP(ju5>hw|Ae4SfvG|)AXu$Se2J_$XLEOM6^2&gQy?)S zv9XkyZ@>r}8xwpA-c!;Fm7>zI(qrk7b9wJB@BOyO9`C-~dm234=@DOBM1#Sb^4GOZ z`uv`#waWOfFS-tDGrwq#uOIpiyUo1pI%S>hPK=UH<$h2|CwJ_t>VLMu90)+*vI4D} z-XC12B<}6{rT6aM32t{@Ty_`ChX4d1009U<00Izz00geEfNM7Bi2L7MpP{q9Z=AWW zhqyri0uX=z1Rwwb2tWV=5P$##AaDf*WI;5%LG+H}pZ}ly|NnWD=nLl)=a}=FV>!<_ z38%xk#o6Ng;I-d5-#A}6A2}a5e$TfYTo8Z&1Rwwb2tWV=5P$##AOL}X3Pf=fi%?1# zZVzc+R;{vBYJP#-1*LX%(3ra6!7?ffaq>CWa-p_~qD1u0Uo|sz+vE$i7%LO427bLV zw$^!B@LGz(C$*j_6AEWqC6YAjiy$04NR#;S$T!0ibX$aCDl4l*nx@=m($CWVh|7Y@ z&-ax+t;b20aXlM7;*G+9C!BK_rCsTkLFD@vtF{_3Q*~ZXr&c-9wBxFrDimP87(r<3ApL+lR diff --git a/tests/__pycache__/test_entity.cpython-313.pyc b/tests/__pycache__/test_entity.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c90fc836aff3da2da5f2672fdf75f649d5ac1c7 GIT binary patch literal 7338 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_group.cpython-313.pyc b/tests/__pycache__/test_group.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7a2e3c0274b61b37d148a49cce39965d4372bde GIT binary patch literal 6557 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 R_ltACJpaVaa1q1)zX0!Aiv0ip literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_metadata.cpython-313.pyc b/tests/__pycache__/test_metadata.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f12cae2b159011c1eba2a2fa221781cb7daf35f6 GIT binary patch literal 4907 zcmdT|-ER}w6~E*0$Mr|-1Qyi}*>pC5)}|#1kg!0vVNK$Ykl?I54vV^xMiXa}SU9$O zXS_fjDz;J!5i14SB3P-?(w8ccR)T$6{tWV^M3+`V;%VQ)hQ9ZlvB$Av6CzX|dLNGO zJ@?#?bI$Lad&XN04ITnbz4Nzp)J@1s99Yd+>TER%gxn`Gk%b|`ioXIAU~C(*T@;xp z6?t}M9~0Zin4=ryu_b)P-%47KCyU3tgfQ2bkZ7!wE%p=pYk4vm^T0_i>!fa5QKobg zFZ0TF<_nTGB0HLi>>MMB=GtAl#UhXS#{#nJ>=m%XK0-iNs+Dac-6f_(ri3XWLVv<> zJR)%W=c+NyT`5&b<(S58DK%BOzH~LR1^a^;#|WB^*<^uh>ssgYPxYrGt>#yQ2Ptnl#$M5^`HE3YO3TA z^ICSGsQA`1)2&~cOe6{GrezL60}5Z_3bXmiNt6 zW&*EzzAiXyX9PIQWl6$WsM3y>XxhPmp)6W=mGFjpU1fjksycwQz>yV31=-=i)14-u z4w}~4>)B;Mn>Vg!(*w17T4w_=Nrc8{T@&M3bw>LwK-0@|*OW;$W#rh6*_R%vqV* zl!Z=o)|1M!^jE5(DOa@{Wy$XPl7;b1I#nZBb#A0QnmsJXhGV@+sdr>Fo(vz2NKsiz z!FBo*BbUmGaS>Py;XZq4FfPXuNog>i94XIBeX;)N=x`FQHar@WBVO*x>o-!l{1k+F zE}PX-26y(Ik4G=XxE&eXnJO?n$G9B~u5&t(P8kYx;h+NJF73LODi|84lbVs5&Sf=_ z2Q#IOhz%q%Iu~&n-ba%7k&fxyjMh=mHP(^8n(iIn7=T0VSZ$8Zm=RQUU+!AA=#4}5y-_7}1h@DQ$W8JB^hc{U%r$%yu#DlK^RLk$ zYXOO>q%0JPI4?tw2z1-@JRyy-u5#X!9y<+4$w*+jiuv(Ux8GJ`IcCP46kf z@Z^ZiREr&gJ~Q1=;Yn&aiMQ6*!kX`Z)jAXlI~A#Wd3+8`1pgB{_esSYST;FKlv*1i zybfArVY|oyiSm1Bts|BD(rvEQd(*zwYgf6y2>|Yj=JRpL_YnuDR_5Up{HWk7Pz($& zq#unLvELWe45w4p+)le5Fd&I3xKzzfD3%!i)tii5N4N_V(J`cNl7G?0bsAcxp@)aR z3x6AaG`LFJH>hu&2A62?@4@xZ(dE$5g|5e^7f-K*x}K!h=x4Qv`u}vwtPf2ll^)rM zgSCT2xEC#3;+cM7ohBO%ZLxQ{Bv|8;k|b~6qgP7uXLdNIWtp}-On!Ie+bfU$uu4x= zIp)}M=-9%<sls!!h(Ewarj@6B53y~fL1_J z9H%JUgK)SEq&7~6p0upeo+?0eE{8gweDGuNhu})+?9-uDI#4xzd^vRd$>ATvKZI98 zzj->gMlbGym*M{zUfwZ7i?aPaV@}-0#CyS^yc)_qVap! z7|39Kre6oLDGGw{oCKZ|&%a6VIq|=8l7^rbz6~rm6l5U3;b3 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2efb10d3f7b774c830a1475bc2544bf6351e3a97 GIT binary patch literal 5763 zcmdT|Uu+Y}8K1Rx?Xj~?!X-fB09i;l;*dW*4k`i%I_FQ1gNwO(lPeY1+dB3pXW{Jn zX4kz-t~#A6wN0v4&}-=hsS@F(kSBsviBq52hxVByPEF@Ps?<{@-cUkSsZaf8*K0e@ z8p>5VwG-vr+4*L^8PEK_-}miohC)FErR(}H(|@l+=x^-BDMGQb`3Y36A{oitIYjIy zM>u%*ob#OZ60gMCyyy6{0uih_4|N~$OQ;1MK(Y`MDE`lGi_gI9ZIF(MC3en$(x=+>2v^m3CvqaCT#GFYw zCfJiJzHAOcI&h^L-W@itKYiUz5q1})sbs{+{ktTCm(oE}`(U)`*Im#V=h{-VC}MZfP-1s3atV* zM5kVU#v96YrzZ2xDkV1ae>zGikGfcbOdMcQT@KW+D*I%=11wm1>6cVkbA^}t8f7d8 zK+{}3J3_~bdku4xep|&H;q4=8t~e;yJ#&tF$KDi*h_Tr~YC=m)tEZBQZvfWo0Q|~i zB5CSmW>y-DkH?0hVzt zQtzNX;k*q}UoVldQzXBPMg%1%n?H; zgkh0-;f4Cu57cDNR0-QgS`P-nSCu4D6YwKwuvk&jDH>8WqGvJ+IJ2Q^G+;lOMzngV z$`sLaS&FR(v@da#H`L6e!MsGG2m9aEr`7%(Owgabl?BcIfv5YvEC_(Ez%zRA}v8ZSB1;HeNn6f98t1@aO|K^$giiHOf~d8< z&~kXS<*?J~-9=xiQ!8}(S!fe}2--qFj6y8>S%k_}=2q~mF7XnLlr?2WhA}SG6K8B# zRkn<&(y`a4j{)#=u?SC^peMx)788l&CBht<3dxKPXS@&U_Q?~;>Sd}4GOWL^(Vbh4 z)@f#)j6M(_BX%ZmmIWMM!C`iy_8DESpRdp3mZJ7@!+gU!ZY3QsWXm?PfN5>xp8$8y zW^wl|gVqw)g1uq^#xHKaR2dzxqc1-#j&V5(ce}@m(L)1{k3#}bU1V;YbNXzVxfFHE zBc5;OujChn%Byo~e>UuL0*gXlyPtcC0pbK--xcXKtl#YfIW% z%DdS(o!h)1p$(t81;THosIZO4@^jUnPB{9Bd{T*W>ixxgOf3vgT9y! zP$Wm7WeeJxEwoh)r2Dr6uBr>8a^t%oYGG~!f|`ZKJ*#-n!r1i-*DfrYH$S@Z(K4Xy z)Kk&uAZbxp$1iRNP$^^n9ZYY9Easn8Ke#uG-);H;qP&=%E=}vju(Im$Xvrf}B^@FrwLKYVBQPC!fcp_~8 zfdt=5WOC{VA&@&+$9)`Zm5+N{u(Liol;Sy67%t&~_SQUBx9%rlI^*jgAUKA06hiw} zLi>I_yB_L(AT}06X;qXKn{T$JJw}-G2^s|I1)8Pr9VAIGj6C)rZJ~sv;7U4A7C>~;Q&Wd^4S!a*MI$Hy# zGM&qq>1;-&k?k<8(RC1(Vtfdq77JOCuXaqVsrlk*(XxQ|uHd~3>gvH)*Ku?^L6wrh z$N!t)c7aw6aJ$gnmjz+P#m4hlby;l*+U(lnnQ;^PxLr|rD%0B1`_7$r)^-oii4S%eq#yCSzbZ+9(qEf3!vy)(MDYjCdauf+^B zweUtBM<5?9W&q6#-_PTovi4jacU5XXT*rqk_lpSDS0QPIW!n+b+GLiO$08w$6@^`x zC<+ZK%Cw%!WmsKQ6qb<}H4Ta~nI?vrNo%U6gW9hsDLttuWIr={h>4?2oM7TrCSGGA z27>NX6vIrI>74OU) z!@h0tClg9iC-y}PVZ3A^l46F7p#*%NSj&2o4d