mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			146 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			146 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Set of functions to interact with the UTBM UV api."""
 | 
						|
 | 
						|
from typing import Iterator
 | 
						|
 | 
						|
import requests
 | 
						|
from django.conf import settings
 | 
						|
from django.utils.functional import cached_property
 | 
						|
 | 
						|
from pedagogy.schemas import ShortUvList, UtbmFullUvSchema, UtbmShortUvSchema, UvSchema
 | 
						|
 | 
						|
 | 
						|
class UtbmApiClient(requests.Session):
 | 
						|
    """A wrapper around `requests.Session` to perform requests to the UTBM UV API."""
 | 
						|
 | 
						|
    BASE_URL = settings.SITH_PEDAGOGY_UTBM_API
 | 
						|
    _cache = {"short_uvs": {}}
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def current_year(self) -> int:
 | 
						|
        """Fetch from the API the latest existing year"""
 | 
						|
        url = f"{self.BASE_URL}/guides/fr"
 | 
						|
        response = self.get(url)
 | 
						|
        return response.json()[-1]["annee"]
 | 
						|
 | 
						|
    def fetch_short_uvs(
 | 
						|
        self, lang: str = "fr", year: int | None = None
 | 
						|
    ) -> list[UtbmShortUvSchema]:
 | 
						|
        """Get the list of UVs in their short format from the UTBM API"""
 | 
						|
        if year is None:
 | 
						|
            year = self.current_year
 | 
						|
        if lang not in self._cache["short_uvs"]:
 | 
						|
            self._cache["short_uvs"][lang] = {}
 | 
						|
        if year not in self._cache["short_uvs"][lang]:
 | 
						|
            url = f"{self.BASE_URL}/uvs/{lang}/{year}"
 | 
						|
            response = self.get(url)
 | 
						|
            uvs = ShortUvList.validate_json(response.content)
 | 
						|
            self._cache["short_uvs"][lang][year] = uvs
 | 
						|
        return self._cache["short_uvs"][lang][year]
 | 
						|
 | 
						|
    def fetch_uvs(
 | 
						|
        self, lang: str = "fr", year: int | None = None
 | 
						|
    ) -> Iterator[UvSchema]:
 | 
						|
        """Fetch all UVs from the UTBM API, parsed in a format that we can use.
 | 
						|
 | 
						|
        Warning:
 | 
						|
            We need infos from the full uv schema, and the UTBM UV API
 | 
						|
            has no route to get all of them at once.
 | 
						|
            We must do one request per UV (for a total of around 730 UVs),
 | 
						|
            which takes a lot of time.
 | 
						|
            Hopefully, there seems to be no rate-limit, so an error
 | 
						|
            in the middle of the process isn't likely to occur.
 | 
						|
        """
 | 
						|
        if year is None:
 | 
						|
            year = self.current_year
 | 
						|
        shorts_uvs = self.fetch_short_uvs(lang, year)
 | 
						|
        # When UVs are common to multiple branches (like most HUMA)
 | 
						|
        # the UTBM API duplicates them for every branch.
 | 
						|
        # We have no way in our db to link a UV to multiple formations,
 | 
						|
        # so we just create a single UV, which formation is the one
 | 
						|
        # of the first UV found in the list.
 | 
						|
        # For example, if we have CC01 (TC), CC01 (IMSI) and CC01 (EDIM),
 | 
						|
        # we will only keep CC01 (TC).
 | 
						|
        unique_short_uvs = {}
 | 
						|
        for uv in shorts_uvs:
 | 
						|
            if uv.code not in unique_short_uvs:
 | 
						|
                unique_short_uvs[uv.code] = uv
 | 
						|
        for uv in unique_short_uvs.values():
 | 
						|
            uv_url = f"{self.BASE_URL}/uv/{lang}/{year}/{uv.code}/{uv.code_formation}"
 | 
						|
            response = requests.get(uv_url)
 | 
						|
            full_uv = UtbmFullUvSchema.model_validate_json(response.content)
 | 
						|
            yield make_clean_uv(uv, full_uv)
 | 
						|
 | 
						|
    def find_uv(self, lang: str, code: str, year: int | None = None) -> UvSchema | None:
 | 
						|
        """Find an UV from the UTBM API."""
 | 
						|
        # query the UV list
 | 
						|
        if not year:
 | 
						|
            year = self.current_year
 | 
						|
        # the UTBM API has no way to fetch a single short uv,
 | 
						|
        # and short uvs contain infos that we need and are not
 | 
						|
        # in the full uv schema, so we must fetch everything.
 | 
						|
        short_uvs = self.fetch_short_uvs(lang, year)
 | 
						|
        short_uv = next((uv for uv in short_uvs if uv.code == code), None)
 | 
						|
        if short_uv is None:
 | 
						|
            return None
 | 
						|
 | 
						|
        # get detailed information about the UV
 | 
						|
        uv_url = f"{self.BASE_URL}/uv/{lang}/{year}/{code}/{short_uv.code_formation}"
 | 
						|
        response = requests.get(uv_url)
 | 
						|
        full_uv = UtbmFullUvSchema.model_validate_json(response.content)
 | 
						|
        return make_clean_uv(short_uv, full_uv)
 | 
						|
 | 
						|
 | 
						|
def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvSchema:
 | 
						|
    """Cleans the data up so that it corresponds to our data representation.
 | 
						|
 | 
						|
    Some of the needed information are in the short uv schema, some
 | 
						|
    other in the full uv schema.
 | 
						|
    Thus we combine those information to obtain a data schema suitable
 | 
						|
    for our needs.
 | 
						|
    """
 | 
						|
    if full_uv.departement == "Pôle Humanités":
 | 
						|
        department = "HUMA"
 | 
						|
    else:
 | 
						|
        department = {
 | 
						|
            "AL": "IMSI",
 | 
						|
            "AE": "EE",
 | 
						|
            "GI": "GI",
 | 
						|
            "GC": "EE",
 | 
						|
            "GM": "MC",
 | 
						|
            "TC": "TC",
 | 
						|
            "GP": "IMSI",
 | 
						|
            "ED": "EDIM",
 | 
						|
            "AI": "GI",
 | 
						|
            "AM": "MC",
 | 
						|
        }.get(short_uv.code_formation, "NA")
 | 
						|
 | 
						|
    match short_uv.ouvert_printemps, short_uv.ouvert_automne:
 | 
						|
        case True, True:
 | 
						|
            semester = "AUTUMN_AND_SPRING"
 | 
						|
        case True, False:
 | 
						|
            semester = "SPRING"
 | 
						|
        case False, True:
 | 
						|
            semester = "AUTUMN"
 | 
						|
        case _:
 | 
						|
            semester = "CLOSED"
 | 
						|
 | 
						|
    return UvSchema(
 | 
						|
        title=full_uv.libelle or "",
 | 
						|
        code=full_uv.code,
 | 
						|
        credit_type=short_uv.code_categorie or "FREE",
 | 
						|
        semester=semester,
 | 
						|
        language=short_uv.code_langue.upper(),
 | 
						|
        credits=full_uv.credits_ects,
 | 
						|
        department=department,
 | 
						|
        hours_THE=next((i.nbh for i in full_uv.activites if i.code == "THE"), 0) // 60,
 | 
						|
        hours_TD=next((i.nbh for i in full_uv.activites if i.code == "TD"), 0) // 60,
 | 
						|
        hours_TP=next((i.nbh for i in full_uv.activites if i.code == "TP"), 0) // 60,
 | 
						|
        hours_TE=next((i.nbh for i in full_uv.activites if i.code == "TE"), 0) // 60,
 | 
						|
        hours_CM=next((i.nbh for i in full_uv.activites if i.code == "CM"), 0) // 60,
 | 
						|
        manager=full_uv.respo_automne or full_uv.respo_printemps or "",
 | 
						|
        objectives=full_uv.objectifs or "",
 | 
						|
        program=full_uv.programme or "",
 | 
						|
        skills=full_uv.acquisition_competences or "",
 | 
						|
        key_concepts=full_uv.acquisition_notions or "",
 | 
						|
    )
 |