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.
- odysseeapi-0.0.1/PKG-INFO +77 -0
- odysseeapi-0.0.1/Readme.md +67 -0
- odysseeapi-0.0.1/odysseeapi/Odyssee.py +391 -0
- odysseeapi-0.0.1/odysseeapi/__init__.py +0 -0
- odysseeapi-0.0.1/odysseeapi.egg-info/PKG-INFO +77 -0
- odysseeapi-0.0.1/odysseeapi.egg-info/SOURCES.txt +8 -0
- odysseeapi-0.0.1/odysseeapi.egg-info/dependency_links.txt +1 -0
- odysseeapi-0.0.1/odysseeapi.egg-info/top_level.txt +1 -0
- odysseeapi-0.0.1/pyproject.toml +19 -0
- odysseeapi-0.0.1/setup.cfg +4 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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"
|