unibo-toolkit 0.4.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 (37) hide show
  1. unibo_toolkit-0.4.0/LICENSE +21 -0
  2. unibo_toolkit-0.4.0/PKG-INFO +18 -0
  3. unibo_toolkit-0.4.0/README.md +3 -0
  4. unibo_toolkit-0.4.0/pyproject.toml +53 -0
  5. unibo_toolkit-0.4.0/setup.cfg +4 -0
  6. unibo_toolkit-0.4.0/src/unibo_toolkit/__init__.py +38 -0
  7. unibo_toolkit-0.4.0/src/unibo_toolkit/clients/__init__.py +5 -0
  8. unibo_toolkit-0.4.0/src/unibo_toolkit/clients/http.py +93 -0
  9. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/__init__.py +9 -0
  10. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/access_type.py +10 -0
  11. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/area.py +47 -0
  12. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/campus.py +13 -0
  13. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/course_type.py +11 -0
  14. unibo_toolkit-0.4.0/src/unibo_toolkit/enums/language.py +11 -0
  15. unibo_toolkit-0.4.0/src/unibo_toolkit/exceptions.py +100 -0
  16. unibo_toolkit-0.4.0/src/unibo_toolkit/logging.py +90 -0
  17. unibo_toolkit-0.4.0/src/unibo_toolkit/models/__init__.py +30 -0
  18. unibo_toolkit-0.4.0/src/unibo_toolkit/models/area_info.py +20 -0
  19. unibo_toolkit-0.4.0/src/unibo_toolkit/models/course.py +446 -0
  20. unibo_toolkit-0.4.0/src/unibo_toolkit/models/curriculum.py +52 -0
  21. unibo_toolkit-0.4.0/src/unibo_toolkit/models/timetable.py +749 -0
  22. unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/__init__.py +7 -0
  23. unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/course.py +561 -0
  24. unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/subjects.py +199 -0
  25. unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/timetable.py +375 -0
  26. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/__init__.py +23 -0
  27. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/custom_logger.py +154 -0
  28. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/date_utils.py +132 -0
  29. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/parsers.py +322 -0
  30. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/subjects_parser.py +170 -0
  31. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/timetable_filters.py +196 -0
  32. unibo_toolkit-0.4.0/src/unibo_toolkit/utils/timetable_parser.py +198 -0
  33. unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/PKG-INFO +18 -0
  34. unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/SOURCES.txt +35 -0
  35. unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/dependency_links.txt +1 -0
  36. unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/requires.txt +4 -0
  37. unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vadym Teliatnyk
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,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: unibo-toolkit
3
+ Version: 0.4.0
4
+ Summary: Python toolkit for scraping and accessing University of Bologna data
5
+ Author-email: Vadym Teliatnyk <vadym.teliatnyk@komo4ek.me>
6
+ License: MIT
7
+ Requires-Python: <4.0,>=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: beautifulsoup4<5.0.0,>=4.9.0
11
+ Requires-Dist: aiohttp<4.0.0,>=3.8.0
12
+ Requires-Dist: lxml<6.0.0,>=4.9.0
13
+ Requires-Dist: python-dotenv<2.0.0,>=1.0.0
14
+ Dynamic: license-file
15
+
16
+ # UniBo Toolkit
17
+
18
+ Python toolkit for University of Bologna data scraping.
@@ -0,0 +1,3 @@
1
+ # UniBo Toolkit
2
+
3
+ Python toolkit for University of Bologna data scraping.
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "unibo-toolkit"
3
+ version = "0.4.0"
4
+ description = "Python toolkit for scraping and accessing University of Bologna data"
5
+ authors = [
6
+ {name = "Vadym Teliatnyk", email = "vadym.teliatnyk@komo4ek.me"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.8,<4.0"
11
+ dependencies = [
12
+ "beautifulsoup4>=4.9.0,<5.0.0",
13
+ "aiohttp>=3.8.0,<4.0.0",
14
+ "lxml>=4.9.0,<6.0.0",
15
+ "python-dotenv>=1.0.0,<2.0.0"
16
+ ]
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "black[d]>=24.0.0,<26.0.0",
21
+ "pytest>=7.0.0,<9.0.0",
22
+ "pytest-asyncio>=0.21.0,<1.0.0",
23
+ "mypy>=1.0.0,<2.0.0"
24
+ ]
25
+
26
+ [tool.black]
27
+ line-length = 100
28
+ target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313']
29
+ include = '\.pyi?$'
30
+ extend-exclude = '''
31
+ /(
32
+ # directories
33
+ \.eggs
34
+ | \.git
35
+ | \.hg
36
+ | \.mypy_cache
37
+ | \.tox
38
+ | \.venv
39
+ | _build
40
+ | buck-out
41
+ | build
42
+ | dist
43
+ | tests/research
44
+ | \.github
45
+ )/
46
+ '''
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [build-system]
52
+ requires = ["setuptools>=61.0", "wheel"]
53
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """UniBo Toolkit - Python library for University of Bologna data scraping."""
2
+
3
+ from unibo_toolkit.clients import HTTPClient
4
+ from unibo_toolkit.enums import AccessType, Area, Campus, CourseType, Language
5
+ from unibo_toolkit.exceptions import (
6
+ CourseNotFoundError,
7
+ InvalidAreaError,
8
+ ScraperError,
9
+ UniboToolkitError,
10
+ UnsupportedLanguageError,
11
+ )
12
+ from unibo_toolkit.logging import setup_logging
13
+ from unibo_toolkit.models import AreaInfo, Bachelor, BaseCourse, Master, SingleCycleMaster
14
+ from unibo_toolkit.scrapers import CourseScraper
15
+
16
+ __version__ = "0.4.0"
17
+
18
+ __all__ = [
19
+ "__version__",
20
+ "HTTPClient",
21
+ "AccessType",
22
+ "Area",
23
+ "Campus",
24
+ "CourseType",
25
+ "Language",
26
+ "AreaInfo",
27
+ "Bachelor",
28
+ "BaseCourse",
29
+ "Master",
30
+ "SingleCycleMaster",
31
+ "CourseScraper",
32
+ "setup_logging",
33
+ "UniboToolkitError",
34
+ "UnsupportedLanguageError",
35
+ "CourseNotFoundError",
36
+ "InvalidAreaError",
37
+ "ScraperError",
38
+ ]
@@ -0,0 +1,5 @@
1
+ """HTTP clients for UniBo toolkit."""
2
+
3
+ from unibo_toolkit.clients.http import HTTPClient
4
+
5
+ __all__ = ["HTTPClient"]
@@ -0,0 +1,93 @@
1
+ """HTTP client for making requests to UniBo website."""
2
+
3
+ from typing import Any, Dict, Optional
4
+ import aiohttp
5
+
6
+
7
+ class HTTPClient:
8
+ """Async HTTP client for UniBo website requests.
9
+
10
+ Handles all HTTP communication with proper error handling,
11
+ timeouts, and user agent configuration.
12
+ """
13
+
14
+ DEFAULT_HEADERS = {
15
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
16
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
17
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ "Accept-Language": "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7",
19
+ }
20
+
21
+ def __init__(
22
+ self,
23
+ timeout: int = 30,
24
+ headers: Optional[Dict[str, str]] = None,
25
+ ):
26
+ """Initialize HTTP client.
27
+
28
+ Args:
29
+ timeout: Request timeout in seconds
30
+ headers: Optional custom headers to merge with defaults
31
+ """
32
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
33
+ self.headers = {**self.DEFAULT_HEADERS, **(headers or {})}
34
+ self._session: Optional[aiohttp.ClientSession] = None
35
+
36
+ async def __aenter__(self):
37
+ """Async context manager entry."""
38
+ self._session = aiohttp.ClientSession(
39
+ timeout=self.timeout,
40
+ headers=self.headers,
41
+ )
42
+ return self
43
+
44
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
45
+ """Async context manager exit."""
46
+ if self._session:
47
+ await self._session.close()
48
+ self._session = None
49
+
50
+ async def get(self, url: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> str:
51
+ """Perform GET request and return response text.
52
+
53
+ Args:
54
+ url: Target URL
55
+ params: Query parameters
56
+ **kwargs: Additional arguments for aiohttp request
57
+
58
+ Returns:
59
+ Response text content
60
+
61
+ Raises:
62
+ aiohttp.ClientError: On network or HTTP errors
63
+ """
64
+ if not self._session:
65
+ raise RuntimeError("HTTPClient must be used as async context manager")
66
+
67
+ async with self._session.get(url, params=params, **kwargs) as response:
68
+ response.raise_for_status()
69
+ return await response.text()
70
+
71
+ async def post(
72
+ self, url: str, data: Optional[Any] = None, json: Optional[Dict[str, Any]] = None, **kwargs
73
+ ) -> str:
74
+ """Perform POST request and return response text.
75
+
76
+ Args:
77
+ url: Target URL
78
+ data: Form data
79
+ json: JSON payload
80
+ **kwargs: Additional arguments for aiohttp request
81
+
82
+ Returns:
83
+ Response text content
84
+
85
+ Raises:
86
+ aiohttp.ClientError: On network or HTTP errors
87
+ """
88
+ if not self._session:
89
+ raise RuntimeError("HTTPClient must be used as async context manager")
90
+
91
+ async with self._session.post(url, data=data, json=json, **kwargs) as response:
92
+ response.raise_for_status()
93
+ return await response.text()
@@ -0,0 +1,9 @@
1
+ """Enumerations for UniBo toolkit."""
2
+
3
+ from unibo_toolkit.enums.access_type import AccessType
4
+ from unibo_toolkit.enums.area import Area
5
+ from unibo_toolkit.enums.campus import Campus
6
+ from unibo_toolkit.enums.course_type import CourseType
7
+ from unibo_toolkit.enums.language import Language
8
+
9
+ __all__ = ["AccessType", "Area", "Campus", "CourseType", "Language"]
@@ -0,0 +1,10 @@
1
+ """Access type enumeration."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class AccessType(Enum):
7
+ """Enrollment access type for courses."""
8
+
9
+ OPEN = "libero"
10
+ LIMITED = "programmato"
@@ -0,0 +1,47 @@
1
+ """Academic area enumeration."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Area(Enum):
7
+ """Academic areas/fields at UniBo.
8
+
9
+ Each area represents a broad category of study programs
10
+ with a unique identifier used in the UniBo website.
11
+ """
12
+
13
+ ECONOMIA_MANAGEMENT = (1, "Economia e management")
14
+ FARMACIA_BIOTECNOLOGIE = (2, "Farmacia e biotecnologie")
15
+ GIURISPRUDENZA = (3, "Giurisprudenza")
16
+ INGEGNERIA_ARCHITETTURA = (4, "Ingegneria e architettura")
17
+ LINGUE_LETTERATURE = (5, "Lingue e Letterature, Traduzione e Interpretazione")
18
+ MEDICINA_CHIRURGIA = (6, "Medicina e Chirurgia")
19
+ MEDICINA_VETERINARIA = (7, "Medicina veterinaria")
20
+ PSICOLOGIA = (8, "Psicologia")
21
+ SCIENZE = (9, "Scienze")
22
+ SCIENZE_AGRO_ALIMENTARI = (10, "Scienze agro-alimentari")
23
+ SCIENZE_EDUCAZIONE = (11, "Scienze dell'educazione e della formazione")
24
+ SCIENZE_MOTORIE = (12, "Scienze motorie")
25
+ SCIENZE_POLITICHE = (13, "Scienze politiche")
26
+ SCIENZE_STATISTICHE = (14, "Scienze Statistiche")
27
+ SOCIOLOGIA = (15, "Sociologia")
28
+ STUDI_UMANISTICI = (16, "Studi umanistici")
29
+
30
+ def __init__(self, area_id: int, title_it: str):
31
+ self.area_id = area_id
32
+ self.title_it = title_it
33
+
34
+ @classmethod
35
+ def from_id(cls, area_id: int):
36
+ """Get Area by ID.
37
+
38
+ Args:
39
+ area_id: Area identifier (1-16)
40
+
41
+ Returns:
42
+ Area enum value or None if not found
43
+ """
44
+ for area in cls:
45
+ if area.area_id == area_id:
46
+ return area
47
+ return None
@@ -0,0 +1,13 @@
1
+ """Campus location enumeration."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Campus(Enum):
7
+ """UniBo campus locations."""
8
+
9
+ BOLOGNA = "bologna"
10
+ CESENA = "cesena"
11
+ FORLI = "forli"
12
+ RAVENNA = "ravenna"
13
+ RIMINI = "rimini"
@@ -0,0 +1,11 @@
1
+ """Course type enumeration."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class CourseType(Enum):
7
+ """Types of degree courses available at UniBo."""
8
+
9
+ BACHELOR = "bachelor"
10
+ MASTER = "master"
11
+ SINGLE_CYCLE_MASTER = "single_cycle_master"
@@ -0,0 +1,11 @@
1
+ """Language enumeration for UniBo courses."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Language(Enum):
7
+ """Language of instruction for courses."""
8
+
9
+ IT = "it"
10
+ EN = "en"
11
+ FR = "fr"
@@ -0,0 +1,100 @@
1
+ """Custom exceptions for UniBo Toolkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+
8
+ class UniboToolkitError(Exception):
9
+ """Base exception for all UniBo Toolkit errors."""
10
+
11
+ pass
12
+
13
+
14
+ class UnsupportedLanguageError(UniboToolkitError):
15
+ """Raised when an unsupported language is provided.
16
+
17
+ The UniBo website only supports Italian (IT) and English (EN) languages.
18
+ Other languages like French (FR) are not available.
19
+
20
+ Attributes:
21
+ language: The unsupported language that was provided
22
+ supported_languages: List of supported language codes
23
+ """
24
+
25
+ def __init__(self, language: str, supported_languages: List[str] = None):
26
+ """Initialize the exception.
27
+
28
+ Args:
29
+ language: The unsupported language code
30
+ supported_languages: Optional list of supported languages
31
+ """
32
+ self.language = language
33
+ self.supported_languages = supported_languages or ["IT", "EN"]
34
+
35
+ message = (
36
+ f"Unsupported language: '{language}'. "
37
+ f"Supported languages are: {', '.join(self.supported_languages)}"
38
+ )
39
+ super().__init__(message)
40
+
41
+
42
+ class CourseNotFoundError(UniboToolkitError):
43
+ """Raised when a requested course cannot be found.
44
+
45
+ Attributes:
46
+ course_id: The ID of the course that was not found
47
+ """
48
+
49
+ def __init__(self, course_id: int):
50
+ """Initialize the exception.
51
+
52
+ Args:
53
+ course_id: The ID of the course that was not found
54
+ """
55
+ self.course_id = course_id
56
+ message = f"Course with ID {course_id} not found"
57
+ super().__init__(message)
58
+
59
+
60
+ class InvalidAreaError(UniboToolkitError):
61
+ """Raised when an invalid academic area is provided.
62
+
63
+ Attributes:
64
+ area_id: The ID of the invalid area
65
+ """
66
+
67
+ def __init__(self, area_id: int):
68
+ """Initialize the exception.
69
+
70
+ Args:
71
+ area_id: The ID of the invalid area
72
+ """
73
+ self.area_id = area_id
74
+ message = f"Invalid area ID: {area_id}"
75
+ super().__init__(message)
76
+
77
+
78
+ class ScraperError(UniboToolkitError):
79
+ """Raised when scraping operations fail.
80
+
81
+ Attributes:
82
+ url: The URL that failed to be scraped
83
+ reason: The reason for the failure
84
+ """
85
+
86
+ def __init__(self, url: str, reason: str = None):
87
+ """Initialize the exception.
88
+
89
+ Args:
90
+ url: The URL that failed
91
+ reason: Optional reason for the failure
92
+ """
93
+ self.url = url
94
+ self.reason = reason
95
+
96
+ message = f"Failed to scrape {url}"
97
+ if reason:
98
+ message += f": {reason}"
99
+
100
+ super().__init__(message)
@@ -0,0 +1,90 @@
1
+ """Logging configuration for unibo_toolkit library."""
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ from unibo_toolkit.utils.custom_logger import CustomLogger
7
+
8
+ _LOGGER_NAME = "UNIBO_TOOLKIT"
9
+ _logger_configured = False
10
+
11
+
12
+ def setup_logging(
13
+ level: int = logging.INFO,
14
+ format_string: Optional[str] = None,
15
+ handler: Optional[logging.Handler] = None,
16
+ ) -> None:
17
+ """Configure logging for unibo_toolkit library.
18
+
19
+ By default, the library uses a NullHandler (no output). Call this function
20
+ to enable logging to see debug information, warnings, and errors.
21
+
22
+ Args:
23
+ level: Log level - one of: DEBUG, INFO, WARNING, ERROR, CRITICAL
24
+ format_string: Custom log format string. If None, uses structured format
25
+ handler: Custom logging handler. If None, uses StreamHandler (console output)
26
+
27
+ Example:
28
+ Enable INFO level logging to console:
29
+ >>> import unibo_toolkit
30
+ >>> unibo_toolkit.setup_logging(level=logging.INFO)
31
+
32
+ Enable DEBUG level with custom format:
33
+ >>> unibo_toolkit.setup_logging(
34
+ ... level=logging.DEBUG,
35
+ ... format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
36
+ ... )
37
+
38
+ Use custom handler (e.g., file logging):
39
+ >>> import logging
40
+ >>> file_handler = logging.FileHandler('unibo_toolkit.log')
41
+ >>> unibo_toolkit.setup_logging(level=logging.DEBUG, handler=file_handler)
42
+ """
43
+ global _logger_configured
44
+
45
+ logger = logging.getLogger(_LOGGER_NAME)
46
+ logger.setLevel(level)
47
+
48
+ # Remove existing handlers to avoid duplicates
49
+ logger.handlers.clear()
50
+
51
+ # Create handler if not provided
52
+ if handler is None:
53
+ handler = logging.StreamHandler()
54
+
55
+ # Set format - default to structured format for CustomLogger
56
+ if format_string is None:
57
+ format_string = "[%(levelname)s] %(name)s - %(message)s"
58
+
59
+ formatter = logging.Formatter(format_string)
60
+ handler.setFormatter(formatter)
61
+
62
+ logger.addHandler(handler)
63
+ _logger_configured = True
64
+
65
+
66
+ def get_logger(name: str, **items: Any) -> CustomLogger:
67
+ """Get custom logger instance for a module.
68
+
69
+ This function returns a CustomLogger that formats messages as
70
+ structured logs with key-value pairs.
71
+
72
+ Args:
73
+ name: Module name (usually __name__)
74
+ **items: Default items to include in all log messages
75
+
76
+ Returns:
77
+ CustomLogger instance
78
+
79
+ Example:
80
+ >>> logger = get_logger(__name__)
81
+ >>> logger.info("Course fetched", course_id=6796)
82
+ # Output: msg="Course fetched" course_id="6796"
83
+ """
84
+ return CustomLogger(f"{_LOGGER_NAME}.{name}", **items)
85
+
86
+
87
+ # Initialize default logger with NullHandler (no output by default)
88
+ _default_logger = logging.getLogger(_LOGGER_NAME)
89
+ _default_logger.addHandler(logging.NullHandler())
90
+ _default_logger.setLevel(logging.WARNING)
@@ -0,0 +1,30 @@
1
+ """Data models for UniBo courses."""
2
+
3
+ from unibo_toolkit.models.area_info import AreaInfo
4
+ from unibo_toolkit.models.course import Bachelor, BaseCourse, Master, SingleCycleMaster
5
+ from unibo_toolkit.models.curriculum import Curriculum
6
+ from unibo_toolkit.models.timetable import (
7
+ AcademicYearTimetable,
8
+ Classroom,
9
+ CurriculumTimetable,
10
+ Subject,
11
+ Timetable,
12
+ TimetableCollection,
13
+ TimetableEvent,
14
+ )
15
+
16
+ __all__ = [
17
+ "AcademicYearTimetable",
18
+ "AreaInfo",
19
+ "Bachelor",
20
+ "BaseCourse",
21
+ "Classroom",
22
+ "Curriculum",
23
+ "CurriculumTimetable",
24
+ "Master",
25
+ "SingleCycleMaster",
26
+ "Subject",
27
+ "Timetable",
28
+ "TimetableCollection",
29
+ "TimetableEvent",
30
+ ]
@@ -0,0 +1,20 @@
1
+ """Area information model."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from unibo_toolkit.enums import Area, CourseType
6
+
7
+
8
+ @dataclass
9
+ class AreaInfo:
10
+ """Information about an academic area.
11
+
12
+ Attributes:
13
+ area: Academic area enum
14
+ course_count: Number of courses in this area
15
+ course_type: Type of courses (Bachelor/Master/SingleCycleMaster)
16
+ """
17
+
18
+ area: Area
19
+ course_count: int
20
+ course_type: CourseType