odysseeapi 0.0.1__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.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: odysseeapi
3
+ Version: 0.0.1
4
+ Summary: Unofficial Python API for Odyssee
5
+ Author-email: Géry Casiez <gery.casiez@univ-lille.fr>
6
+ Project-URL: Repository, https://github.com/casiez/OdysseeAPI
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Odyssee API
12
+
13
+ Provides an unofficial API for the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/), allowing users to access and interact with their data programmatically.
14
+
15
+ ## Features
16
+
17
+ - get information about candidates
18
+ - get information about committee members
19
+ - get information about institutions
20
+ - get keywords
21
+ - assign applications to committee members
22
+ - get reports from committee members
23
+ - provide decision on each candidate after the first round of evaluation
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install odysseeapi
29
+ ```
30
+
31
+ ## Getting the authentication token
32
+ 1. Go to the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/) using Chrome
33
+ 1. Open the developer tools (F12 or right-click and select "Inspect")
34
+ 1. Go to the "Network" tab
35
+ 1. Refresh the page
36
+ 1. Look for a request
37
+ - Click on the request and go to the "Headers" tab
38
+ - Look for the "Authorization" header and copy its value (it should start with "Bearer")
39
+ 1. Copy the token and use it in your code to authenticate with the API
40
+
41
+ Note that the authentication uses both cookies and a bearer token, so you need to provide both to access the API.
42
+ The API automatically handles the cookie (using [browsercookie](https://pypi.org/project/browsercookie/)), so you only need to provide the bearer token.
43
+
44
+ Note that the token is valid for a limited time, so you may need to repeat this process periodically to obtain a new token.
45
+
46
+ ![image](bearer.png)
47
+
48
+ ## Minimal example
49
+
50
+ ```python
51
+ from odysseeapi.Odyssee import Odyssee
52
+
53
+ numposte = "123456"
54
+ token = """eyJhbGciOiJSUzI1N... (token value) ..."""
55
+ candidates = odyssee.get_candidates()
56
+ print(candidates)
57
+
58
+ rapporteurs = odyssee.get_committee_members()
59
+ print(rapporteurs)
60
+
61
+ etablissements = odyssee.get_institutions()
62
+ print(etablissements)
63
+
64
+ keywords = odyssee.get_keywords()
65
+ print(keywords)
66
+
67
+ candidates_with_details = odyssee.get_candidates_with_details()
68
+ print(candidates_with_details)
69
+
70
+ # odyssee.assign_jury_members_to_candidate("f1e2d3c4-b5a6-4789-9876-543210fedcba", "98765432-10fe-dcba-9876-543210fedcba", "abcd1234-5678-90ef-ghij-klmnopqrstuv")
71
+
72
+ # Download the reports of the committee members for the candidates in the specified directory
73
+ odyssee.downloadReports("rapportsOdyssee")
74
+
75
+ # A l'issue de la première réunion du jury, enregistrer l'avis pour chaque candidat et les résultats du vote
76
+ # odyssee.opinion_for_interview("f1e2d3c4-b5a6-4789-9876-543210fedcba", "A", "Motif audition", 16, 16, 0, 0, 16, 0)
77
+ ```
@@ -0,0 +1,67 @@
1
+ # Odyssee API
2
+
3
+ Provides an unofficial API for the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/), allowing users to access and interact with their data programmatically.
4
+
5
+ ## Features
6
+
7
+ - get information about candidates
8
+ - get information about committee members
9
+ - get information about institutions
10
+ - get keywords
11
+ - assign applications to committee members
12
+ - get reports from committee members
13
+ - provide decision on each candidate after the first round of evaluation
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install odysseeapi
19
+ ```
20
+
21
+ ## Getting the authentication token
22
+ 1. Go to the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/) using Chrome
23
+ 1. Open the developer tools (F12 or right-click and select "Inspect")
24
+ 1. Go to the "Network" tab
25
+ 1. Refresh the page
26
+ 1. Look for a request
27
+ - Click on the request and go to the "Headers" tab
28
+ - Look for the "Authorization" header and copy its value (it should start with "Bearer")
29
+ 1. Copy the token and use it in your code to authenticate with the API
30
+
31
+ Note that the authentication uses both cookies and a bearer token, so you need to provide both to access the API.
32
+ The API automatically handles the cookie (using [browsercookie](https://pypi.org/project/browsercookie/)), so you only need to provide the bearer token.
33
+
34
+ Note that the token is valid for a limited time, so you may need to repeat this process periodically to obtain a new token.
35
+
36
+ ![image](bearer.png)
37
+
38
+ ## Minimal example
39
+
40
+ ```python
41
+ from odysseeapi.Odyssee import Odyssee
42
+
43
+ numposte = "123456"
44
+ token = """eyJhbGciOiJSUzI1N... (token value) ..."""
45
+ candidates = odyssee.get_candidates()
46
+ print(candidates)
47
+
48
+ rapporteurs = odyssee.get_committee_members()
49
+ print(rapporteurs)
50
+
51
+ etablissements = odyssee.get_institutions()
52
+ print(etablissements)
53
+
54
+ keywords = odyssee.get_keywords()
55
+ print(keywords)
56
+
57
+ candidates_with_details = odyssee.get_candidates_with_details()
58
+ print(candidates_with_details)
59
+
60
+ # odyssee.assign_jury_members_to_candidate("f1e2d3c4-b5a6-4789-9876-543210fedcba", "98765432-10fe-dcba-9876-543210fedcba", "abcd1234-5678-90ef-ghij-klmnopqrstuv")
61
+
62
+ # Download the reports of the committee members for the candidates in the specified directory
63
+ odyssee.downloadReports("rapportsOdyssee")
64
+
65
+ # A l'issue de la première réunion du jury, enregistrer l'avis pour chaque candidat et les résultats du vote
66
+ # odyssee.opinion_for_interview("f1e2d3c4-b5a6-4789-9876-543210fedcba", "A", "Motif audition", 16, 16, 0, 0, 16, 0)
67
+ ```
@@ -0,0 +1,391 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Odyssee.py -
4
+ #
5
+ # Authors:
6
+ # Géry Casiez https://gery.casiez.net
7
+ #
8
+ # 2026
9
+ #
10
+ # BSD License https://opensource.org/licenses/BSD-3-Clause
11
+ #
12
+ # Redistribution and use in source and binary forms, with or without
13
+ # modification, are permitted provided that the following conditions are met:
14
+ #
15
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions
16
+ # and the following disclaimer.
17
+ #
18
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
19
+ # and the following disclaimer in the documentation and/or other materials provided with the distribution.
20
+ #
21
+ # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or
22
+ # promote products derived from this software without specific prior written permission.
23
+
24
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ import requests
34
+ import browsercookie
35
+ import json
36
+ import tqdm
37
+ import os
38
+
39
+ class Odyssee(object):
40
+ def __init__(self, numposte, bearer_token):
41
+ """Initialize the Odyssee class.
42
+ numposte: the numposte to use for the API calls
43
+ bearer_token: the bearer token to use for the API calls"""
44
+
45
+ self.numposte = numposte
46
+ self.base_url = "https://odyssee.enseignementsup-recherche.gouv.fr/gateway/"
47
+ self.s = requests.session()
48
+ self.headers = {"Authorization": f"Bearer {bearer_token}"}
49
+ self.cookies = browsercookie.chrome()
50
+ self.candidatures = None
51
+ self.rapporteurs = None
52
+ self.etablissements = None
53
+ self.keywords = None
54
+ self.reportsPath = "rapportsOdyssee"
55
+
56
+ def get_candidates(self):
57
+ """
58
+ Get candidates for a given numposte.
59
+ returns a list of candidates, where each candidate is a dictionary containing the following keys:
60
+ {
61
+ "numeroOffrePoste": ,
62
+ "candidat": {
63
+ "offreJury": ,
64
+ "civilite": ,
65
+ "nomUsage": ,
66
+ "prenom": ,
67
+ "idKeycloakActuel": ,
68
+ "idKeycloakSuivant": ,
69
+ "premierAvis": ,
70
+ "motifPremierAvis": ,
71
+ "secondAvis": ,
72
+ "motifSecondAvis": ,
73
+ "rangDansClassement": ,
74
+ "articleReference":
75
+ },
76
+ "rapporteur1": ,
77
+ "rapporteur2": ,
78
+ "depotRapportUtilisateur": ,
79
+ "nombreRapportDepose": ,
80
+ "etatDossierCandidature": ,
81
+ "candidatureAttribueeRapporteurConnecte":
82
+ }
83
+ """
84
+
85
+ candidatures = self.s.get(f"{self.base_url}recrutements/offre-poste/candidature/candidatures/examen/{self.numposte}", params={"page": "0", "limite": "200", "filtres": "()"}, headers=self.headers, cookies=self.cookies)
86
+ if candidatures.status_code != 200:
87
+ msg = f"Failed to get candidatures: {candidatures.status_code} - {candidatures.text}"
88
+ raise Exception(msg)
89
+ json_data = json.loads(candidatures.content)
90
+ self.candidatures = json_data["content"]
91
+ return self.candidatures
92
+
93
+ def get_committee_members(self):
94
+ """Get committee members for a given numposte.
95
+ returns a list of committee members, where each member is a dictionary containing the following keys:
96
+ {
97
+ "id": ,
98
+ "idKeycloak": ,
99
+ "nomUsage": ,
100
+ "prenom": ,
101
+ "civilite": ,
102
+ "rang": ,
103
+ "etablissement": ,
104
+ "rapportAttribuePourCePoste": ,
105
+ "rapportAttribueTousPostes": ,
106
+ "cheminRapport": ,
107
+ "numeroRapporteur":
108
+ }
109
+ """
110
+ rapporteurs = self.s.get(f"{self.base_url}recrutements/offre-jury/chercher-rapporteurs/{self.numposte}", params={"page": "0", "limite": "50"}, headers=self.headers, cookies=self.cookies)
111
+ if rapporteurs.status_code != 200:
112
+ msg = f"Failed to get rapporteurs: {rapporteurs.status_code} - {rapporteurs.text}"
113
+ raise Exception(msg)
114
+ json_data = json.loads(rapporteurs.content)
115
+ self.rapporteurs = json_data["content"]
116
+ return self.rapporteurs
117
+
118
+ def get_institutions(self):
119
+ """Get institutions.
120
+ returns a list of institutions, where each institution is a dictionary containing the following keys:
121
+ {
122
+ "id": ,
123
+ "codeUai": ,
124
+ "libelle": ,
125
+ "lienSiteWeb": ,
126
+ "actif": ,
127
+ "etablissementRattachement": ,
128
+ "idEtablissementRattachement": ,
129
+ "adresse": ,
130
+ "logo": ,
131
+ "academie": {
132
+ "id": ,
133
+ "libelle": ,
134
+ "code": ,
135
+ "libelleCourt": ,
136
+ "dateOuverture": ,
137
+ "dateFermeture": ,
138
+ "recteurRegionAcademique": ,
139
+ "natureTerritoire": ,
140
+ "region": {
141
+ "id": ,
142
+ "libelle": ,
143
+ "code": ,
144
+ "paysDto": {
145
+ "id": ,
146
+ "libelle": ,
147
+ "code": ,
148
+ "libelleEuraxess":
149
+ }
150
+ }
151
+ },
152
+ "determinant": {
153
+ "id": ,
154
+ "libelle": ,
155
+ "code":
156
+ },
157
+ "cnap": ,
158
+ "sante": ,
159
+ "dateOuverture": ,
160
+ "dateFermeture": ,
161
+ "description":
162
+ }
163
+ """
164
+ etablissements = self.s.get(f"{self.base_url}referentiels/etablissements", headers=self.headers, cookies=self.cookies)
165
+ if etablissements.status_code != 200:
166
+ msg = f"Failed to get etablissements: {etablissements.status_code} - {etablissements.text}"
167
+ raise Exception(msg)
168
+ self.etablissements = json.loads(etablissements.content)
169
+ return self.etablissements
170
+
171
+ def get_keywords(self):
172
+ """Get keywords
173
+ returns a list of keywords, where each keyword is a dictionary containing the following keys:
174
+ {
175
+ "id": ,
176
+ "libelle": ,
177
+ "sectionId": ,
178
+ "supprimable":
179
+ }
180
+ """
181
+ keywords = self.s.get(f"{self.base_url}referentiels/mots-cles", headers=self.headers, cookies=self.cookies)
182
+ if keywords.status_code != 200:
183
+ msg = f"Failed to get mots clés: {keywords.status_code} - {keywords.text}"
184
+ raise Exception(msg)
185
+ self.keywords = json.loads(keywords.content)
186
+ return self.keywords
187
+
188
+ def __detailCandidature(self, id_candidat):
189
+ candidature = self.s.get(f"{self.base_url}recrutements/offre-poste/candidature/detail-candidature/{self.numposte}/{id_candidat}", headers=self.headers, cookies=self.cookies)
190
+ if candidature.status_code != 200:
191
+ msg = f"Failed to get candidature details for candidate {id_candidat}: {candidature.status_code} - {candidature.text}"
192
+ raise Exception(msg)
193
+ return json.loads(candidature.content)
194
+
195
+ def get_candidates_with_details(self):
196
+ """Get details of candidates for a given numposte.
197
+ returns a list of details of candidates, where each detail is a dictionary containing the following keys:
198
+ {
199
+ "candidatureSection": {
200
+ "idSection":
201
+ },
202
+ "candidatureHdr": ,
203
+ "candidatureThese": {
204
+ "id": ,
205
+ "dateObtention": ,
206
+ "lieuSoutenanceEtablissement": ,
207
+ "titre": ,
208
+ "lieuSoutenanceAutre": ,
209
+ "directeur": {
210
+ "id": ,
211
+ "nom": ,
212
+ "prenom": ,
213
+ "civilite": ,
214
+ "qualiteMembreJury":
215
+ }
216
+ },
217
+ "candidatureJuryThese": {
218
+ "president": {
219
+ "id": ,
220
+ "nom":,
221
+ "prenom": ,
222
+ "civilite": ,
223
+ "qualiteMembreJury":
224
+ },
225
+ "membres": [
226
+ {
227
+ "id": ,
228
+ "nom": ,
229
+ "prenom": ,
230
+ "civilite": ,
231
+ "qualiteMembreJury":
232
+ },
233
+ ...
234
+ ]
235
+ },
236
+ "candidatureLieuRecherche": {
237
+ "etablissementLieuRecherche": ,
238
+ "laboratoiresReferences": [
239
+ ...
240
+ ],
241
+ "laboratoiresNonReferences":
242
+ },
243
+ "candidatureBlocMotCle": {
244
+ "description": ,
245
+ "motsCles": [
246
+ {
247
+ "id": ,
248
+ "motCle": ,
249
+ "motCleLibre":
250
+ },
251
+ ...
252
+ ]
253
+ },
254
+ """
255
+ if self.candidatures is None:
256
+ self.get_candidates()
257
+
258
+ self.detailCandidatures = []
259
+ for c in tqdm.tqdm(self.candidatures, desc="Récupération des détails des candidatures"):
260
+ id_candidat = c["candidat"]["idKeycloakActuel"]
261
+ self.detailCandidatures.append(self.__detailCandidature(id_candidat))
262
+ return self.detailCandidatures
263
+
264
+ def assign_jury_members_to_candidate(self, id_candidat, id_rapporteur1, id_rapporteur2):
265
+ """Assign rapporteurs to a candidature.
266
+ id_candidat: the id of the candidate to assign rapporteurs to
267
+ id_rapporteur1: the id of the first rapporteur to assign
268
+ id_rapporteur2: the id of the second rapporteur to assign
269
+ returns true if the assignment was successful, false otherwise
270
+ """
271
+
272
+ status = self.s.post(f"{self.base_url}recrutements/candidature-rapporteur/creer-rapporteurs-pour-candidature/{self.numposte}/{id_candidat}", json=[{"numeroRapporteur":1,"id":id_rapporteur1},{"numeroRapporteur":2,"id":id_rapporteur2}], headers=self.headers, cookies=self.cookies)
273
+ return status.status_code == 201
274
+
275
+ def __createDir(self, path):
276
+ if (not(os.path.exists(path))):
277
+ os.makedirs(path)
278
+
279
+ def __getReports(self, c, callback=None):
280
+ response = self.s.get(f"{self.base_url}recrutements/candidature-rapporteur/recuperation-rapporteurs/{self.numposte}/{c['candidat']['idKeycloakActuel']}", headers=self.headers, cookies=self.cookies)
281
+ count = 0
282
+
283
+ res = {}
284
+
285
+ if response.status_code == 200:
286
+ infos = json.loads(response.content)
287
+ for i, r in enumerate(infos):
288
+ if r.get("cheminRapport", None) is not None:
289
+ filename = self.__downloadReport(r, c)
290
+ if callback is not None and filename is not None:
291
+ res = callback(filename, res, c, i)
292
+ count += 1
293
+ else:
294
+ msg = f"Failed to get rapporteurs for candidate {c['candidat']['idKeycloakActuel']}: {response.status_code} - {response.text}"
295
+ raise Exception(msg)
296
+
297
+ res['count'] = count
298
+ return res
299
+
300
+ def __downloadReport(self, infoRapport, c):
301
+ response = self.s.get(f"{self.base_url}s3/fichier", params={"chemin": infoRapport["cheminRapport"]}, headers=self.headers, cookies=self.cookies)
302
+
303
+ if response.status_code == 200:
304
+ fileName = f"{self.reportsPath}/{c['candidat']['nomUsage'].upper()}_{c['candidat']['prenom'].capitalize()}_{infoRapport['nomUsage']}_{infoRapport['prenom'].capitalize()}.pdf"
305
+ with open(fileName, "wb") as f:
306
+ f.write(response.content)
307
+ return fileName
308
+ else:
309
+ msg = f"Failed to download report for candidate {c['candidat']['idKeycloakActuel']}: {response.status_code} - {response.text}"
310
+ raise Exception(msg)
311
+ return None
312
+
313
+ def download_reports(self, path, callback=None):
314
+ """Download reports for all candidatures and save them in the specified path."""
315
+ self.reportsPath = path
316
+ self.__createDir(self.reportsPath)
317
+ totaltermines = 0
318
+ allinfos = []
319
+
320
+ if self.candidatures is None:
321
+ self.get_candidates()
322
+
323
+ for c in tqdm.tqdm(self.candidatures, desc="Telechargement des rapports"):
324
+ infos = self.__getReports(c, callback)
325
+ totaltermines += infos.get("count", 0)
326
+ infos["candidatID"] = c["candidat"]["idKeycloakActuel"]
327
+ allinfos.append(infos)
328
+
329
+ infos = {
330
+ "total": len(self.candidatures)*2,
331
+ "termines": totaltermines,
332
+ "infos": allinfos
333
+ }
334
+
335
+ return infos
336
+
337
+ def __getIDVoteJury(self, id_candidat):
338
+ response = self.s.get(f"{self.base_url}recrutements/vote-jury/recuperer/{self.numposte}?votePremierAvis=true&idKeycloak={id_candidat}", headers=self.headers, cookies=self.cookies)
339
+ if response.status_code == 200:
340
+ infos = json.loads(response.content)
341
+ return infos["id"]
342
+ else:
343
+ msg = f"Failed to get ID of vote jury for candidate {id_candidat}: {response.status_code} - {response.text}"
344
+ raise Exception(msg)
345
+ return None
346
+
347
+ def opinion_for_interview(self, id_candidat, avis, motif, nombreVotants, suffragesExprimes, bulletinsNuls, bulletinsBlancs, bulletinsEnAccord, bulletinsEnDesaccord):
348
+ """
349
+ Record the opinion of the jury for a given candidate.
350
+ id_candidat: the ID of the candidate for whom to record the opinion
351
+ avis: "A" for "Favorable opinion for audition", "D" for "Unfavorable opinion for audition"
352
+ motif: the reason for the opinion (max 1400 characters)
353
+ nombreVotants: the number of voters
354
+ suffragesExprimes: the number of votes cast
355
+ bulletinsNuls: the number of null ballots
356
+ bulletinsBlancs: the number of blank ballots
357
+ bulletinsEnAccord: the number of ballots in agreement
358
+ bulletinsEnDesaccord: the number of ballots in disagreement
359
+ returns true if the avis was successfully recorded, false otherwise
360
+ """
361
+ if len(motif) > 1400:
362
+ raise Exception("The opinion motif must be at most 1400 characters long")
363
+
364
+ id_vote_jury = self.__getIDVoteJury(id_candidat)
365
+ if id_vote_jury is None:
366
+ msg = f"Failed to get ID of vote jury for candidate {id_candidat}"
367
+ raise Exception(msg)
368
+
369
+ jsondata = {
370
+ "numeroPoste": self.numposte,
371
+ "idKeycloak": id_candidat,
372
+ "avis": {
373
+ "code": "1" if avis == "A" else "2"
374
+ },
375
+ "motif": motif,
376
+ "estPremierAvis": True,
377
+ "voteJuryAEnregistrer": {
378
+ "numeroOffrePoste": self.numposte,
379
+ "estPremierAvis": True,
380
+ "idKeycloak": id_candidat,
381
+ "id": id_vote_jury,
382
+ "nombreVotants": nombreVotants,
383
+ "suffragesExprimes": suffragesExprimes,
384
+ "bulletinsNuls": bulletinsNuls,
385
+ "bulletinsBlancs": bulletinsBlancs,
386
+ "bulletinsEnAccord": bulletinsEnAccord,
387
+ "bulletinsEnDesaccord": bulletinsEnDesaccord
388
+ }
389
+ }
390
+
391
+ return self.s.put(f"{self.base_url}recrutements/offre-poste/candidature/enregistrer-avis", json=jsondata, headers=self.headers, cookies=self.cookies)
File without changes
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: odysseeapi
3
+ Version: 0.0.1
4
+ Summary: Unofficial Python API for Odyssee
5
+ Author-email: Géry Casiez <gery.casiez@univ-lille.fr>
6
+ Project-URL: Repository, https://github.com/casiez/OdysseeAPI
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Odyssee API
12
+
13
+ Provides an unofficial API for the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/), allowing users to access and interact with their data programmatically.
14
+
15
+ ## Features
16
+
17
+ - get information about candidates
18
+ - get information about committee members
19
+ - get information about institutions
20
+ - get keywords
21
+ - assign applications to committee members
22
+ - get reports from committee members
23
+ - provide decision on each candidate after the first round of evaluation
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install odysseeapi
29
+ ```
30
+
31
+ ## Getting the authentication token
32
+ 1. Go to the [Odyssee platform](https://odyssee.enseignementsup-recherche.gouv.fr/) using Chrome
33
+ 1. Open the developer tools (F12 or right-click and select "Inspect")
34
+ 1. Go to the "Network" tab
35
+ 1. Refresh the page
36
+ 1. Look for a request
37
+ - Click on the request and go to the "Headers" tab
38
+ - Look for the "Authorization" header and copy its value (it should start with "Bearer")
39
+ 1. Copy the token and use it in your code to authenticate with the API
40
+
41
+ Note that the authentication uses both cookies and a bearer token, so you need to provide both to access the API.
42
+ The API automatically handles the cookie (using [browsercookie](https://pypi.org/project/browsercookie/)), so you only need to provide the bearer token.
43
+
44
+ Note that the token is valid for a limited time, so you may need to repeat this process periodically to obtain a new token.
45
+
46
+ ![image](bearer.png)
47
+
48
+ ## Minimal example
49
+
50
+ ```python
51
+ from odysseeapi.Odyssee import Odyssee
52
+
53
+ numposte = "123456"
54
+ token = """eyJhbGciOiJSUzI1N... (token value) ..."""
55
+ candidates = odyssee.get_candidates()
56
+ print(candidates)
57
+
58
+ rapporteurs = odyssee.get_committee_members()
59
+ print(rapporteurs)
60
+
61
+ etablissements = odyssee.get_institutions()
62
+ print(etablissements)
63
+
64
+ keywords = odyssee.get_keywords()
65
+ print(keywords)
66
+
67
+ candidates_with_details = odyssee.get_candidates_with_details()
68
+ print(candidates_with_details)
69
+
70
+ # odyssee.assign_jury_members_to_candidate("f1e2d3c4-b5a6-4789-9876-543210fedcba", "98765432-10fe-dcba-9876-543210fedcba", "abcd1234-5678-90ef-ghij-klmnopqrstuv")
71
+
72
+ # Download the reports of the committee members for the candidates in the specified directory
73
+ odyssee.downloadReports("rapportsOdyssee")
74
+
75
+ # A l'issue de la première réunion du jury, enregistrer l'avis pour chaque candidat et les résultats du vote
76
+ # odyssee.opinion_for_interview("f1e2d3c4-b5a6-4789-9876-543210fedcba", "A", "Motif audition", 16, 16, 0, 0, 16, 0)
77
+ ```
@@ -0,0 +1,8 @@
1
+ Readme.md
2
+ pyproject.toml
3
+ odysseeapi/Odyssee.py
4
+ odysseeapi/__init__.py
5
+ odysseeapi.egg-info/PKG-INFO
6
+ odysseeapi.egg-info/SOURCES.txt
7
+ odysseeapi.egg-info/dependency_links.txt
8
+ odysseeapi.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ odysseeapi
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 61.0", "requests", "browsercookie", "tqdm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "odysseeapi"
7
+ version = "0.0.1"
8
+ authors = [
9
+ {name = "Géry Casiez", email = "gery.casiez@univ-lille.fr"},
10
+ ]
11
+ description = "Unofficial Python API for Odyssee"
12
+ readme = {file = "Readme.md", content-type = "text/markdown"}
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: BSD License"
16
+ ]
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/casiez/OdysseeAPI"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+