schoolapp-api 2.0.0__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.
Files changed (38) hide show
  1. schoolapp_api-2.0.0/LICENSE +21 -0
  2. schoolapp_api-2.0.0/PKG-INFO +91 -0
  3. schoolapp_api-2.0.0/README.md +79 -0
  4. schoolapp_api-2.0.0/pyproject.toml +20 -0
  5. schoolapp_api-2.0.0/schoolapp_api/__init__.py +10 -0
  6. schoolapp_api-2.0.0/schoolapp_api/auth.py +81 -0
  7. schoolapp_api-2.0.0/schoolapp_api/constants.py +38 -0
  8. schoolapp_api-2.0.0/schoolapp_api/http_client.py +67 -0
  9. schoolapp_api-2.0.0/schoolapp_api/managers/__init__.py +11 -0
  10. schoolapp_api-2.0.0/schoolapp_api/managers/attendance_manager.py +15 -0
  11. schoolapp_api-2.0.0/schoolapp_api/managers/base_manager.py +36 -0
  12. schoolapp_api-2.0.0/schoolapp_api/managers/course_manager.py +45 -0
  13. schoolapp_api-2.0.0/schoolapp_api/managers/grades_manager.py +35 -0
  14. schoolapp_api-2.0.0/schoolapp_api/managers/profile_manager.py +15 -0
  15. schoolapp_api-2.0.0/schoolapp_api/parsers/__init__.py +6 -0
  16. schoolapp_api-2.0.0/schoolapp_api/parsers/absences.py +45 -0
  17. schoolapp_api-2.0.0/schoolapp_api/parsers/annees.py +37 -0
  18. schoolapp_api-2.0.0/schoolapp_api/parsers/filieres.py +63 -0
  19. schoolapp_api-2.0.0/schoolapp_api/parsers/modules.py +55 -0
  20. schoolapp_api-2.0.0/schoolapp_api/parsers/note_elem.py +33 -0
  21. schoolapp_api-2.0.0/schoolapp_api/parsers/note_mod.py +33 -0
  22. schoolapp_api-2.0.0/schoolapp_api/parsers/profile.py +87 -0
  23. schoolapp_api-2.0.0/schoolapp_api/parsers/sanctions.py +42 -0
  24. schoolapp_api-2.0.0/schoolapp_api/parsers/semestres.py +37 -0
  25. schoolapp_api-2.0.0/schoolapp_api/parsers/stats.py +34 -0
  26. schoolapp_api-2.0.0/schoolapp_api/school_app_client.py +64 -0
  27. schoolapp_api-2.0.0/schoolapp_api/types/__init__.py +9 -0
  28. schoolapp_api-2.0.0/schoolapp_api/types/annee.py +23 -0
  29. schoolapp_api-2.0.0/schoolapp_api/types/base.py +34 -0
  30. schoolapp_api-2.0.0/schoolapp_api/types/element.py +42 -0
  31. schoolapp_api-2.0.0/schoolapp_api/types/module.py +22 -0
  32. schoolapp_api-2.0.0/schoolapp_api/types/semestre.py +24 -0
  33. schoolapp_api-2.0.0/schoolapp_api.egg-info/PKG-INFO +91 -0
  34. schoolapp_api-2.0.0/schoolapp_api.egg-info/SOURCES.txt +36 -0
  35. schoolapp_api-2.0.0/schoolapp_api.egg-info/dependency_links.txt +1 -0
  36. schoolapp_api-2.0.0/schoolapp_api.egg-info/requires.txt +1 -0
  37. schoolapp_api-2.0.0/schoolapp_api.egg-info/top_level.txt +1 -0
  38. schoolapp_api-2.0.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2026] [Aferiad Kamal]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: schoolapp-api
