nsarchive 3.0.0a1__tar.gz → 3.0.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nsarchive
3
- Version: 3.0.0a1
3
+ Version: 3.0.0a2
4
4
  Summary: API-wrapper pour récupérer des données liées à Nation
5
5
  License: GPL-3.0
6
6
  Author: happex
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: pillow (>=10.4,<11.0)
15
- Requires-Dist: supabase (>=2.9.1,<3.0.0)
16
15
  Description-Content-Type: text/markdown
17
16
 
18
17
  # NSArchive
@@ -1,27 +1,24 @@
1
1
  """
2
2
  nsarchive - API-wrapper pour récupérer des données liées à Nation.
3
3
 
4
- Version: 2.0.0
4
+ Version: 3.0.0a2
5
5
  License: GPL-3.0
6
6
  Auteur : happex <110610727+okayhappex@users.noreply.github.com>
7
7
 
8
8
  Dependencies:
9
9
  - Python ^3.10
10
- - supabase ^2.9.1
11
10
  - pillow ^10.4
12
11
 
13
12
  Le fichier README.md fournit des détails supplémentaires pour l'utilisation.
14
13
  """
15
14
 
16
- # Import des types et des exceptions
15
+ # Import des types
17
16
  from .cls.base import NSID
18
17
  from .cls.archives import *
19
18
  from .cls.entities import *
20
19
  from .cls.republic import *
21
20
  from .cls.economy import *
22
21
 
23
- from .cls.exceptions import *
24
-
25
22
  # Import des instances
26
23
  from .instances._economy import EconomyInstance
27
24
  from .instances._entities import EntityInstance
