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.
- unibo_toolkit-0.4.0/LICENSE +21 -0
- unibo_toolkit-0.4.0/PKG-INFO +18 -0
- unibo_toolkit-0.4.0/README.md +3 -0
- unibo_toolkit-0.4.0/pyproject.toml +53 -0
- unibo_toolkit-0.4.0/setup.cfg +4 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/__init__.py +38 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/clients/__init__.py +5 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/clients/http.py +93 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/__init__.py +9 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/access_type.py +10 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/area.py +47 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/campus.py +13 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/course_type.py +11 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/enums/language.py +11 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/exceptions.py +100 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/logging.py +90 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/models/__init__.py +30 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/models/area_info.py +20 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/models/course.py +446 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/models/curriculum.py +52 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/models/timetable.py +749 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/__init__.py +7 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/course.py +561 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/subjects.py +199 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/scrapers/timetable.py +375 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/__init__.py +23 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/custom_logger.py +154 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/date_utils.py +132 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/parsers.py +322 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/subjects_parser.py +170 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/timetable_filters.py +196 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit/utils/timetable_parser.py +198 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/PKG-INFO +18 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/SOURCES.txt +35 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/dependency_links.txt +1 -0
- unibo_toolkit-0.4.0/src/unibo_toolkit.egg-info/requires.txt +4 -0
- 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,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,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,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,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,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
|