3
+ Version: 2.0.0
4
+ Summary: Professional client for interacting with the School App API
5
+ Author: Aferiad Kamal
6
+ License: MIT
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: beautifulsoup4>=4.9.0
11
+ Dynamic: license-file
12
+
13
+ # School App Client - Refactored
14
+
15
+ Clean, organized structure for the School App client with proper separation of concerns.
16
+
17
+ ## πŸ“ Project Structure
18
+
19
+ ```
20
+ β”œβ”€β”€ http_client.py # Base HTTP client (GET/POST requests, cookie management)
21
+ β”œβ”€β”€ auth.py # Authentication & CSRF token handling
22
+ β”œβ”€β”€ school_app_client.py # Main API client (combines everything)
23
+ └── example_usage.py # Usage example
24
+ ```
25
+
26
+ ## 🎯 Architecture
27
+
28
+ ### 1. **http_client.py** - HTTP Layer
29
+ - Handles raw HTTP operations (GET/POST)
30
+ - Cookie jar and session management
31
+ - Common headers configuration
32
+ - Error handling for network requests
33
+
34
+ ### 2. **auth.py** - Authentication Layer
35
+ - Login flow management
36
+ - CSRF token extraction and updates
37
+ - Session state tracking
38
+ - Decoupled from HTTP implementation
39
+
40
+ ### 3. **school_app_client.py** - API Layer
41
+ - High-level API methods (`get_filieres()`, `get_modules()`)
42
+ - Orchestrates HTTP client and auth manager
43
+ - Business logic for School App endpoints
44
+
45
+ ## πŸš€ Usage
46
+
47
+ ```python
48
+ from school_app_client import SchoolAppClient
49
+
50
+ # Initialize
51
+ client = SchoolAppClient()
52
+
53
+ # Login
54
+ client.login("your.email@example.com", "password")
55
+
56
+ # Fetch data
57
+ filieres = client.get_filieres()
58
+ modules = client.get_modules(niveau="1A", filiere="API-MPT", semestre="S1")
59
+
60
+ # If you encounter 403 errors, force CSRF refresh
61
+ modules = client.get_modules(
62
+ niveau="1A",
63
+ filiere="API-MPT",
64
+ semestre="S1",
65
+ refresh_csrf=True
66
+ )
67
+ ```
68
+
69
+ ## ✨ Benefits of This Structure
70
+
71
+ - **Separation of Concerns**: Each module has a single responsibility
72
+ - **Testability**: Easy to mock and unit test each layer
73
+ - **Maintainability**: Changes in one layer don't affect others
74
+ - **Extensibility**: Easy to add new endpoints or authentication methods
75
+ - **Reusability**: HTTP client can be used for other projects
76
+
77
+ ## 🎯 Key Features
78
+
79
+ - **Automatic CSRF Management**: CSRF tokens are automatically refreshed from page content
80
+ - **Session Persistence**: Cookie-based session management keeps you logged in
81
+ - **Smart Error Handling**: Detects 403 errors and suggests CSRF refresh
82
+ - **Optional CSRF Force Refresh**: Use `refresh_csrf=True` to force token refresh before requests
83
+ - **Clean Separation**: HTTP, Auth, and API layers are completely decoupled
84
+
85
+ ## πŸ”§ Potential Extensions
86
+
87
+ - Add logging module
88
+ - Implement response parsers (HTML β†’ structured data)
89
+ - Add caching layer
90
+ - Create async version using `aiohttp`
91
+ - Add retry logic with exponential backoff
@@ -0,0 +1,79 @@
1
+ # School App Client - Refactored
2
+
3
+ Clean, organized structure for the School App client with proper separation of concerns.
4
+
5
+ ## πŸ“ Project Structure
6
+
7
+ ```
8
+ β”œβ”€β”€ http_client.py # Base HTTP client (GET/POST requests, cookie management)
9
+ β”œβ”€β”€ auth.py # Authentication & CSRF token handling
10
+ β”œβ”€β”€ school_app_client.py # Main API client (combines everything)
11
+ └── example_usage.py # Usage example
12
+ ```
13
+
14
+ ## 🎯 Architecture
15
+
16
+ ### 1. **http_client.py** - HTTP Layer
17
+ - Handles raw HTTP operations (GET/POST)
18
+ - Cookie jar and session management
19
+ - Common headers configuration
20
+ - Error handling for network requests
21
+
22
+ ### 2. **auth.py** - Authentication Layer
23
+ - Login flow management
24
+ - CSRF token extraction and updates
25
+ - Session state tracking
26
+ - Decoupled from HTTP implementation
27
+
28
+ ### 3. **school_app_client.py** - API Layer
29
+ - High-level API methods (`get_filieres()`, `get_modules()`)
30
+ - Orchestrates HTTP client and auth manager
31
+ - Business logic for School App endpoints
32
+
33
+ ## πŸš€ Usage
34
+
35
+ ```python
36
+ from school_app_client import SchoolAppClient
37
+
38
+ # Initialize
39
+ client = SchoolAppClient()
40
+
41
+ # Login
42
+ client.login("your.email@example.com", "password")
43
+
44
+ # Fetch data
45
+ filieres = client.get_filieres()
46
+ modules = client.get_modules(niveau="1A", filiere="API-MPT", semestre="S1")
47
+
48
+ # If you encounter 403 errors, force CSRF refresh
49
+ modules = client.get_modules(
50
+ niveau="1A",
51
+ filiere="API-MPT",
52
+ semestre="S1",
53
+ refresh_csrf=True
54
+ )
55
+ ```
56
+
57
+ ## ✨ Benefits of This Structure
58
+
59
+ - **Separation of Concerns**: Each module has a single responsibility
60
+ - **Testability**: Easy to mock and unit test each layer
61
+ - **Maintainability**: Changes in one layer don't affect others
62
+ - **Extensibility**: Easy to add new endpoints or authentication methods
63
+ - **Reusability**: HTTP client can be used for other projects
64
+
65
+ ## 🎯 Key Features
66
+
67
+ - **Automatic CSRF Management**: CSRF tokens are automatically refreshed from page content
68
+ - **Session Persistence**: Cookie-based session management keeps you logged in
69
+ - **Smart Error Handling**: Detects 403 errors and suggests CSRF refresh
70
+ - **Optional CSRF Force Refresh**: Use `refresh_csrf=True` to force token refresh before requests
71
+ - **Clean Separation**: HTTP, Auth, and API layers are completely decoupled
72
+
73
+ ## πŸ”§ Potential Extensions
74
+
75
+ - Add logging module
76
+ - Implement response parsers (HTML β†’ structured data)
77
+ - Add caching layer
78
+ - Create async version using `aiohttp`
79
+ - Add retry logic with exponential backoff
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "schoolapp-api"
7
+ version = "2.0.0"
8
+ description = "Professional client for interacting with the School App API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Aferiad Kamal"}
14
+ ]
15
+ dependencies = [
16
+ "beautifulsoup4>=4.9.0",
17
+ ]
18
+
19
+ [tool.setuptools]
20
+ packages = ["schoolapp_api", "schoolapp_api.managers", "schoolapp_api.parsers", "schoolapp_api.types"]
@@ -0,0 +1,10 @@
1
+ """
2
+ Professional School App API Client
3
+ """
4
+
5
+ from .school_app_client import SchoolAppClient
6
+ from .http_client import HTTPClient
7
+ from .auth import AuthManager
8
+
9
+ __version__ = "2.0.0"
10
+ __all__ = ["SchoolAppClient", "HTTPClient", "AuthManager"]
@@ -0,0 +1,81 @@
1
+ """
2
+ Authentication and CSRF token management
3
+ """
4
+ import re
5
+ import logging
6
+ from schoolapp_api.constants import LOGIN_URL
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class AuthManager:
11
+ """Handles authentication and CSRF token management"""
12
+
13
+ def __init__(self, http_client, base_url):
14
+ self.http_client = http_client
15
+ self.base_url = base_url
16
+ self.login_url = LOGIN_URL
17
+ self.csrf_token = None
18
+ self.logged_in = False
19
+
20
+ @staticmethod
21
+ def extract_csrf_token(html_content):
22
+ """Extract CSRF token from HTML content"""
23
+ match = re.search(r'name="_csrf"\s+value="([^"]+)"', html_content)
24
+ return match.group(1) if match else None
25
+
26
+ def update_csrf_token(self, html_content):
27
+ """Update CSRF token from HTML content"""
28
+ new_csrf = self.extract_csrf_token(html_content)
29
+ if new_csrf:
30
+ self.csrf_token = new_csrf
31
+ return True
32
+ return False
33
+
34
+ def login(self, email, password):
35
+ """Login and maintain session"""
36
+ if self.logged_in:
37
+ logger.info("Already logged in!")
38
+ return True
39
+
40
+ logger.info("Fetching login page...")
41
+ code, url, content = self.http_client.get(self.login_url)
42
+
43
+ if not content or not self.update_csrf_token(content):
44
+ logger.error("Failed to fetch login page or retrieve CSRF token.")
45
+ return False
46
+
47
+ logger.info(f"Got CSRF token, logging in...")
48
+ login_data = {
49
+ '_csrf': self.csrf_token,
50
+ 'email': email,
51
+ 'password': password
52
+ }
53
+
54
+ code, response_url, content = self.http_client.post(
55
+ self.login_url,
56
+ login_data,
57
+ referer=self.login_url
58
+ )
59
+
60
+ if response_url and ("/login" in response_url and "error" in response_url):
61
+ logger.error("Login failed! Invalid credentials.")
62
+ return False
63
+
64
+ logger.info("Login successful!")
65
+ self.logged_in = True
66
+ return True
67
+
68
+ def is_logged_in(self):
69
+ """Check if currently logged in"""
70
+ return self.logged_in
71
+
72
+ def refresh_csrf_from_url(self, url):
73
+ """Fetch a fresh CSRF token from a specific page"""
74
+ try:
75
+ code, response_url, content = self.http_client.get(url)
76
+ if content and self.update_csrf_token(content):
77
+ return True
78
+ return False
79
+ except Exception as e:
80
+ logger.warning(f"CSRF refresh warning: {e}")
81
+ return False
@@ -0,0 +1,38 @@
1
+ """
2
+ Centralized constants for the School App API
3
+ """
4
+
5
+ BASE_URL = "https://schoolapp.ensam-umi.ac.ma"
6
+
7
+ # Endpoints
8
+ LOGIN_URL = f"{BASE_URL}/login"
9
+ INDEX_URL = f"{BASE_URL}/index"
10
+ MODULES_URL = f"{BASE_URL}/plan-etudes-view/modules"
11
+ FILIERES_URL = f"{BASE_URL}/plan-etudes-view/filieres"
12
+
13
+ # Student Grade Endpoints
14
+ CURRENT_ELEM_URL = f"{BASE_URL}/student/noteselem-encours"
15
+ CURRENT_MOD_URL = f"{BASE_URL}/student/notesmod-encours"
16
+ ELEM_URL = f"{BASE_URL}/student/noteselem"
17
+ MOD_URL = f"{BASE_URL}/student/notesmod"
18
+ ANNEES_URL = f"{BASE_URL}/student/notesannee"
19
+ SEMESTRES_URL = f"{BASE_URL}/student/notessem"
20
+
21
+ # Absence and Sanctions Endpoints
22
+ ABSENCES_URL = f"{BASE_URL}/student/absence/bilan"
23
+ SANCTIONS_URL = f"{BASE_URL}/student/absence/sanctions"
24
+
25
+ # Stats Endpoints
26
+ EVAL_STAT_URL = f"{BASE_URL}/notes-stat/elemevalsat"
27
+ MOD_STAT_URL = f"{BASE_URL}/notes-stat/modsat"
28
+ ANNEE_STAT_URL = f"{BASE_URL}/notes-stat/anneesat"
29
+ SEM_STAT_URL = f"{BASE_URL}/notes-stat/semsat"
30
+
31
+ # Default Headers
32
+ DEFAULT_HEADERS = {
33
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
34
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
35
+ 'Accept-Language': 'en-US,en;q=0.9,fr;q=0.8',
36
+ 'Connection': 'keep-alive',
37
+ 'Upgrade-Insecure-Requests': '1'
38
+ }
@@ -0,0 +1,67 @@
1
+ """
2
+ HTTP Client with session and cookie management
3
+ """
4
+ import urllib.request
5
+ import urllib.parse
6
+ import urllib.error
7
+ import http.cookiejar
8
+ import logging
9
+
10
+ from schoolapp_api.constants import DEFAULT_HEADERS
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class HTTPClient:
15
+ """Base HTTP client for making authenticated requests"""
16
+
17
+ def __init__(self, base_url):
18
+ self.base_url = base_url
19
+ self.cookie_jar = http.cookiejar.CookieJar()
20
+ opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie_jar))
21
+ urllib.request.install_opener(opener)
22
+ self.headers = DEFAULT_HEADERS.copy()
23
+
24
+ def get(self, url, params=None):
25
+ """Perform GET request"""
26
+ try:
27
+ if params:
28
+ query_string = urllib.parse.urlencode(params, doseq=True)
29
+ separator = "&" if "?" in url else "?"
30
+ url = f"{url}{separator}{query_string}"
31
+
32
+ req = urllib.request.Request(url, headers=self.headers, method="GET")
33
+
34
+ with urllib.request.urlopen(req) as response:
35
+ code = response.getcode()
36
+ response_url = response.geturl()
37
+ content = response.read().decode("utf-8")
38
+ return code, response_url, content
39
+
40
+ except urllib.error.HTTPError as e:
41
+ logger.error(f"HTTP Error {e.code}: {e.reason}")
42
+ return e.code, None, None
43
+ except Exception as e:
44
+ logger.error(f"GET Error: {e}")
45
+ return None, None, None
46
+
47
+ def post(self, url, data, referer=None):
48
+ """Perform POST request"""
49
+ try:
50
+ encoded_data = urllib.parse.urlencode(data).encode('utf-8')
51
+ req = urllib.request.Request(url, data=encoded_data, headers=self.headers)
52
+ req.add_header('Content-Type', 'application/x-www-form-urlencoded')
53
+ req.add_header('Origin', self.base_url)
54
+ if referer:
55
+ req.add_header('Referer', referer)
56
+
57
+ with urllib.request.urlopen(req) as response:
58
+ code = response.getcode()
59
+ response_url = response.geturl()
60
+ content = response.read().decode('utf-8')
61
+ return code, response_url, content
62
+ except urllib.error.HTTPError as e:
63
+ logger.error(f"HTTP Error {e.code}: {e.reason}")
64
+ return e.code, None, None
65
+ except Exception as e:
66
+ logger.error(f"POST Error: {e}")
67
+ return None, None, None
@@ -0,0 +1,11 @@
1
+ from .grades_manager import GradesManager
2
+ from .attendance_manager import AttendanceManager
3
+ from .profile_manager import ProfileManager
4
+ from .course_manager import CourseManager
5
+
6
+ __all__ = [
7
+ 'GradesManager',
8
+ 'AttendanceManager',
9
+ 'ProfileManager',
10
+ 'CourseManager'
11
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ Manager for attendance and sanctions
3
+ """
4
+ from schoolapp_api.managers.base_manager import BaseManager
5
+ from schoolapp_api.constants import ABSENCES_URL, SANCTIONS_URL
6
+ from schoolapp_api import parsers
7
+
8
+ class AttendanceManager(BaseManager):
9
+ """Handles fetching of absences and sanctions"""
10
+
11
+ def get_absences(self):
12
+ return self.get_json_or_parse(ABSENCES_URL, parsers.absences)
13
+
14
+ def get_sanctions(self):
15
+ return self.get_json_or_parse(SANCTIONS_URL, parsers.sanctions)
@@ -0,0 +1,36 @@
1
+ """
2
+ Base client for shared manager logic
3
+ """
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class BaseManager:
9
+ """Base class for all managers"""
10
+
11
+ def __init__(self, client):
12
+ self.client = client
13
+ self.http_client = client.http_client
14
+ self.auth = client.auth
15
+
16
+ def ensure_logged_in(self):
17
+ """Ensures the client is logged in before making requests"""
18
+ if not self.auth.is_logged_in():
19
+ logger.error("Not logged in! Call login() first.")
20
+ return False
21
+ return True
22
+
23
+ def get_json_or_parse(self, url, parser, params=None, refresh_csrf=True):
24
+ """Helper to fetch content, update CSRF, and parse"""
25
+ if not self.ensure_logged_in():
26
+ return None
27
+
28
+ code, url, content = self.http_client.get(url, params=params)
29
+
30
+ if not content:
31
+ return None
32
+
33
+ if refresh_csrf:
34
+ self.auth.update_csrf_token(content)
35
+
36
+ return parser.parse(content)
@@ -0,0 +1,45 @@
1
+ """
2
+ Manager for courses and study plans
3
+ """
4
+ import logging
5
+ from schoolapp_api.managers.base_manager import BaseManager
6
+ from schoolapp_api.constants import MODULES_URL
7
+ from schoolapp_api import parsers
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class CourseManager(BaseManager):
12
+ """Handles fetching of modules and study plans"""
13
+
14
+ def get_modules(self, niveau, filiere, semestre, refresh_csrf=False):
15
+ """Fetch modules for specific niveau/filiere/semestre"""
16
+ if not self.ensure_logged_in():
17
+ return None
18
+
19
+ # Always GET modules page first to refresh CSRF token
20
+ logger.info("Opening modules page to refresh state...")
21
+ code, url, content = self.http_client.get(MODULES_URL)
22
+
23
+ if not content:
24
+ return None
25
+
26
+ self.auth.update_csrf_token(content)
27
+
28
+ if refresh_csrf:
29
+ self.auth.refresh_csrf_from_url(MODULES_URL)
30
+
31
+ logger.info(f"Fetching modules for {niveau} {filiere} {semestre}...")
32
+ modules_data = {
33
+ '_csrf': self.auth.csrf_token,
34
+ 'niveau': niveau,
35
+ 'filiere': filiere,
36
+ 'semestre': semestre
37
+ }
38
+
39
+ code, response_url, content = self.http_client.post(
40
+ MODULES_URL,
41
+ modules_data,
42
+ referer=MODULES_URL
43
+ )
44
+
45
+ return parsers.modules.parse(content)
@@ -0,0 +1,35 @@
1
+ """
2
+ Manager for grades and academic results
3
+ """
4
+ from schoolapp_api.managers.base_manager import BaseManager
5
+ from schoolapp_api.constants import (
6
+ CURRENT_ELEM_URL, CURRENT_MOD_URL, ELEM_URL, MOD_URL,
7
+ ANNEES_URL, SEMESTRES_URL
8
+ )
9
+ from schoolapp_api import parsers, types
10
+
11
+ class GradesManager(BaseManager):
12
+ """Handles fetching of grades for elements, modules, years, and semesters"""
13
+
14
+ def get_element_notes(self, current=False):
15
+ url = CURRENT_ELEM_URL if current else ELEM_URL
16
+ data = self.get_json_or_parse(url, parsers.note_elem)
17
+ return [types.Element(self.client, item) for item in data] if data else []
18
+
19
+ def get_module_notes(self, current=False):
20
+ url = CURRENT_MOD_URL if current else MOD_URL
21
+ data = self.get_json_or_parse(url, parsers.note_mod)
22
+ # Note: In the original code, get_mod_note returned types.Element,
23
+ # but it should probably return types.Module if it's for modules.
24
+ # Looking at original school_app_client.py:L272 it used types.Element.
25
+ # I'll stick to what was there but maybe use Module if appropriate.
26
+ # types/Module.py exists.
27
+ return [types.Module(self.client, item) for item in data] if data else []
28
+
29
+ def get_years(self):
30
+ data = self.get_json_or_parse(ANNEES_URL, parsers.annees)
31
+ return [types.Annee(self.client, item) for item in data] if data else []
32
+
33
+ def get_semesters(self):
34
+ data = self.get_json_or_parse(SEMESTRES_URL, parsers.semestres)
35
+ return [types.Semestre(self.client, item) for item in data] if data else []
@@ -0,0 +1,15 @@
1
+ """
2
+ Manager for student profile and programs
3
+ """
4
+ from schoolapp_api.managers.base_manager import BaseManager
5
+ from schoolapp_api.constants import INDEX_URL, FILIERES_URL
6
+ from schoolapp_api import parsers
7
+
8
+ class ProfileManager(BaseManager):
9
+ """Handles fetching of student profile and filieres"""
10
+
11
+ def get_profile(self):
12
+ return self.get_json_or_parse(INDEX_URL, parsers.profile)
13
+
14
+ def get_filieres(self):
15
+ return self.get_json_or_parse(FILIERES_URL, parsers.filieres)
@@ -0,0 +1,6 @@
1
+ """
2
+ Parsers for School App API HTML content
3
+ """
4
+ from . import absences, annees, filieres, modules, note_elem, note_mod, profile, sanctions, semestres, stats
5
+
6
+ __all__ = ["absences", "annees", "filieres", "modules", "note_elem", "note_mod", "profile", "sanctions", "semestres", "stats"]
@@ -0,0 +1,45 @@
1
+ from bs4 import BeautifulSoup
2
+ import re
3
+
4
+ def parse(html_content):
5
+ """Parse HTML content containing absence details."""
6
+ soup = BeautifulSoup(html_content, 'html.parser')
7
+ result = {"summary": [], "details": []}
8
+
9
+ tables = soup.find_all('table', class_='table table-striped table-sm')
10
+ if len(tables) < 2:
11
+ return result
12
+
13
+ # Summary
14
+ for row in tables[0].find_all('tr')[1:]:
15
+ cells = row.find_all('td')
16
+ if len(cells) >= 4:
17
+ result["summary"].append({
18
+ "CodeElem": cells[0].get_text(strip=True),
19
+ "Intitule": cells[1].get_text(strip=True),
20
+ "Non_Justifiee": int(cells[2].get_text(strip=True) or 0),
21
+ "Justifiee": int(cells[3].get_text(strip=True) or 0)
22
+ })
23
+
24
+ # Details
25
+ for row in tables[1].find_all('tr')[1:]:
26
+ cells = row.find_all('td')
27
+ if len(cells) >= 5:
28
+ result["details"].append({
29
+ "Element": cells[0].get_text(strip=True),
30
+ "Date": cells[1].get_text(strip=True),
31
+ "Seance": cells[2].get_text(strip=True),
32
+ "Justif": cells[3].get_text(strip=True).lower() == "true",
33
+ "Remarques": cells[4].get_text(strip=True)
34
+ })
35
+
36
+ # Semester info
37
+ alerts = soup.find_all('div', class_='alert alert-info')
38
+ for alert in alerts:
39
+ text = alert.get_text()
40
+ if "semestre" in text.lower():
41
+ match = re.search(r'S\d', text)
42
+ if match:
43
+ result["semestre"] = match.group()
44
+
45
+ return result
@@ -0,0 +1,37 @@
1
+ from bs4 import BeautifulSoup
2
+
3
+ def parse(html_content):
4
+ """Parse HTML content containing annual averages."""
5
+ soup = BeautifulSoup(html_content, 'html.parser')
6
+ table = soup.find('table', class_='table table-striped table-sm') or soup.find('table')
7
+
8
+ if not table:
9
+ return []
10
+
11
+ headers = [th.get_text(strip=True) for th in table.find_all('th')]
12
+ if len(headers) < 9:
13
+ headers = ["Niveau", "Filiere", "AU", "Statut", "Moy AnnΓ©e", "PJ", "Decision", "Classement", "Releve de Notes"]
14
+
15
+ data = []
16
+ tbody = table.find('tbody') or table
17
+
18
+ for row in tbody.find_all('tr'):
19
+ cells = row.find_all('td')
20
+ if len(cells) >= 9:
21
+ row_data = {}
22
+ for i, cell in enumerate(cells[:9]):
23
+ if i == 8: # Download link
24
+ link = cell.find('a')
25
+ row_data[headers[i]] = link.get('href') if link else cell.get_text(strip=True)
26
+ else:
27
+ val = cell.get_text(strip=True)
28
+ if headers[i] in ["Moy AnnΓ©e", "PJ"]:
29
+ try:
30
+ row_data[headers[i]] = float(val) if val and val != "--" else None
31
+ except ValueError:
32
+ row_data[headers[i]] = val
33
+ else:
34
+ row_data[headers[i]] = val
35
+ data.append(row_data)
36
+
37
+ return data