Spaces:
Running
Running
File size: 8,649 Bytes
5754325 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
import datetime
import time
import re
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from requests.exceptions import HTTPError
# CONSTANTS
ENDPOINT_ACCESS_TOKEN = "https://entreprise.francetravail.fr/connexion/oauth2/access_token"
OFFRES_DEMPLOI_V2_BASE = "https://api.francetravail.io/partenaire/offresdemploi/v2/"
REFERENTIEL_ENDPOINT = "{}/referentiel".format(OFFRES_DEMPLOI_V2_BASE)
SEARCH_ENDPOINT = "{}/offres/search".format(OFFRES_DEMPLOI_V2_BASE)
class Api:
"""
Class to authentificate and use the methods of the 'API Offres emploi v2' from Emploi Store (Pole Emploi).
"""
def __init__(self, client_id, client_secret, verbose=False, proxies=None):
"""
Constructor to authentificate to 'Offres d'emploi v2'. Authentification is done using OAuth client credential grant. 'client_id' and 'client_secret' must be specified.
Retry mechanisms are implemented in case the user does too many requests (code 429: too many requests) or just because the API might sometimes be unreliable (code 502: bad gateway).
:param client_id: the client ID
:type client_id: str
:param client_secret: the client secret
:type client_secret: str
:param verbose: whether to add verbosity
:type verbose: bool
:param proxies: (optional) The proxies configuration
:type proxies: dict with keys 'http' and/or 'https'
:returns: None
:Example 1:
>>> from offres_demploi import Api
>>> client = Api(client_id="<your_client_id>", client_secret="<your_client_secret")
:Example 2:
>>> from offres_demploi import Api
>>> proxy = "localhost:3128"
>>> proxies = {"http": proxy, "https": proxy}
>>> client_id = "<your_client_id>"
>>> client_secret = "<your_client_secret"
>>> client = Api(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, proxies=proxies)
"""
self.client_id = client_id
self.client_secret = client_secret
self.verbose = verbose
self.proxies = proxies
self.timeout = 60
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=1,
status_forcelist=(
502,
429,
), # 429 for too many requests and 502 for bad gateway
respect_retry_after_header=False,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
self.session = session
def get_token(self):
"""
Get the token as a class field (for subsequent use).
:rtype: dict
:returns: A token with fields form API + expires_at custom field
:raises HTTPError: Error when requesting the ressource
"""
data = dict(
grant_type="client_credentials",
client_id=self.client_id,
client_secret=self.client_secret,
scope="api_offresdemploiv2 o2dsoffre application_{}".format(
self.client_id
),
)
headers = {"content-type": "application/x-www-form-urlencoded"}
params = dict(realm="/partenaire")
current_time = datetime.datetime.today()
r = requests.post(
url=ENDPOINT_ACCESS_TOKEN,
headers=headers,
data=data,
params=params,
timeout=self.timeout,
proxies=self.proxies,
)
try:
r.raise_for_status()
except HTTPError as error:
if r.status_code == 400:
complete_message = str(error) + "\n" + str(r.json())
raise HTTPError(complete_message)
else:
raise error
else:
token = r.json()
token["expires_at"] = current_time + datetime.timedelta(
seconds=token["expires_in"]
)
self.token = token
return token
def is_expired(self):
"""
Test if the broken as expired (based on the 'expires_at' field)
:rtype: boolean
:returns: True if the token has expired, False otherwise
"""
expired = datetime.datetime.today() >= self.token["expires_at"]
return expired
def get_headers(self):
"""
:rtype: dict
:returns: The headers necessary to do requests. Will ask a new token if it has expired since or it has never been requested
"""
if not hasattr(self, "token"):
if self.verbose:
print("Token has not been requested yet. Requesting token")
self.get_token()
elif self.is_expired():
if self.verbose:
print("Token is expired. Requesting new token")
self.get_token()
headers = {
"Authorization": "Bearer {}".format(self.token["access_token"])
}
return headers
def referentiel(self, referentiel):
"""
Get dictionary of 'referentiel'.
'Réferentiel' available: domaine, appellations (domaines professionnelles ROME), metiers, themes, continents,
pays, regions, departements , communes , secteursActivites, naturesContrats, typesContrats, niveauxFormations,
permis, langues
Full list available at: https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-offres-demploi-v2/referentiels.html
:param referentiel: The 'referentiel' to look for
:type referentiel: str
:raises HTTPError: Error when requesting the ressource
:rtype: dict
:returns: The 'referentiel' with the keys 'code' for the acronyme/abbreviation and 'libelle' for the full name.
:Example:
>>> client.referentiel("themes")
"""
referentiel_endpoint = "{}/{}".format(REFERENTIEL_ENDPOINT, referentiel)
r = self.session.get(
url=referentiel_endpoint,
headers=self.get_headers(),
timeout=self.timeout,
proxies=self.proxies,
)
try:
r.raise_for_status()
except Exception as e:
raise e
else:
return r.json()
def search(self, params=None, silent_http_errors=False):
"""
Make job search based on parameters defined in:
https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-offres-demploi-v2/rechercher-par-criteres.html
:param params: The parameters of the search request
:type param: dict
:param silent_http_errors: Silent HTTP errors if True, raise error otherwise. Default is False
:type silent_http_errors: bool
:raises HTTPError: Error when requesting the ressource
:rtype: dict
:returns: A dictionary with three fields:
- 'filtresPossibles', that display the aggregates output
- 'resultats': that is the job offers
- 'Content-Range': the current range index ('first_index' and 'last_index') and the maximum result index ('max_results')
:Example:
>>> params = {}
>>> params.update({"MotsCles": "Ouvrier"})
>>> params.update({"minCreationDate": "2020-01-01T00:00:00Z"})
>>> client.search(params=params)
"""
if self.verbose:
print('Making request with params {}'.format(params))
r = self.session.get(
url=SEARCH_ENDPOINT,
params=params,
headers=self.get_headers(),
timeout=self.timeout,
proxies=self.proxies,
)
try:
r.raise_for_status()
except HTTPError as error:
if r.status_code == 400:
complete_message = str(error) + "\n" + r.json()["message"]
if silent_http_errors:
print(complete_message)
else:
raise HTTPError(complete_message)
else:
if silent_http_errors:
print(str(error))
else:
raise error
else:
found_range = re.search(
pattern="offres (?P<first_index>\d+)-(?P<last_index>\d+)/(?P<max_results>\d+)",
string=r.headers["Content-Range"],
).groupdict()
out = r.json()
out.update({"Content-Range": found_range})
return out |