nsarchive 3.0.0a1__tar.gz → 3.0.0a3__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.0a3
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.0a3
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,362 @@
1
+ import requests
2
+ import time
3
+ import typing
4
+ import urllib
5
+ import warnings
6
+
7
+ from .base import NSID
8
+
9
+ from .. import utils
10
+
11
+ default_headers = {}
12
+
13
+ class Permission:
14
+ def __init__(self, initial: str = "----"):
15
+ self.append: bool
16
+ self.manage: bool
17
+ self.edit: bool
18
+ self.read: bool
19
+
20
+ self.load(initial)
21
+
22
+ def load(self, val: str) -> None:
23
+ if 'a' in val: self.append = True
24
+ if 'm' in val: self.manage = True
25
+ if 'e' in val: self.edit = True
26
+ if 'r' in val: self.read = True
27
+
28
+ class PositionPermissions:
29
+ """
30
+ Permissions d'une position à l'échelle du serveur. Certaines sont attribuées selon l'appartenance à divers groupes ayant une position précise
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ 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
35
+ 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 = /
36
+ self.constitution = Permission() # APPEND = laws.append, MANAGE = laws.manage, EDIT = modifier la constitution, READ = /
37
+ 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
38
+ 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
39
+ self.laws = Permission() # APPEND = proposer un texte de loi, MANAGE = accepter ou refuser une proposition, EDIT = modifier un texte, READ = /
40
+ 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
41
+ self.money = 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
42
+ 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 = /
43
+ 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
44
+ 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
45
+ 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
46
+ 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
47
+
48
+ def merge(self, permissions: dict[str, str] | typing.Self):
49
+ if isinstance(permissions, PositionPermissions):
50
+ permissions = permissions.__dict__
51
+
52
+ for key, val in permissions.items():
53
+ perm: Permission = self.__getattribute__(key)
54
+ perm.load(val)
55
+
56
+
57
+ class Position:
58
+ """
59
+ Position légale d'une entité
60
+
61
+ ## Attributs
62
+ - name: `str`\n
63
+ Titre de la position
64
+ - id: `str`\n
65
+ Identifiant de la position
66
+ - permissions: `.PositionPermissions`\n
67
+ Permissions accordées à l'utilisateur
68
+ """
69
+
70
+ def __init__(self, id: str = 'inconnu') -> None:
71
+ self.name: str = "Inconnue"
72
+ self.id = id
73
+ self.permissions: PositionPermissions = PositionPermissions()
74
+
75
+ def __repr__(self):
76
+ return self.id
77
+
78
+ class Entity:
79
+ """
80
+ Classe de référence pour les entités
81
+
82
+ ## Attributs
83
+ - id: `NSID`\n
84
+ Identifiant de l'entité
85
+ - name: `str`\n
86
+ Nom d'usage de l'entité
87
+ - registerDate: `int`\n
88
+ Date d'enregistrement de l'entité
89
+ - position: `.Position`\n
90
+ Position légale de l'entité
91
+ - additional: `dict`\n
92
+ Infos supplémentaires exploitables par les bots
93
+ """
94
+
95
+ def __init__(self, id: NSID) -> None:
96
+ self._url = "" # URL de l'entité pour une requête GET
97
+
98
+ self.id: NSID = NSID(id) # ID hexadécimal de l'entité
99
+ self.name: str = "Entité Inconnue"
100
+ self.registerDate: int = 0
101
+ self.position: Position = Position()
102
+ self.additional: dict = {}
103
+
104
+ def set_name(self, new_name: str) -> None:
105
+ if len(new_name) > 32:
106
+ raise ValueError(f"Name length mustn't exceed 32 characters.")
107
+
108
+ res = requests.post(f"{self._url}/rename?name={new_name}", headers = default_headers)
109
+
110
+ if res.status_code == 200:
111
+ self.name = new_name
112
+ else:
113
+ print(res.status_code)
114
+ res.raise_for_status()
115
+
116
+ def set_position(self, position: Position) -> None:
117
+ res = requests.post(f"{self._url}/change_position?position={position.id}", headers = default_headers)
118
+
119
+ if res.status_code == 200:
120
+ self.position = position
121
+ else:
122
+ res.raise_for_status()
123
+
124
+ def add_link(self, key: str, value: str | int) -> None:
125
+ if isinstance(value, str):
126
+ _class = "string"
127
+ elif isinstance(value, int):
128
+ _class = "integer"
129
+ else:
130
+ raise TypeError("Only strings and integers can be recorded as an additional link")
131
+
132
+ params = {
133
+ "link": key,
134
+ "value": value,
135
+ "type": _class
136
+ }
137
+
138
+ query = "&".join(f"{k}={ urllib.parse.quote(v) }" for k, v in params.items())
139
+
140
+ res = requests.post(f"{self._url}/add_link?{query}", headers = default_headers)
141
+
142
+ if res.status_code == 200:
143
+ self.additional[key] = value
144
+ else:
145
+ print(res.text)
146
+ res.raise_for_status()
147
+
148
+ def unlink(self, key: str) -> None:
149
+ res = requests.post(f"{self._url}/remove_link?link={urllib.parse.quote(key)}", headers = default_headers)
150
+
151
+ if res.status_code == 200:
152
+ del self.additional[key]
153
+ else:
154
+ res.raise_for_status()
155
+
156
+ class User(Entity):
157
+ """
158
+ Entité individuelle
159
+
160
+ ## Attributs
161
+ - Tous les attributs de la classe `.Entity`
162
+ - xp: `int`\n
163
+ Points d'expérience de l'entité
164
+ - boosts: `dict[str, int]`\n
165
+ Ensemble des boosts dont bénéficie l'entité
166
+ - votes: `list[NSID]`\n
167
+ Liste des votes auxquels a participé l'entité
168
+ """
169
+
170
+ def __init__(self, id: NSID) -> None:
171
+ super().__init__(NSID(id))
172
+
173
+ self.xp: int = 0
174
+ self.boosts: dict[str, int] = {}
175
+ self.votes: list[NSID] = []
176
+
177
+ def _load(self, _data: dict):
178
+ self.xp = _data['xp']
179
+ self.boosts = _data['boosts']
180
+
181
+ self.votes = [ NSID(vote) for vote in _data['votes'] ]
182
+
183
+ def get_level(self) -> None:
184
+ i = 0
185
+ while self.xp > int(round(25 * (i * 2.5) ** 2, -2)):
186
+ i += 1
187
+
188
+ return i
189
+
190
+ def add_xp(self, amount: int) -> None:
191
+ boost = 0 if 0 in self.boosts.values() or amount <= 0 else max(list(self.boosts.values()) + [ 1 ])
192
+ res = requests.post(f"{self._url}/add_xp?amount={amount * boost}", headers = default_headers)
193
+
194
+ if res.status_code == 200:
195
+ self.xp += amount * boost
196
+ else:
197
+ res.raise_for_status()
198
+
199
+ def edit_boost(self, name: str, multiplier: int = -1) -> None:
200
+ res = requests.post(f"{self._url}/edit_boost?boost={name}&multiplier={multiplier}", headers = default_headers)
201
+
202
+ if res.status_code == 200:
203
+ if multiplier >= 0:
204
+ self.boosts[name] = multiplier
205
+ else:
206
+ del self.boosts[name]
207
+ else:
208
+ res.raise_for_status()
209
+
210
+ class MemberPermissions:
211
+ """
212
+ Permissions d'un utilisateur à l'échelle d'un groupe
213
+ """
214
+
215
+ def __init__(self) -> None:
216
+ self.manage_organization = False # Renommer l'organisation, changer le logo
217
+ self.manage_shares = False # Revaloriser les actions
218
+ self.manage_roles = False # Changer les rôles des membres
219
+ self.manage_members = False # Virer quelqu'un d'une entreprise, l'y inviter
220
+
221
+ def edit(self, **permissions: bool) -> None:
222
+ for perm in permissions.values():
223
+ self.__setattr__(*perm)
224
+
225
+ class GroupMember:
226
+ """
227
+ Membre au sein d'une entité collective
228
+
229
+ ## Attributs
230
+ - permission_level: `dict[str, int]`\n
231
+ Niveau d'accréditation du membre (0 = salarié, 4 = administrateur)
232
+ """
233
+
234
+ def __init__(self, id: NSID) -> None:
235
+ self.id = id
236
+ self.permission_level: dict = { # Échelle de permissions selon le groupe de travail
237
+ "general": 0
238
+ }
239
+
240
+ def group_permissions(self, team: str = "general") -> MemberPermissions:
241
+ p = MemberPermissions()
242
+ team_perms = self.permission_level[team]
243
+
244
+ if team_perms >= 1: # Responsable
245
+ p.manage_members = True
246
+
247
+ if team_perms >= 2: # Superviseur
248
+ p.manage_roles = True
249
+
250
+ if team_perms >= 3: # Chef d'équipe
251
+ pass
252
+
253
+ if team_perms >= 4: # Directeur
254
+ p.manage_shares = True
255
+ p.manage_organization = True
256
+
257
+ return p
258
+
259
+ class GroupInvite:
260
+ def __init__(self, id: NSID):
261
+ self.id: NSID = id
262
+ self.team: str = "general"
263
+ self.level: str = 0
264
+ self._expires: int = round(time.time()) + 604800
265
+
266
+ class Organization(Entity):
267
+ """
268
+ Entité collective
269
+
270
+ ## Attributs
271
+ - Tous les attributs de la classe `.Entity`
272
+ - owner: `.Entity`\n
273
+ Utilisateur ou entreprise propriétaire de l'entité collective
274
+ - avatar: `bytes`\n
275
+ Avatar/logo de l'entité collective
276
+ - certifications: `dict[str, int]`\n
277
+ Liste des certifications et de leur date d'ajout
278
+ - members: `list[.GroupMember]`\n
279
+ Liste des membres de l'entreprise
280
+ - parts: `list[.Share]`\n
281
+ Liste des actions émises par l'entreprise
282
+ """
283
+
284
+ def __init__(self, id: NSID) -> None:
285
+ super().__init__(NSID(id))
286
+
287
+ self.owner: Entity = User(NSID(0x0))
288
+ self.avatar: bytes = utils.open_asset('default_avatar.png')
289
+
290
+ self.certifications: dict = {}
291
+ self.members: list[GroupMember] = []
292
+ self.invites: dict[GroupInvite] = []
293
+
294
+ def _load(self, _data: dict):
295
+ res = requests.get(f"{self._url}/avatar")
296
+
297
+ if res.status_code == 200:
298
+ self.avatar = res.content
299
+ else:
300
+ warnings.warn(f"Failed to get avatar for {self.id}")
301
+
302
+ for _member in _data['members']:
303
+ member = GroupMember(_member['id'])
304
+ member.permission_level = _member['level']
305
+
306
+ self.members.append(member)
307
+
308
+ self.certifications = _data['certifications']
309
+
310
+ def add_certification(self, certification: str, __expires: int = 2419200) -> None:
311
+ res = requests.post(f"{self._url}/add_certification?name={certification}&duration={__expires}", headers = default_headers)
312
+
313
+ if res.status_code == 200:
314
+ self.certifications[certification] = int(round(time.time()) + __expires)
315
+ else:
316
+ res.raise_for_status()
317
+
318
+ def has_certification(self, certification: str) -> bool:
319
+ return certification in self.certifications.keys()
320
+
321
+ def remove_certification(self, certification: str) -> None:
322
+ res = requests.post(f"{self._url}/remove_certification?name={certification}", headers = default_headers)
323
+
324
+ if res.status_code == 200:
325
+ del self.certifications[certification]
326
+ else:
327
+ res.raise_for_status()
328
+
329
+ def invite_member(self, member: NSID, level: int = 0, team: str = "general") -> None:
330
+ if not isinstance(member, NSID):
331
+ raise TypeError("L'entrée membre doit être de type NSID")
332
+
333
+ res = requests.post(f"{self._url}/invite_member?id={member}&level={level}&team={team}", headers = default_headers)
334
+
335
+ if res.status_code == 200:
336
+ invite = GroupInvite(member)
337
+ invite.team = team
338
+ invite.level = level
339
+
340
+ self.invites.append(invite)
341
+ else:
342
+ res.raise_for_status()
343
+
344
+ def remove_member(self, member: GroupMember) -> None:
345
+ for _member in self.members:
346
+ if _member.id == member.id:
347
+ self.members.remove(_member)
348
+
349
+ def remove(self, member: GroupMember) -> None:
350
+ self.remove_member(member)
351
+
352
+ def set_owner(self, member: User) -> None:
353
+ self.owner = member
354
+
355
+ def get_members_by_attr(self, attribute: str = "id") -> list[str]:
356
+ return [ member.__getattribute__(attribute) for member in self.members ]
357
+
358
+ def save_avatar(self, data: bytes = None):
359
+ if not data:
360
+ return
361
+
362
+ self.avatar = data