@@ -0,0 +1,237 @@
1
+ import io
2
+ import json
3
+ import requests
4
+ import typing
5
+ import warnings
6
+
7
+ class NSID(str):
8
+ """
9
+ Nation Server ID
10
+
11
+ ID unique et universel pour l'ensemble des entités et évènements. Il prend les `int`, les `str` et les autres instances `NSID` pour les convertir en un identifiant hexadécimal.
12
+ """
13
+ unknown = "0"
14
+ admin = "1"
15
+ gov = "2"
16
+ court = "3"
17
+ assembly = "4"
18
+ office = "5"
19
+ hexabank = "6"
20
+ archives = "7"
21
+
22
+ maintenance_com = "101"
23
+ audiovisual_dept = "102"
24
+ interior_dept = "103"
25
+ justice_dept = "104"
26
+ egalitary_com = "105"
27
+ antifraud_dept = "106"
28
+
29
+ def __new__(cls, value):
30
+ if type(value) == int:
31
+ value = hex(value)
32
+ elif type(value) in (str, NSID):
33
+ value = hex(int(value, 16))
34
+ else:
35
+ raise TypeError(f"<{value}> is not NSID serializable")
36
+
37
+ if value.startswith("0x"):
38
+ value = value[2:]
39
+
40
+ instance = super(NSID, cls).__new__(cls, value.upper())
41
+ return instance
42
+
43
+ class Instance:
44
+ """
45
+ Instance qui servira de base à toutes les instances.
46
+ """
47
+
48
+ def __init__(self, url: str, token: str = None):
49
+ self.url = url
50
+ self.token = token
51
+
52
+ self.default_headers = {
53
+ "Authorization": f"Bearer {self.token}",
54
+ "Content-Type": "application/json",
55
+ }
56
+
57
+ def request_token(self, username: str, password: str) -> str | None:
58
+ res = requests.post(f"{self.url}/auth/login", json = {
59
+ "username": username,
60
+ "password": password
61
+ })
62
+
63
+ if res.status_code == 200:
64
+ return res.json()["token"]
65
+ elif res.status_code in (401, 403):
66
+ raise PermissionError(res.json()['message'])
67
+ else:
68
+ raise Exception(f"Error {res.status_code}: {res.json()['message']}")
69
+
70
+ def _get_item(self, endpoint: str, body: dict = None, headers: dict = None) -> dict:
71
+ """
72
+ Récupère des données JSON depuis l'API
73
+
74
+ ## Paramètres
75
+ endpoint: `str`:
76
+ Endpoint de l'URL
77
+ headers: `dict` (optional)
78
+ Headers à envoyer
79
+ body: `dict` (optional)
80
+ Données à envoyer
81
+
82
+ ## Renvoie
83
+ - `list` de tous les élements correspondants
84
+ - `None` si aucune donnée n'est trouvée
85
+ """
86
+
87
+ if not headers:
88
+ headers = self.default_headers
89
+
90
+ res = requests.get(f"{self.url}/{endpoint}", headers = headers, json = body, timeout = 5)
91
+
92
+ if 200 <= res.status_code < 300:
93
+ return res.json()
94
+ elif res.status_code == 404:
95
+ return
96
+ elif res.status_code in (403, 401):
97
+ raise PermissionError(res.json()['message'])
98
+ else:
99
+ raise Exception(f"Error {res.status_code}: {res.json()['message']}")
100
+
101
+ def _get_by_ID(self, _class: str, id: NSID) -> dict:
102
+ _data = self._get_item(f"/model/{_class}/{id}")
103
+
104
+ return _data
105
+
106
+ def _put_in_db(self, endpoint: str, body: dict, headers: dict = None, use_PUT: bool = False) -> None:
107
+ """
108
+ Publie des données JSON dans une table nation-db.
109
+
110
+ ## Paramètres
111
+ endpoint: `str`
112
+ Endpoint de l'URL
113
+ body: `dict`
114
+ Données à envoyer
115
+ headers: `dict` (optionnel)
116
+ Headers à envoyer
117
+ """
118
+
119
+ if not headers:
120
+ headers = headers
121
+
122
+ if use_PUT:
123
+ res = requests.put(f"{self.url}/{endpoint}", headers = headers, json = body)
124
+ else:
125
+ res = requests.post(f"{self.url}/{endpoint}", headers = headers, json = body)
126
+
127
+ if 200 <= res.status_code < 300:
128
+ return res.json()
129
+ else:
130
+ print(res.text)
131
+ res.raise_for_status()
132
+
133
+ def _delete(self, _class: str, ids: list[NSID]) -> None:
134
+ """
135
+ Supprime des données JSON dans une table nation-db.
136
+
137
+ ## Paramètres
138
+ _class: `str`
139
+ Classe des entités à supprimer
140
+ ids: `list[NSID]`
141
+ ID des entités à supprimer
142
+ """
143
+
144
+ res = requests.post(f"{self.url}/delete_{_class}", json = { "ids": ids })
145
+
146
+ if 200 <= res.status_code < 300:
147
+ return res.json()
148
+ elif res.status_code in (403, 401):
149
+ raise PermissionError(res.json()['message'])
150
+ else:
151
+ raise Exception(f"Error {res.status_code}: {res.json()['message']}")
152
+
153
+ def _delete_by_ID(self, _class: str, id: NSID):
154
+ warnings.showwarning("Method '_delete_by_id' is deprecated. Use '_delete' instead.")
155
+ self._delete(_class, id)
156
+
157
+ def fetch(self, _class: str, **query: typing.Any) -> list:
158
+ res = requests.get(f"{self.url}/fetch/{_class}", params = query)
159
+
160
+ if res.status_code == 200:
161
+ matches = res.json()
162
+ elif res.status_code in (401, 403):
163
+ matches = []
164
+ else:
165
+ res.raise_for_status()
166
+
167
+ return matches
168
+
169
+
170
+ def _upload_file(self, bucket: str, name: str, data: bytes, overwrite: bool = False, headers: dict = None) -> dict:
171
+ """
172
+ Envoie un fichier dans un bucket nation-db.
173
+
174
+ ## Paramètres
175
+ bucket: `str`
176
+ Nom du bucket où le fichier sera stocké
177
+ name: `str`
178
+ Nom du fichier dans le drive
179
+ data: `bytes`
180
+ Données à uploader
181
+ overwrite: `bool` (optional)
182
+ Overwrite ou non
183
+ headers: `dict` (optional)
184
+ Headers à envoyer
185
+
186
+ ## Renvoie
187
+ - `dict` contenant les informations de l'upload si réussi
188
+ - `None` en cas d'échec
189
+ """
190
+
191
+ if not headers:
192
+ headers = self.default_headers
193
+ headers['Content-Type'] = 'image/png'
194
+
195
+ body = {
196
+ "name": name,
197
+ "overwrite": json.dumps(overwrite)
198
+ }
199
+
200
+ file = ("file", "image/png", data)
201
+
202
+ res = requests.put(f"{self.url}/upload_file/{bucket}", headers = headers, json = body, files = [ file ])
203
+
204
+ if res.status_code == 200:
205
+ return res.json()
206
+ elif res.status_code in (403, 401):
207
+ raise PermissionError(res.json()['message'])
208
+ elif res.status_code == 409:
209
+ raise FileExistsError(res.json()['message'])
210
+ else:
211
+ raise Exception(f"Error {res.status_code}: {res.json()['message']}")
212
+
213
+ def _download_from_storage(self, bucket: str, path: str, headers: dict = None) -> bytes:
214
+ """
215
+ Télécharge un fichier depuis le stockage nation-db.
216
+
217
+ ## Paramètres
218
+ bucket: `str`\n
219
+ Nom du bucket où il faut chercher le fichier
220
+ path: `str`\n
221
+ Chemin du fichier dans le bucket
222
+
223
+ ## Renvoie
224
+ - Le fichier demandé en `bytes`
225
+ """
226
+
227
+ if not headers:
228
+ headers = self.default_headers
229
+
230
+ res = requests.get(f"{self.url}/drive/{bucket}/{path}", headers = headers)
231
+
232
+ if res.status_code == 200:
233
+ return res.json()
234
+ elif res.status_code in (403, 401):
235
+ raise PermissionError(res.json()['message'])
236
+ else:
237
+ raise Exception(f"Error {res.status_code}: {res.json()['message']}")
@@ -0,0 +1,392 @@
1
+ import requests
2
+ import time
3
+ import typing
4
+ import urllib.parse
5
+
6
+ from .base import NSID
7
+
8
+ from .. import utils
9
+
10
+ default_headers = {}
11
+
12
+ class Permission:
13
+ def __init__(self, initial: str = "----"):
14
+ self.append: bool
15
+ self.manage: bool
16
+ self.edit: bool
17
+ self.read: bool
18
+
19
+ self.load(initial)
20
+
21
+ def load(self, val: str) -> None:
22
+ if 'a' in val: self.append = True
23
+ if 'm' in val: self.manage = True
24
+ if 'e' in val: self.edit = True
25
+ if 'r' in val: self.read = True
26
+
27
+ class PositionPermissions:
28
+ """
29
+ Permissions d'une position à l'échelle du serveur. Certaines sont attribuées selon l'appartenance à divers groupes ayant une position précise
30
+ """
31
+
32
+ def __init__(self) -> None:
33
+ self.bank_accounts = Permission("a---") # APPEND = ouvrir un ou plusieurs comptes, MANAGE = voir les infos globales concernant les comptes en banque, EDIT = gérer des comptes en banque, READ = voir les infos d'un compte en banque individuel
34
+ self.bots = Permission() # APPEND = publier un message sous l'identité d'un bot, MANAGE = proposer d'héberger un bot, EDIT = changer les paramètres d'un bot, READ = /
35
+ self.constitution = Permission() # APPEND = laws.append, MANAGE = laws.manage, EDIT = modifier la constitution, READ = /
36
+ self.database = Permission() # APPEND = créer des sous-bases de données, MANAGE = gérer la abse de données, EDIT = modifier les éléments, READ = avoir accès à toutes les données sans exception
37
+ self.items = Permission("---r") # APPEND = vendre, MANAGE = gérer des items dont on n'est pas propriétaire (hors marketplace), EDIT = gérer des items dont on n'est pas propriétaire (dans le marketplace), READ = accéder au marketplace
38
+ self.laws = Permission() # APPEND = proposer un texte de loi, MANAGE = accepter ou refuser une proposition, EDIT = modifier un texte, READ = /
39
+ self.members = Permission("---r") # APPEND = créer des entités, MANAGE = modérer des entités (hors Discord), EDIT = modifier des entités, READ = voir le profil des entités
40
+ self.national_channel = Permission() # APPEND = prendre la parole sur la chaîne nationale, MANAGE = voir qui peut prendre la parole, EDIT = modifier le planning de la chaîne nationale, READ = /
41
+ self.organizations = Permission("---r") # APPEND = créer une nouvelle organisation, MANAGE = exécuter des actions administratives sur les organisations, EDIT = modifier des organisations, READ = voir le profil de n'importe quelle organisation
42
+ self.reports = Permission() # APPEND = déposer plainte, MANAGE = accépter ou refuser une plainte, EDIT = /, READ = accéder à des infos supplémentaires pour une plainte
43
+ self.state_budgets = Permission() # APPEND = débloquer un nouveau budget, MANAGE = gérer les budjets, EDIT = gérer les sommes pour chaque budjet, READ = accéder aux infos concernant les budgets
44
+ self.votes = Permission() # APPEND = déclencher un vote, MANAGE = fermer un vote, EDIT = /, READ = lire les propriétés d'un vote avant sa fermeture
45
+
46
+ def merge(self, permissions: dict[str, str] | typing.Self):
47
+ if isinstance(permissions, PositionPermissions):
48
+ permissions = permissions.__dict__
49
+
50
+ for key, val in permissions.items():
51
+ perm: Permission = self.__getattribute__(key)
52
+ perm.load(val)
53
+
54
+
55
+ class Position:
56
+ """
57
+ Position légale d'une entité
58
+
59
+ ## Attributs
60
+ - name: `str`\n
61
+ Titre de la position
62
+ - id: `str`\n
63
+ Identifiant de la position
64
+ - permissions: `.PositionPermissions`\n
65
+ Permissions accordées à l'utilisateur
66
+ """
67
+
68
+ def __init__(self, id: str = 'inconnu') -> None:
69
+ self.name: str = "Inconnue"
70
+ self.id = id
71
+ self.permissions: PositionPermissions = PositionPermissions()
72
+
73
+ def __repr__(self):
74
+ return self.id
75
+
76
+ class Entity:
77
+ """
78
+ Classe de référence pour les entités
79
+
80
+ ## Attributs
81
+ - id: `NSID`\n
82
+ Identifiant de l'entité
83
+ - name: `str`\n
84
+ Nom d'usage de l'entité
85
+ - registerDate: `int`\n
86
+ Date d'enregistrement de l'entité
87
+ - position: `.Position`\n
88
+ Position légale de l'entité
89
+ - additional: `dict`\n
90
+ Infos supplémentaires exploitables par les bots
91
+ """
92
+
93
+ def __init__(self, id: NSID) -> None:
94
+ self._url = "" # URL de l'entité pour une requête GET
95
+
96
+ self.id: NSID = NSID(id) # ID hexadécimal de l'entité
97
+ self.name: str = "Entité Inconnue"
98
+ self.registerDate: int = 0
99
+ self.position: Position = Position()
100
+ self.additional: dict = {}
101
+
102
+ def set_name(self, new_name: str) -> None:
103
+ if len(new_name) > 32:
104
+ raise ValueError(f"Name length mustn't exceed 32 characters.")
105
+
106
+ res = requests.post(f"{self._url}/rename?name={new_name}", headers = default_headers)
107
+
108
+ if res.status_code == 200:
109
+ self.name = new_name
110
+ else:
111
+ print(res.status_code)
112
+ res.raise_for_status()
113
+
114
+ def set_position(self, position: Position) -> None:
115
+ res = requests.post(f"{self._url}/change_position?position={position.id}", headers = default_headers)
116
+
117
+ if res.status_code == 200:
118
+ self.position = position
119
+ else:
120
+ res.raise_for_status()
121
+
122
+ def add_link(self, key: str, value: str | int) -> None:
123
+ if isinstance(value, str):
124
+ _class = "string"
125
+ elif isinstance(value, int):
126
+ _class = "integer"
127
+ else:
128
+ raise TypeError("Only strings and integers can be recorded as an additional link")
129
+
130
+ params = {
131
+ "link": key,
132
+ "value": value,
133
+ "type": _class
134
+ }
135
+
136
+ query = "&".join(f"{k}={ urllib.parse.quote(v) }" for k, v in params.items())
137
+
138
+ res = requests.post(f"{self._url}/add_link?{query}", headers = default_headers)
139
+
140
+ if res.status_code == 200:
141
+ self.additional[key] = value
142
+ else:
143
+ print(res.text)
144
+ res.raise_for_status()
145
+
146
+ def unlink(self, key: str) -> None:
147
+ res = requests.post(f"{self._url}/remove_link?link={urllib.parse.quote(key)}", headers = default_headers)
148
+
149
+ if res.status_code == 200:
150
+ del self.additional[key]
151
+ else:
152
+ res.raise_for_status()
153
+
154
+ class User(Entity):
155
+ """
156
+ Entité individuelle
157
+
158
+ ## Attributs
159
+ - Tous les attributs de la classe `.Entity`
160
+ - xp: `int`\n
161
+ Points d'expérience de l'entité
162
+ - boosts: `dict[str, int]`\n
163
+ Ensemble des boosts dont bénéficie l'entité
164
+ - votes: `list[NSID]`\n
165
+ Liste des votes auxquels a participé l'entité
166
+ """
167
+
168
+ def __init__(self, id: NSID) -> None:
169
+ super().__init__(NSID(id))
170
+
171
+ self.xp: int = 0
172
+ self.boosts: dict[str, int] = {}
173
+ self.groups: list[NSID] = []
174
+
175
+ def get_level(self) -> None:
176
+ i = 0
177
+ while self.xp > int(round(25 * (i * 2.5) ** 2, -2)):
178
+ i += 1
179
+
180
+ return i
181
+
182
+ def add_xp(self, amount: int) -> None:
183
+ boost = 0 if 0 in self.boosts.values() or amount <= 0 else max(list(self.boosts.values()) + [ 1 ])
184
+ res = requests.post(f"{self._url}/add_xp?amount={amount * boost}", headers = default_headers)
185
+
186
+ if res.status_code == 200:
187
+ self.xp += amount * boost
188
+ else:
189
+ res.raise_for_status()
190
+
191
+ def edit_boost(self, name: str, multiplier: int = -1) -> None:
192
+ res = requests.post(f"{self._url}/edit_boost?boost={name}&multiplier={multiplier}", headers = default_headers)
193
+
194
+ if res.status_code == 200:
195
+ if multiplier >= 0:
196
+ self.boosts[name] = multiplier
197
+ else:
198
+ del self.boosts[name]
199
+ else:
200
+ res.raise_for_status()
201
+
202
+ class MemberPermissions:
203
+ """
204
+ Permissions d'un utilisateur à l'échelle d'un groupe
205
+ """
206
+
207
+ def __init__(self) -> None:
208
+ self.manage_organization = False # Renommer l'organisation, changer le logo
209
+ self.manage_shares = False # Revaloriser les actions
210
+ self.manage_roles = False # Changer les rôles des membres
211
+ self.manage_members = False # Virer quelqu'un d'une entreprise, l'y inviter
212
+
213
+ def edit(self, **permissions: bool) -> None:
214
+ for perm in permissions.values():
215
+ self.__setattr__(*perm)
216
+
217
+ class GroupMember:
218
+ """
219
+ Membre au sein d'une entité collective
220
+
221
+ ## Attributs
222
+ - permission_level: `dict[str, int]`\n
223
+ Niveau d'accréditation du membre (0 = salarié, 4 = administrateur)
224
+ """
225
+
226
+ def __init__(self, id: NSID) -> None:
227
+ self.id = id
228
+ self.permission_level: dict = { # Échelle de permissions selon le groupe de travail
229
+ "general": 0
230
+ }
231
+
232
+ def group_permissions(self, team: str = "general") -> MemberPermissions:
233
+ p = MemberPermissions()
234
+ team_perms = self.permission_level[team]
235
+
236
+ if team_perms >= 1: # Responsable
237
+ p.manage_members = True
238
+
239
+ if team_perms >= 2: # Superviseur
240
+ p.manage_roles = True
241
+
242
+ if team_perms >= 3: # Chef d'équipe
243
+ pass
244
+
245
+ if team_perms >= 4: # Directeur
246
+ p.manage_shares = True
247
+ p.manage_organization = True
248
+
249
+ return p
250
+
251
+ class GroupInvite:
252
+ def __init__(self, id: NSID):
253
+ self.id: NSID = id
254
+ self.team: str = "general"
255
+ self.level: str = 0
256
+ self._expires: int = round(time.time()) + 604800
257
+
258
+ class Share:
259
+ """
260
+ Action d'une entreprise
261
+
262
+ ## Attributs
263
+ - owner: `NSID`\n
264
+ Identifiant du titulaire de l'action
265
+ - price: `int`\n
266
+ Prix de l'action
267
+ """
268
+
269
+ def __getstate__(self) -> dict:
270
+ return {
271
+ "owner": self.owner,
272
+ "price": self.price
273
+ }
274
+
275
+ def __setstate__(self, state: dict):
276
+ self.owner: NSID = state['owner']
277
+ self.price: int = state['price']
278
+
279
+ def __init__(self, owner: NSID = NSID(0x0), price: int = 10):
280
+ self.owner: NSID = owner
281
+ self.price: int = price
282
+
283
+ def assign_owner(self, owner: NSID):
284
+ self.owner = owner
285
+
286
+ def set_price(self, price: int):
287
+ self.price = price
288
+
289
+ class Organization(Entity):
290
+ """
291
+ Entité collective
292
+
293
+ ## Attributs
294
+ - Tous les attributs de la classe `.Entity`
295
+ - owner: `.Entity`\n
296
+ Utilisateur ou entreprise propriétaire de l'entité collective
297
+ - avatar: `bytes`\n
298
+ Avatar/logo de l'entité collective
299
+ - certifications: `dict[str, int]`\n
300
+ Liste des certifications et de leur date d'ajout
301
+ - members: `list[.GroupMember]`\n
302
+ Liste des membres de l'entreprise
303
+ - parts: `list[.Share]`\n
304
+ Liste des actions émises par l'entreprise
305
+ """
306
+
307
+ def __init__(self, id: NSID) -> None:
308
+ super().__init__(NSID(id))
309
+
310
+ self.owner: Entity = User(NSID(0x0))
311
+ self.avatar: bytes = utils.open_asset('default_avatar.png')
312
+
313
+ self.certifications: dict = {}
314
+ self.members: list[GroupMember] = []
315
+ self.invites: dict[GroupInvite] = []
316
+
317
+ self.parts: list[Share] = 50 * [ Share(self.owner.id, 0) ]
318
+
319
+ def add_certification(self, certification: str, __expires: int = 2419200) -> None:
320
+ res = requests.post(f"{self._url}/add_certification?name={certification}&duration={__expires}", headers = default_headers)
321
+
322
+ if res.status_code == 200:
323
+ self.certifications[certification] = int(round(time.time()) + __expires)
324
+ else:
325
+ res.raise_for_status()
326
+
327
+ def has_certification(self, certification: str) -> bool:
328
+ return certification in self.certifications.keys()
329
+
330
+ def remove_certification(self, certification: str) -> None:
331
+ res = requests.post(f"{self._url}/remove_certification?name={certification}", headers = default_headers)
332
+
333
+ if res.status_code == 200:
334
+ del self.certifications[certification]
335
+ else:
336
+ res.raise_for_status()
337
+
338
+ def invite_member(self, member: NSID, level: int = 0, team: str = "general") -> None:
339
+ if not isinstance(member, NSID):
340
+ raise TypeError("L'entrée membre doit être de type NSID")
341
+
342
+ res = requests.post(f"{self._url}/invite_member?id={member}&level={level}&team={team}", headers = default_headers)
343
+
344
+ if res.status_code == 200:
345
+ invite = GroupInvite(member)
346
+ invite.team = team
347
+ invite.level = level
348
+
349
+ self.invites.append(invite)
350
+ else:
351
+ res.raise_for_status()
352
+
353
+ def remove_member(self, member: GroupMember) -> None:
354
+ for _member in self.members:
355
+ if _member.id == member.id:
356
+ self.members.remove(_member)
357
+
358
+ def remove(self, member: GroupMember) -> None:
359
+ self.remove_member(member)
360
+
361
+ def set_owner(self, member: User) -> None:
362
+ self.owner = member
363
+
364
+ def get_members_by_attr(self, attribute: str = "id") -> list[str]:
365
+ return [ member.__getattribute__(attribute) for member in self.members ]
366
+
367
+ def get_shares(self, include_worth: bool = False) -> dict[str, int] | dict[str, dict[str, int]]:
368
+ shares = {}
369
+
370
+ for share in self.parts:
371
+ if include_worth:
372
+ if share.owner in shares.keys():
373
+ shares[share.owner]['count'] += 1
374
+ shares[share.owner]['worth'] += share.price
375
+ else:
376
+ shares[share.owner] = {
377
+ 'count': 1,
378
+ 'worth': share.price
379
+ }
380
+ else:
381
+ if share.owner in shares.keys():
382
+ shares[share.owner] += 1
383
+ else:
384
+ shares[share.owner] = 1
385
+
386
+ return shares
387
+
388
+ def save_avatar(self, data: bytes = None):
389
+ if not data:
390
+ return
391
+
392
+ self.avatar = data