├── .python-version
├── app
├── resources
│ ├── .gitignore
│ ├── migrate.sql
│ ├── lecturetube_availability.csv
│ ├── generate_rooms.py
│ ├── generate_lecturetube_availability.py
│ ├── shorthands.csv
│ └── generate_courses.py
├── static
│ ├── logo32.png
│ ├── logo48.png
│ ├── social.png
│ ├── logo180.png
│ ├── logo192.png
│ ├── statistics.js
│ └── home.js
├── tiss.py
├── templates
│ ├── template.css
│ ├── nobrowser.html
│ ├── statistics.html
│ └── home.html
├── monitoring.py
├── __init__.py
└── format.py
├── .dockerignore
├── architecture.png
├── screenshot.png
├── package.json
├── .gitignore
├── bettercal.service
├── Dockerfile
├── tests
├── conftest.py
├── test_request.py
├── calendar_de.ics
└── calendar_en.ics
├── pyproject.toml
├── LICENSE
├── .github
└── workflows
│ └── CI.yml
├── README.md
└── uv.lock
/.python-version:
--------------------------------------------------------------------------------
1 | 3.14
--------------------------------------------------------------------------------
/app/resources/.gitignore:
--------------------------------------------------------------------------------
1 | *driver.log
2 | venv/
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | bettercal.db
2 | .venv/
3 | node_modules/
4 | __pycache__/
5 | .github/
6 |
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/architecture.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/screenshot.png
--------------------------------------------------------------------------------
/app/static/logo32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/app/static/logo32.png
--------------------------------------------------------------------------------
/app/static/logo48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/app/static/logo48.png
--------------------------------------------------------------------------------
/app/static/social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/app/static/social.png
--------------------------------------------------------------------------------
/app/static/logo180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/app/static/logo180.png
--------------------------------------------------------------------------------
/app/static/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flofriday/better-tiss-calendar/HEAD/app/static/logo192.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "tailwindcss": "^4.0.5"
4 | },
5 | "dependencies": {
6 | "@tailwindcss/cli": "^4.0.5"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv/
2 | __pycache__/
3 | .pytest_cache/
4 | .ruff_cache/
5 | node_modules/
6 | app/static/style.css
7 | bettercal.db*
8 | .DS_Store
9 | requirements.txt
10 | backup.sh
11 |
--------------------------------------------------------------------------------
/app/tiss.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from icalendar import Calendar, Component
3 |
4 |
5 | def get_calendar(url: str) -> Component:
6 | resp = requests.get(url)
7 | resp.raise_for_status()
8 | return Calendar.from_ical(resp.text)
9 |
--------------------------------------------------------------------------------
/bettercal.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=A better TISS calendar
3 | After=network.target
4 |
5 | [Service]
6 | WorkingDirectory=/home/bettercal/better-tiss-calendar/
7 | ExecStart=/home/bettercal/.local/bin/uv run -- gunicorn -w 5 --bind localhost:5003 app:app
8 | Restart=always
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/app/resources/migrate.sql:
--------------------------------------------------------------------------------
1 | -- Migrating from legacy statistics to daily statistics
2 | CREATE TABLE statistics_daily (
3 | id INTEGER PRIMARY KEY AUTOINCREMENT,
4 | date TEXT DEFAULT (DATE('now')),
5 | token_hash TEXT NOT NULL,
6 | UNIQUE(date, token_hash)
7 | );
8 | INSERT INTO statistics_daily (date, token_hash)
9 | SELECT DISTINCT date(date) date, token_hash
10 | FROM statistics;
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:24-trixie-slim AS tailwindbuild
2 | WORKDIR /app
3 | COPY . .
4 | RUN npm install
5 | RUN npx @tailwindcss/cli -i app/templates/template.css -o app/static/style.css
6 |
7 | FROM python:3.14-slim-trixie
8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
9 |
10 | WORKDIR /app
11 | COPY --from=tailwindbuild /app .
12 | RUN uv sync
13 |
14 | ENTRYPOINT ["uv", "run", "--", "gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
15 |
--------------------------------------------------------------------------------
/app/resources/lecturetube_availability.csv:
--------------------------------------------------------------------------------
1 | AA0448
2 | AE0141
3 | AE0238
4 | AE0239
5 | AE0341
6 | AEEG40
7 | AEU141
8 | AHEG07
9 | BA02A05
10 | BA02A17
11 | BA02G02
12 | BAU178A
13 | BAU276A
14 | BC01A46
15 | BD01B33
16 | BD01B41
17 | BD02D32
18 | BF05A03
19 | CAEG17
20 | CAEG31
21 | CD0304
22 | CDEG08
23 | CDEG13
24 | CF0205
25 | CF0229
26 | CF0235
27 | DA02E08
28 | DA02F20
29 | DA02G15
30 | DA02K01
31 | DB02H04
32 | DB02H12
33 | DC02H03
34 | DD05B04
35 | DEU116
36 | EAEG06
37 | EBEG09
38 | HE0102
39 | HEEG02
40 | HHEG01
41 | OA01E29
42 | OC01I27
43 | OZEG76
44 | OZEG80
45 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import pytest
4 | from flask.testing import FlaskClient, FlaskCliRunner
5 | from syrupy.extensions.single_file import SingleFileSnapshotExtension
6 |
7 | from app import app as backendapp
8 |
9 |
10 | @pytest.fixture()
11 | def app():
12 | app = backendapp
13 | app.config.update(
14 | {
15 | "TESTING": True,
16 | }
17 | )
18 |
19 | yield app
20 |
21 |
22 | @pytest.fixture()
23 | def client(app) -> FlaskClient:
24 | return app.test_client()
25 |
26 |
27 | @pytest.fixture()
28 | def runner(app) -> FlaskCliRunner:
29 | return app.test_cli_runner()
30 |
31 |
32 | class ICALSnapshotExtension(SingleFileSnapshotExtension):
33 | file_extension = "ical"
34 |
35 | def serialize(self, data: str, **kwargs: Any) -> bytes:
36 | return data.encode("utf-8")
37 |
38 |
39 | @pytest.fixture
40 | def snapshot_ical(snapshot):
41 | return snapshot.use_extension(ICALSnapshotExtension)
42 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "better-tiss-calendar"
3 | version = "0.0.1"
4 | dependencies = [
5 | "flask>=3.1.2",
6 | "gunicorn>=23.0.0",
7 | "icalendar>=6.3.2",
8 | "requests>=2.32.5",
9 | ]
10 | requires-python = "==3.14.*"
11 | authors = [{ name = "flofriday" }]
12 | description = "A better TISS calendar"
13 | readme = "README.md"
14 | license = { file = "LICENSE.txt" }
15 |
16 | [tool.pytest.ini_options]
17 | testpaths = ["tests"]
18 | pythonpath = ["."]
19 |
20 | [tool.coverage.run]
21 | branch = true
22 | source = ["app"]
23 |
24 | [tool.ruff.lint]
25 | select = ["E", "F", "UP", "B", "SIM", "I"]
26 |
27 | [tool.ty.environment]
28 | python = ".venv"
29 |
30 | [tool.ty.src]
31 | exclude = ["venv", ".venv", "**/resources"]
32 |
33 | [tool.pyright]
34 | venvPath = "."
35 | venv = ".venv"
36 | exclude = ["venv", ".venv", "**/resources"]
37 |
38 | [dependency-groups]
39 | dev = [
40 | "pytest-mock>=3.15.1",
41 | "pytest>=9.0.2",
42 | "pyright>=1.1.407",
43 | "syrupy>=5.0.0",
44 | "ruff>=0.14.10",
45 | "ty>=0.0.4 ",
46 | ]
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 flofriday
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.
22 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | # A basic action that deploys to the server
2 |
3 | name: CI
4 |
5 | on: [push]
6 |
7 | jobs:
8 | test:
9 | runs-on: Ubuntu-24.04
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v4
13 |
14 | - name: Install uv
15 | uses: astral-sh/setup-uv@v5
16 |
17 | - name: Set up Python
18 | run: uv python install
19 |
20 | - name: Install dependencies
21 | run: uv sync
22 |
23 | - name: Linting
24 | run: uv run ruff check
25 |
26 | - name: Formatting
27 | run: uv run ruff format --check
28 |
29 | - name: Typecheck
30 | run: uv run ty check
31 |
32 | - name: Test with pytest
33 | run: uv run pytest
34 |
35 | deploy:
36 | needs: test
37 | if: ${{ github.ref_name == 'main' }}
38 | runs-on: ubuntu-latest
39 | steps:
40 | - name: deploy on production server
41 | uses: appleboy/ssh-action@v1.2.1
42 | with:
43 | host: ${{ secrets.HOST }}
44 | username: ${{ secrets.USERNAME }}
45 | password: ${{ secrets.PASSWORD }}
46 | port: ${{ secrets.PORT }}
47 | script_stop: true
48 | script: |
49 | cd better-tiss-calendar
50 | git fetch --all
51 | git reset --hard origin/main
52 | npm install
53 | npx @tailwindcss/cli --minify -i app/templates/template.css -o app/static/style.css
54 | /home/bettercal/.local/bin/uv sync
55 | systemctl --user restart bettercal.service
56 |
--------------------------------------------------------------------------------
/app/templates/template.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | /*
4 | The default border color has changed to `currentColor` in Tailwind CSS v4,
5 | so we've added these compatibility styles to make sure everything still
6 | looks the same as it did with Tailwind CSS v3.
7 |
8 | If we ever want to remove these styles, we need to add an explicit border
9 | color utility to any element that depends on these defaults.
10 | */
11 |
12 | @utility no-scrollbar {
13 | @layer utilities {
14 | /* Hide scrollbar for Chrome, Safari and Opera */
15 | &::-webkit-scrollbar {
16 | display: none;
17 | }
18 |
19 | /* Hide scrollbar for IE, Edge and Firefox */
20 | -ms-overflow-style: none; /* IE and Edge */
21 | scrollbar-width: none; /* Firefox */
22 | }
23 | }
24 |
25 | @layer utilities {
26 | :root {
27 | --text-color: #fff;
28 | --secondary-color: rgb(136, 136, 136);
29 | --background-color: #000;
30 | }
31 |
32 | body {
33 | background-color: var(--background-color);
34 | color: var(--text-color);
35 | font-family: "Inter", sans-serif;
36 | }
37 |
38 | a {
39 | @apply text-blue-400 visited:text-purple-400 hover:underline;
40 | }
41 |
42 | @keyframes flyaway {
43 | 0% {
44 | transform: translateY(30px);
45 | opacity: 1;
46 | }
47 | 100% {
48 | transform: translateY(-35px);
49 | opacity: 0;
50 | }
51 | }
52 |
53 | .flyaway {
54 | animation: 1.5s ease-out 0s 1 flyaway;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/static/statistics.js:
--------------------------------------------------------------------------------
1 | let currentChart = null;
2 |
3 | function drawStatisticsChart(displayData) {
4 | if (currentChart) currentChart.destroy();
5 |
6 | const ctx = document.getElementById("dailyChart");
7 |
8 | Chart.defaults.backgroundColor = "rgba(255, 255, 255, 0.2)";
9 | Chart.defaults.borderColor = "rgba(255, 255, 255, 0.2)";
10 | Chart.defaults.color = "#FFF";
11 |
12 | console.log(displayData);
13 | let labels = displayData.map(([l, d, m, t]) => l);
14 | let daily = displayData.map(([l, d, m, t]) => d);
15 | let monthly = displayData.map(([l, d, m, t]) => m);
16 | let total = displayData.map(([l, d, m, t]) => t);
17 | currentChart = new Chart(ctx, {
18 | type: "line",
19 | data: {
20 | labels: labels,
21 | datasets: [
22 | {
23 | label: "daily",
24 | data: daily,
25 | borderWidth: 1,
26 | borderColor: "#a3c4f3",
27 | backgroundColor: "#a3c4f3",
28 | },
29 | {
30 | label: "montly",
31 | data: monthly,
32 | borderWidth: 1,
33 | borderColor: "#ffcfd2",
34 | backgroundColor: "#ffcfd2",
35 | },
36 | {
37 | label: "total",
38 | data: total,
39 | borderWidth: 1,
40 | borderColor: "#b9fbc0",
41 | backgroundColor: "#b9fbc0",
42 | },
43 | ],
44 | },
45 | options: {
46 | scales: {
47 | y: {
48 | beginAtZero: true,
49 | },
50 | },
51 | elements: {
52 | point: {
53 | radius: 2,
54 | },
55 | },
56 | },
57 | });
58 | }
59 |
60 | const lastMonthCheck = document.getElementById("last-month");
61 | lastMonthCheck.addEventListener("change", render);
62 |
63 | function render() {
64 | if (lastMonthCheck.checked) {
65 | // only last 30 days
66 | drawStatisticsChart(chart_data.slice(Math.max(chart_data.length - 30, 0)));
67 | } else {
68 | // original
69 | drawStatisticsChart(chart_data);
70 | }
71 | }
72 |
73 | render();
74 |
--------------------------------------------------------------------------------
/app/resources/generate_rooms.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # dependencies = [
3 | # "attrs==23.2.0",
4 | # "certifi==2024.7.4",
5 | # "h11==0.14.0",
6 | # "idna==3.7",
7 | # "outcome==1.3.0.post0",
8 | # "PySocks==1.7.1",
9 | # "selenium==4.19.0",
10 | # "sniffio==1.3.1",
11 | # "sortedcontainers==2.4.0",
12 | # "trio==0.25.0",
13 | # "trio-websocket==0.11.1",
14 | # "typing_extensions==4.11.0",
15 | # "urllib3==2.2.2",
16 | # "wsproto==1.2.0",
17 | # ]
18 | # ///
19 |
20 | # This script creates the files rooms.csv.
21 | # It does this automatically by the room UI from tiss and extracting the
22 | # information from there.
23 | # To run this enter the following:
24 | # uv run generate_rooms.py
25 |
26 | import csv
27 |
28 | from selenium import webdriver
29 | from selenium.webdriver.common.by import By
30 | from selenium.webdriver.support import expected_conditions as EC
31 | from selenium.webdriver.support.ui import WebDriverWait
32 |
33 |
34 | def main():
35 | TISS_ROOM_LINK = "https://tiss.tuwien.ac.at/events/selectRoom.xhtml"
36 | NAVIGATION_BUTTON_SELECTOR = ".ui-paginator-pages > *"
37 | TABLE_ROW_SELECTOR = "#tableForm\\:roomTbl_data > *"
38 |
39 | data: list[list[str]] = []
40 |
41 | with webdriver.Firefox() as driver:
42 | driver.get(TISS_ROOM_LINK)
43 | navigation_buttons = WebDriverWait(driver, 10).until(
44 | EC.presence_of_all_elements_located(
45 | (By.CSS_SELECTOR, NAVIGATION_BUTTON_SELECTOR)
46 | )
47 | )
48 |
49 | for i in range(0, len(navigation_buttons)):
50 | navigation_buttons = WebDriverWait(driver, 5).until(
51 | EC.presence_of_all_elements_located(
52 | (By.CSS_SELECTOR, NAVIGATION_BUTTON_SELECTOR)
53 | )
54 | )
55 | navigation_buttons[i].click()
56 | table_rows = WebDriverWait(driver, 5).until(
57 | EC.presence_of_all_elements_located(
58 | (By.CSS_SELECTOR, TABLE_ROW_SELECTOR)
59 | )
60 | )
61 | for row in table_rows:
62 | cells = row.find_elements(By.CSS_SELECTOR, "td")
63 | room = [cell.text for cell in cells]
64 | room.append(
65 | cells[0]
66 | .find_elements(By.CSS_SELECTOR, "a")[0]
67 | .get_attribute("href")
68 | )
69 | data.append(room)
70 |
71 | data.sort(key=lambda i: i[0])
72 |
73 | with open("rooms.csv", "w", newline="", encoding="utf-8") as f:
74 | csvwriter = csv.writer(f)
75 | csvwriter.writerows(data)
76 |
77 |
78 | if __name__ == "__main__":
79 | main()
80 |
--------------------------------------------------------------------------------
/app/monitoring.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | from dataclasses import dataclass
3 | from sqlite3 import Connection
4 |
5 |
6 | def add_usage(db: Connection, token: str):
7 | hashed_token = hashlib.sha256(token.encode()).hexdigest()
8 | db.cursor().execute(
9 | "INSERT OR IGNORE INTO statistics_daily (token_hash) VALUES (?)",
10 | (hashed_token,),
11 | )
12 | db.commit()
13 |
14 |
15 | @dataclass
16 | class statistic:
17 | daily_users: int
18 | monthly_users: int
19 | total_users: int
20 |
21 |
22 | def get_statistics(db: Connection) -> statistic:
23 | # Get daily active users
24 | cursor = db.cursor()
25 | cursor.execute(
26 | """SELECT COUNT(*)
27 | FROM statistics_daily
28 | WHERE date == DATE('now');"""
29 | )
30 | daily_users = cursor.fetchone()[0]
31 |
32 | # Get monthly active users
33 | cursor.execute(
34 | """SELECT COUNT(DISTINCT token_hash) AS unique_active_users
35 | FROM statistics_daily
36 | WHERE date >= DATE('now', '-30 days');"""
37 | )
38 | monthly_users = cursor.fetchone()[0]
39 |
40 | # Get all users
41 | cursor.execute(
42 | """SELECT COUNT(DISTINCT token_hash) AS unique_users
43 | FROM statistics_daily;"""
44 | )
45 | total_users = cursor.fetchone()[0]
46 |
47 | return statistic(daily_users, monthly_users, total_users)
48 |
49 |
50 | # Only the usage for today will change, so we can cache all past days.
51 | chart_cache: list[tuple[str, int, int, int]] = []
52 |
53 |
54 | def get_chart_data(db: Connection) -> list[tuple[str, int, int, int]]:
55 | global chart_cache
56 | since = chart_cache[-1][0] if len(chart_cache) > 0 else None
57 |
58 | # Fetch the newest data
59 | new_data = get_chart_data_since(db, since)
60 | result = chart_cache + new_data
61 |
62 | # Update the cache (never store the last day because we don't trust it)
63 | # Well the day is not over so it will probably change.
64 | chart_cache += new_data[:-1]
65 |
66 | return result
67 |
68 |
69 | def get_chart_data_since(
70 | db: Connection, since: str | None = None
71 | ) -> list[tuple[str, int, int, int]]:
72 | if since is None:
73 | # This is just any day far in the past but, the exact day is just the day before
74 | # it went into production.
75 | since = "2023-07-16"
76 |
77 | # Get daily active users
78 | cursor = db.cursor()
79 | cursor.execute(
80 | """
81 | SELECT s.date AS day,
82 | COUNT(*) AS 'daily',
83 | (SELECT COUNT(DISTINCT token_hash)
84 | FROM statistics_daily s2
85 | WHERE s2.date <= s.date
86 | AND s2.date >= DATE( s.date, '-30 days')
87 | ) AS 'monbthly',
88 | (SELECT COUNT(DISTINCT token_hash)
89 | FROM statistics_daily s3
90 | WHERE s3.date <= s.date
91 | ) AS 'total'
92 | FROM statistics_daily s
93 | WHERE day > ?
94 | GROUP BY day
95 | ORDER BY day
96 | """,
97 | (since,),
98 | )
99 | rows = cursor.fetchall()
100 | return rows
101 |
--------------------------------------------------------------------------------
/app/resources/generate_lecturetube_availability.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # dependencies = [
3 | # "attrs==23.2.0",
4 | # "certifi==2024.7.4",
5 | # "h11==0.14.0",
6 | # "idna==3.7",
7 | # "outcome==1.3.0.post0",
8 | # "PySocks==1.7.1",
9 | # "selenium==4.19.0",
10 | # "sniffio==1.3.1",
11 | # "sortedcontainers==2.4.0",
12 | # "trio==0.25.0",
13 | # "trio-websocket==0.11.1",
14 | # "typing_extensions==4.11.0",
15 | # "urllib3==2.2.2",
16 | # "wsproto==1.2.0",
17 | # ]
18 | # ///
19 |
20 | # This script creates the file lecturetube_availability.csv.
21 | # It does this automatically by the room UI from colab.tuwien.ac.at
22 | # and extracting the information from there
23 | # To run this enter the following:
24 | # uv run generate_lecturetube_availability.py
25 |
26 | import csv
27 |
28 | from selenium import webdriver
29 | from selenium.webdriver.common.by import By
30 | from selenium.webdriver.support import expected_conditions as EC
31 | from selenium.webdriver.support.ui import WebDriverWait
32 |
33 |
34 | def main():
35 | COLAB_LECTURETUBE_LINK = "https://colab.tuwien.ac.at/lecturetube/"
36 | NAVIGATION_ELEMENTS_SELECTOR = "nav.ht-pages-nav > ul.ht-pages-nav-top > li"
37 | ANKOR_LECTUREHALL_LIST_IDENTIFIER_DE = "hoersaalliste"
38 | ANKOR_LECTUREHALL_LIST_IDENTIFIER_EN = "lecture-halls"
39 | LECTURETUBELIST_CONTENT_SELECTOR = "#main-content"
40 | LECTURETUBELIST_ROW_SELECTOR = "tbody > tr"
41 |
42 | data: list[list[str]] = []
43 |
44 | with webdriver.Firefox() as driver:
45 | driver.get(COLAB_LECTURETUBE_LINK)
46 | navigation_elements = WebDriverWait(driver, 10).until(
47 | EC.presence_of_all_elements_located(
48 | (By.CSS_SELECTOR, NAVIGATION_ELEMENTS_SELECTOR)
49 | )
50 | )
51 |
52 | for listelement in navigation_elements:
53 | ankor = listelement.find_element(By.TAG_NAME, "a")
54 | href = ankor.get_attribute("href")
55 | if href and (
56 | (ANKOR_LECTUREHALL_LIST_IDENTIFIER_DE in href)
57 | or (ANKOR_LECTUREHALL_LIST_IDENTIFIER_EN in href)
58 | ):
59 | ankor.click()
60 | break
61 |
62 | main_content = WebDriverWait(driver, 10).until(
63 | EC.presence_of_element_located(
64 | (By.CSS_SELECTOR, LECTURETUBELIST_CONTENT_SELECTOR)
65 | )
66 | )
67 |
68 | table_rows = main_content.find_elements(
69 | By.CSS_SELECTOR, LECTURETUBELIST_ROW_SELECTOR
70 | )
71 | for row in table_rows:
72 | cells = row.find_elements(By.CSS_SELECTOR, "td")
73 | room_details = [cell.text for cell in cells]
74 | roomcode = room_details[2]
75 | if room_details[4] == "JA" and verifyHasLectureTubeStreaming(
76 | driver, roomcode
77 | ):
78 | data.append([roomcode])
79 |
80 | data.sort(key=lambda i: i[0])
81 |
82 | with open("lecturetube_availability.csv", "w", newline="", encoding="utf-8") as f:
83 | csvwriter = csv.writer(f)
84 | csvwriter.writerows(data)
85 |
86 |
87 | def verifyHasLectureTubeStreaming(driver, room_code):
88 | LECTURETUBE_ROOM_LINK = (
89 | f"https://live.video.tuwien.ac.at/room/{room_code}/player.html"
90 | )
91 | CONTENT_CARD_SELECTOR = "main#content > .card"
92 |
93 | previous_window = driver.current_window_handle
94 | driver.switch_to.new_window("tab")
95 | driver.get(LECTURETUBE_ROOM_LINK)
96 | content_card = WebDriverWait(driver, 10).until(
97 | EC.presence_of_element_located((By.CSS_SELECTOR, CONTENT_CARD_SELECTOR))
98 | )
99 | video_tags = content_card.find_elements(By.TAG_NAME, "video")
100 | driver.close()
101 | driver.switch_to.window(previous_window)
102 | return len(video_tags) > 0
103 |
104 |
105 | if __name__ == "__main__":
106 | main()
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # better-tiss-calendar
2 |
3 | 
4 | TISS is the information service of TU Wien which exports a suboptimal calendar
5 | of events (like lectures). This project improves the calendar by rewriting the
6 | events and enriching them with useful information.
7 |
8 | [Live Website](https://bettercal.flofriday.dev)
9 |
10 | ## Features
11 |
12 | - Remove lecture number from the title
13 | - Use shorthands instead of the long name (optional)
14 | - Correct address in the location field, not just the room
15 | - Floor information
16 | - Links to [TUW-Maps](https://maps.tuwien.ac.at), [TISS](https://tiss.tuwien.ac.at/), [TUWEL](https://tuwel.tuwien.ac.at/) and [LectureTube](https://live.video.tuwien.ac.at/)
17 | - Drop in replacement
18 | - Easy setup: no login, no account and no rage inducing captchas
19 | - Self-hosting friendly
20 |
21 | ## Importing the calendar
22 |
23 | Adding the bettercal calendar to your outlook, google calendar or iPhone app is
24 | identical to the official Tiss calendar. However, if you have never done that
25 | or not in a long time, here are some guides:
26 |
27 | - [iPhone](https://support.apple.com/guide/iphone/set-up-mail-contacts-and-calendar-accounts-ipha0d932e96/ios#:~:text=Go%20to%20Settings%20%3E%20Calendar%20%3E%20Accounts,your%20server%20and%20account%20information.)
28 | - [macOS](https://support.apple.com/guide/calendar/subscribe-to-calendars-icl1022/mac#:~:text=In%20the%20Calendar%20app%20on,an%20account%20for%20the%20subscription.)
29 | - [Gmail](https://support.google.com/calendar/answer/37100?hl=en&co=GENIE.Platform%3DDesktop&oco=1) (Note: you can only add it on desktop, but then it will also show up on mobile)
30 | - [Outlook](https://support.microsoft.com/en-us/office/import-or-subscribe-to-a-calendar-in-outlook-com-or-outlook-on-the-web-cff1429c-5af6-41ec-a5b4-74f2c278e98c)
31 |
32 | ## How it works
33 |
34 | 
35 |
36 | Calendar clients download subscribed calendars periodically (some even let you
37 | set the polling rate). Now with bettercal when a request comes in for a calendar this
38 | server downloads the original calendar from tiss and formats all events and
39 | enriches them with more information before returning it to the client.
40 |
41 | ## Build it yourself
42 |
43 | You first need to install [node](https://nodejs.org/en) and [uv](https://github.com/astral-sh/uv).
44 |
45 | ```bash
46 | npm install
47 | npx @tailwindcss/cli -i app/templates/template.css -o app/static/style.css
48 | uv run flask run --debug
49 | ```
50 |
51 | The server should now start at http://localhost:5000
52 |
53 | **Note:** While working on the frontend, it is quite handy to add the `--watch`
54 | flag to the tailwind command.
55 |
56 | **Warning:** The flask server here cannot be used in production and is optimized
57 | for development comfort.
58 |
59 | ### Tests
60 |
61 | You can run all tests with:
62 |
63 | ```bash
64 | uv run pytest
65 | ```
66 |
67 | In case you update the output of the calendar, you will need to regenerate the
68 | snapshots (aka. reference output used by some tests), which you can do with:
69 |
70 | ```bash
71 | uv run pytest --snapshot-update
72 | ```
73 |
74 | ## Build with Docker
75 |
76 | ```bash
77 | docker build -t bettercal .
78 | docker run -it --rm -p 5000:5000 bettercal
79 | ```
80 |
81 | The server should now start at http://localhost:5000
82 |
83 | This approach can be used in production. However, statistics will die with the
84 | container, but they can be preserved by adding the
85 | `--mount type=bind,source="$(pwd)"/bettercal.db,target=/app/bettercal.db` argument to the
86 | `docker run` command.
87 |
88 | ## How we deploy
89 |
90 | In production we use the [gunicorn](https://gunicorn.org/) server.
91 | The main branch is automatically deployed as a systemd user service on a linux
92 | box. The systemd config can be read in `bettercal.service`.
93 |
94 | ## Contributing
95 |
96 | Contributions are quite welcome, you are awesome. 😊🎉
97 |
98 | If you want to add a shorthand for a lecture, the file you need to edit is
99 | `ressources/shorthands.csv`.
100 |
101 | For formatting and linting we use `ruff` and for typechecking `ty`
102 |
103 | ```bash
104 | uv run ruff format
105 | uv run ruff check --fix
106 | uv run ty check # Still pre-release
107 | # uv run pyright # Fallback for ty
108 | ```
--------------------------------------------------------------------------------
/app/static/home.js:
--------------------------------------------------------------------------------
1 | // Should we use typescript or a svelte for just this single page?
2 |
3 | const verifyBtn = document.getElementById("verify");
4 | const copyBtn = document.getElementById("copy");
5 | const urlText = document.getElementById("original_url");
6 | const betterText = document.getElementById("better_url");
7 | const betterTextPlaceholder = document.getElementById("better_url_placeholder");
8 | const errorText = document.getElementById("error_message");
9 | const copyText = document.getElementById("copy_feedback");
10 | const googleCheck = document.getElementById("is_google");
11 | const noShorthandCheck = document.getElementById("no_shorthand");
12 | const previewTitle = document.getElementById("preview-title");
13 | const previewLectureName = document.getElementById("preview-lecturename");
14 | const importTip = document.getElementById("import-tip");
15 |
16 | let isDisabled = true;
17 | let currentUrl = "";
18 |
19 | window.onload = () => {
20 | copyBtn.disabled = true;
21 |
22 | if (!urlText.value && localStorage.getItem("originalUrl")) {
23 | urlText.value = localStorage.getItem("originalUrl");
24 | }
25 | };
26 |
27 | urlText.onchange = () => {
28 | disableBetterUrl();
29 | };
30 |
31 | urlText.onkeydown = () => {
32 | disableBetterUrl();
33 | };
34 |
35 | verifyBtn.onclick = async () => {
36 | await verify();
37 | };
38 |
39 | googleCheck.onchange = () => {
40 | if (isDisabled) return;
41 | setBetterUrl(currentUrl);
42 | };
43 |
44 | noShorthandCheck.onchange = () => {
45 | if (noShorthandCheck.checked) {
46 | previewTitle.innerText = "Einführung in die Programmierung 1 VU";
47 | } else {
48 | previewTitle.innerText = "EP1 VU";
49 | }
50 | previewLectureName.hidden = noShorthandCheck.checked;
51 |
52 | if (isDisabled) return;
53 | setBetterUrl(currentUrl);
54 | };
55 |
56 | copyBtn.onclick = () => {
57 | if (copyBtn.disabled) return;
58 |
59 | text = betterText.innerText;
60 | navigator.clipboard.writeText(text);
61 |
62 | copyText.classList.remove("flyaway");
63 | setTimeout(function () {
64 | copyText.classList.add("flyaway");
65 | }, 1);
66 | };
67 |
68 | async function verify(url) {
69 | url = urlText.value
70 | .trim()
71 | .replace(/Download$/, "")
72 | .trim();
73 |
74 | urlText.value = url;
75 | currentUrl = url;
76 | try {
77 | const response = await fetch(`/verify?url=${encodeURIComponent(url)}`);
78 | if (response.ok) {
79 | localStorage.setItem("originalUrl", url);
80 | setBetterUrl(url);
81 | } else {
82 | localStorage.setItem("originalUrl", "");
83 | let message = await response.text();
84 | if (message.trim().length > 0) {
85 | setErrorMessage(message);
86 | } else {
87 | setErrorMessage("Server returned: " + response.statusText);
88 | }
89 | }
90 | } catch (error) {
91 | setErrorMessage("Could not connect to the server: " + error);
92 | }
93 | }
94 |
95 | function disableBetterUrl() {
96 | isDisabled = true;
97 | errorText.classList.add("invisible");
98 | betterText.classList.add("hidden");
99 | betterTextPlaceholder.classList.remove("hidden");
100 | importTip.classList.add("invisible");
101 | importTip.classList.add("opacity-0");
102 | copyBtn.disabled = true;
103 | }
104 |
105 | function setErrorMessage(message) {
106 | errorText.classList.remove("invisible");
107 | errorText.innerText = "🔥 " + message;
108 | }
109 |
110 | function setBetterUrl(originalUrl) {
111 | isDisabled = false;
112 | errorText.classList.add("invisible");
113 | betterText.classList.remove("hidden");
114 | betterTextPlaceholder.classList.add("hidden");
115 | importTip.classList.remove("invisible");
116 | importTip.classList.remove("opacity-0");
117 | copyBtn.disabled = false;
118 |
119 | const tmpurl = new URL(originalUrl);
120 | const searchParams = new URLSearchParams(tmpurl.search);
121 | const token = searchParams.get("token");
122 | const locale = searchParams.get("locale");
123 |
124 | const domain = window.location.origin;
125 | let betterUrl = `${domain}/personal.ics?token=${encodeURIComponent(
126 | token,
127 | )}&locale=${encodeURIComponent(locale)}`;
128 | if (googleCheck.checked) {
129 | betterUrl += "&google";
130 | }
131 | if (noShorthandCheck.checked) {
132 | betterUrl += "&noshorthand";
133 | }
134 |
135 | betterText.innerText = betterUrl;
136 | }
137 |
--------------------------------------------------------------------------------
/app/templates/nobrowser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | No Browser
7 |
8 |
9 |
10 |
11 |
12 |
18 |
24 |
25 |
26 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
Oh no 😰
46 |
47 |
48 | Unfortunatly you cannot open the calendar in the browser.
49 |
50 |
51 | But there is no need to panic, on the page where you created the calendar there are even instruction how to add it to your calendar app.
52 |
53 |
54 |
55 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sqlite3
3 | import traceback
4 | from urllib.parse import urlparse
5 |
6 | import requests
7 | from flask import Flask, g, render_template, request, send_from_directory
8 |
9 | import app.tiss as tiss
10 | from app.format import improve_calendar
11 | from app.monitoring import add_usage, get_chart_data, get_statistics
12 |
13 | app = Flask(__name__)
14 |
15 | DATABASE = "bettercal.db"
16 |
17 |
18 | def create_db() -> sqlite3.Connection:
19 | if app.config["TESTING"]:
20 | db = sqlite3.connect(":memory:")
21 | else:
22 | db = sqlite3.connect(DATABASE)
23 | db.execute("PRAGMA journal_mode=WAL;")
24 |
25 | db.cursor().executescript(
26 | """
27 | CREATE TABLE IF NOT EXISTS statistics_daily (
28 | id INTEGER PRIMARY KEY AUTOINCREMENT,
29 | date TEXT DEFAULT (DATE('now')),
30 | token_hash TEXT NOT NULL,
31 | UNIQUE(date, token_hash)
32 | );
33 | """
34 | )
35 | db.commit()
36 | return db
37 |
38 |
39 | def get_db() -> sqlite3.Connection:
40 | db = getattr(g, "_database", None)
41 | if db is None:
42 | db = g._database = create_db()
43 | return db
44 |
45 |
46 | @app.teardown_appcontext
47 | def close_connection(exception):
48 | db = getattr(g, "_database", None)
49 | if db is not None:
50 | db.close()
51 |
52 |
53 | # Preheat the statistics cache
54 | get_chart_data(create_db())
55 |
56 |
57 | @app.route("/")
58 | def home():
59 | return render_template("home.html")
60 |
61 |
62 | @app.route("/statistics")
63 | def statistic_page():
64 | statistic = get_statistics(get_db())
65 | chart_data = get_chart_data(get_db())
66 | return render_template(
67 | "statistics.html", statistic=statistic, chart_data=json.dumps(chart_data)
68 | )
69 |
70 |
71 | @app.route("/static/")
72 | def static_asset(path):
73 | return send_from_directory("static", path)
74 |
75 |
76 | @app.route("/verify")
77 | def verify():
78 | url = request.args.get("url", "").strip()
79 | if url is None or url == "":
80 | return "Empty links don't work *surprised picatchu meme*", 400
81 |
82 | # A better error message if the submitted url is not of the icalendar but
83 | # of the html page itself.
84 | if url.startswith("https://tiss.tuwien.ac.at/events/personSchedule.xhtml"):
85 | return "Almost, the url we need is at the bottom of the page you submitted", 400
86 |
87 | # Inspecting the url
88 | scheme, loc, path, _, query, _ = urlparse(url)
89 | if not (
90 | scheme == "https"
91 | and loc == "tiss.tuwien.ac.at"
92 | and path == "/events/rest/calendar/personal"
93 | ):
94 | return "The url must point to the TISS calendar", 400
95 | if "token=" not in query:
96 | return "The complete calendar url must be submitted, including the token.", 400
97 |
98 | try:
99 | tiss.get_calendar(url)
100 | except requests.HTTPError:
101 | return "TISS rejected this url. Maybe the token is invalid?", 400
102 | except ConnectionError:
103 | return "Could not contact TISS. Maybe TISS is down?", 400
104 | except ValueError:
105 | return "TISS didn't return an ical file, did you paste the correct url?", 400
106 | except Exception:
107 | return "Something unexpected went wrong, maybe create an GitHub issue?", 500
108 |
109 | return "Ok"
110 |
111 |
112 | @app.route("/personal.ics")
113 | def icalendar():
114 | # If accessed from a browser render fallback
115 | if "text/html" in request.headers.get("Accept", ""):
116 | # The calendar shouldn't be opend in the but subscribed in your app
117 | return render_template("nobrowser.html"), 406
118 |
119 | token = request.args.get("token")
120 | if token is None:
121 | return "No token provided", 400
122 |
123 | locale = request.args.get("locale")
124 | if locale is None:
125 | return "No locale provided", 400
126 |
127 | # FIXME: Maybe we can autodetect if google is the client and remove this
128 | # option
129 | is_google = "google" in request.args
130 | use_shorthand = "noshorthand" not in request.args
131 |
132 | url = f"https://tiss.tuwien.ac.at/events/rest/calendar/personal?token={token}&locale={locale}"
133 | cal = tiss.get_calendar(url)
134 | try:
135 | cal = improve_calendar(
136 | cal,
137 | google_cal=is_google,
138 | use_shorthand=use_shorthand,
139 | locale=locale,
140 | )
141 | except Exception as e:
142 | # A error occured during reformatting, print a traceback for loggs but
143 | # continue with returning the original calendar.
144 | traceback.print_exception(e)
145 |
146 | body = cal.to_ical()
147 |
148 | add_usage(get_db(), token)
149 |
150 | return body, 200, {"Content-Type": "text/calendar; charset=utf-8"}
151 |
--------------------------------------------------------------------------------
/tests/test_request.py:
--------------------------------------------------------------------------------
1 | from flask.testing import FlaskClient
2 | from icalendar import Calendar, Component
3 |
4 |
5 | def get_test_calendar(lang: str = "de"):
6 | # FIXME: we need better examples, at the moment they only have 5 events and
7 | # we could even merge multiple into one.
8 | with open(f"tests/calendar_{lang}.ics") as f:
9 | cal = Calendar.from_ical(f.read())
10 | return cal
11 |
12 |
13 | def calendar_event_cnt(cal: Component) -> int:
14 | return sum([1 for c in cal.walk() if c.name == "VEVENT"])
15 |
16 |
17 | def calendar_summaries(cal: Component) -> list[str]:
18 | return [c.get("summary") for c in cal.walk() if c.name == "VEVENT"]
19 |
20 |
21 | def calendar_descriptions(cal: Component) -> list[str]:
22 | descriptions = [c.get("description") for c in cal.walk() if c.name == "VEVENT"]
23 | return [d for d in descriptions if d is not None]
24 |
25 |
26 | def test_home_page(client: FlaskClient):
27 | response = client.get("/")
28 | assert response.status_code == 200
29 | assert b"Original calendar url" in response.data
30 |
31 |
32 | def test_verify_bad_url(client: FlaskClient, mocker):
33 | mocker.patch("app.tiss.get_calendar", return_value=None)
34 |
35 | response = client.get("/verify?url=nooAUrl")
36 | assert response.status_code == 400
37 |
38 |
39 | def test_verify_not_tiss_url(client: FlaskClient, mocker):
40 | mocker.patch("app.tiss.get_calendar", return_value=get_test_calendar())
41 |
42 | response = client.get("/verify?url=https://example.com")
43 | assert response.status_code == 400
44 |
45 |
46 | def test_verify_success(client: FlaskClient, mocker):
47 | mocker.patch("app.tiss.get_calendar", return_value=get_test_calendar())
48 |
49 | response = client.get(
50 | "/verify?url=https://tiss.tuwien.ac.at/events/rest/calendar/personal?locale=de%26token=justATestingTokenObviouslyNotReal"
51 | )
52 | assert response.status_code == 200
53 |
54 |
55 | def test_icalendar_de_success(client: FlaskClient, mocker, snapshot_ical):
56 | testcal = get_test_calendar(lang="de")
57 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
58 |
59 | response = client.get(
60 | "/personal.ics?locale=de&token=justATestingTokenObviouslyNotReal"
61 | )
62 | assert response.status_code == 200
63 |
64 | # Make sure the thing returned is still parseable
65 | Calendar.from_ical(response.data)
66 |
67 | assert snapshot_ical == response.text
68 |
69 |
70 | def test_icalendar_en_success(client: FlaskClient, mocker, snapshot_ical):
71 | testcal = get_test_calendar(lang="en")
72 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
73 |
74 | response = client.get(
75 | "/personal.ics?locale=en&token=justATestingTokenObviouslyNotReal"
76 | )
77 | assert response.status_code == 200
78 |
79 | # Make sure the thing returned is still parseable
80 | Calendar.from_ical(response.data)
81 |
82 | assert snapshot_ical == response.text
83 |
84 |
85 | def test_icalendar_forgoogle_de_success(client: FlaskClient, mocker, snapshot_ical):
86 | testcal = get_test_calendar(lang="de")
87 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
88 |
89 | response = client.get(
90 | "/personal.ics?locale=de&token=justATestingTokenObviouslyNotReal&google"
91 | )
92 | assert response.status_code == 200
93 |
94 | # Make sure the thing returned is still parseable
95 | Calendar.from_ical(response.data)
96 |
97 | assert snapshot_ical == response.text
98 |
99 |
100 | def test_icalendar_forgoogle_en_success(client: FlaskClient, mocker, snapshot_ical):
101 | testcal = get_test_calendar(lang="en")
102 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
103 |
104 | response = client.get(
105 | "/personal.ics?locale=en&token=justATestingTokenObviouslyNotReal&google"
106 | )
107 | assert response.status_code == 200
108 |
109 | # Make sure the thing returned is still parseable
110 | Calendar.from_ical(response.data)
111 |
112 | assert snapshot_ical == response.text
113 |
114 |
115 | def test_icalendar_noshorthands_de_success(client: FlaskClient, mocker, snapshot_ical):
116 | testcal = get_test_calendar(lang="de")
117 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
118 |
119 | response = client.get(
120 | "/personal.ics?locale=de&token=justATestingTokenObviouslyNotReal&noshorthand"
121 | )
122 | assert response.status_code == 200
123 |
124 | # Make sure the thing returned is still parseable
125 | Calendar.from_ical(response.data)
126 |
127 | assert snapshot_ical == response.text
128 |
129 |
130 | def test_icalendar_noshorthands_en_success(client: FlaskClient, mocker, snapshot_ical):
131 | testcal = get_test_calendar(lang="en")
132 | mocker.patch("app.tiss.get_calendar", return_value=testcal)
133 |
134 | response = client.get(
135 | "/personal.ics?locale=en&token=justATestingTokenObviouslyNotReal&noshorthand"
136 | )
137 | assert response.status_code == 200
138 |
139 | # Make sure the thing returned is still parseable
140 | Calendar.from_ical(response.data)
141 |
142 | assert snapshot_ical == response.text
143 |
--------------------------------------------------------------------------------
/app/templates/statistics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Statistics
7 |
8 |
9 |
10 |
11 |
12 |
18 |
24 |
25 |
26 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
Statistics
46 |
47 |
48 | - Today's active users: {{statistic.daily_users}}
49 | - Montly active users: {{statistic.monthly_users}}
50 | - Total users: {{statistic.total_users}}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
81 |
82 |
83 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/resources/shorthands.csv:
--------------------------------------------------------------------------------
1 | Shorthand, Lecture de, Lecture en
2 | prolog, Propädeutikum für Informatik, N/A
3 | akmath, Angleichungskurs Mathematik für INF und WINF, Harmonisation Cours Mathematics for INF and WINF
4 | orientierung, Orientierung Informatik und Wirtschaftsinformatik, Orientation Informatics and Business Informatics
5 | adm, Algebra und Diskrete Mathematik für Informatik und Wirtschaftsinformatik, N/A
6 | ana, Analysis für Informatik und Wirtschaftsinformatik, Analysis for Computer Science
7 | ep1, Einführung in die Programmierung 1, Introduction to Programming 1
8 | dwi, Denkweisen der Informatik, ways of thinking in informatics
9 | tgi, Technische Grundlagen der Informatik, Fundamentals of Digital Systems
10 | gr-buf, Grundlagen der Betriebs- und Unternehmensführung, Fundamentals of Business Management
11 | gr-org, Grundlagen der Organisation, Organization
12 | klr, Kosten- und Leistungsrechnung, Cost Accounting
13 | rw, Rechnungswesen, Accounting
14 | fmod, Formale Modellierung, Formal Modelling
15 | omod, Objektorientierte Modellierung, Object-oriented modeling
16 | algodat, Algorithmen und Datenstrukturen, Algorithms and Data Structures
17 | ep2, Einführung in die Programmierung 2, Introduction to Programming 2
18 | progpar, Programmierparadigmen, Programming Paradigms
19 | fp, Funktionale Programmierung, Functional Programming
20 | lc, Logikprogrammierung und Constraints, Logic programming and constraints
21 | bs, Betriebssysteme, Operating Systems
22 | am, Abstrakte Maschinen, Abstract Machines
23 | parco, Parallel Computing, N/A
24 | ueb, Übersetzerbau, Compilers
25 | evc, Einführung in Visual Computing, Introduction to Visual Computing
26 | cg, Computergraphik, Computer Graphics
27 | edbv, Einführung in die digitale Bildverarbeitung, Introduction to Digital Image Processing
28 | multimedia, Multimedia, N/A
29 | vis, Visualisierung 1, Visualization 1
30 | vis2, Visualisierung 2, Visualization 2
31 | ecg, Einführung in die Computergraphik, Introduction to Computer Graphics
32 | em, Einführung in die Mustererkennung, Introduction to Pattern Recognition
33 | iid, Interface and Interaction Design, N/A
34 | ue, Usability Engineering, N/A
35 | avp, Audio Production and Video Production, N/A
36 | sec, Socially Embedded Computing, N/A
37 | gwg, Gesellschaftswissenschaftliche Grundlagen der Informatik, Social issues in computing
38 | dbs, Datenbanksysteme, Data Base Systems
39 | ewbs, Einführung in wissensbasierte Systeme, Introduction to Knowledge-based Systems
40 | ssd, Semistrukturierte Daten, Semistructured Data
41 | webengineering, Web Engineering, N/A
42 | eai, Einführung in Artificial Intelligence, Introduction to Artificial Intelligence
43 | ab, Argumentieren und Beweisen, Argumentation and Proof
44 | dp, Deklaratives Problemlösen, Declarative Problem Solving
45 | vh, Vertrags- und Haftungsrecht, Contract and liability law
46 | gp, Grundlagen der Physik, Introduction to basic physics for computer science students
47 | introsec, Introduction to Security, N/A
48 | i-crypto, Introduction to Cryptography, N/A
49 | dir, Daten- und Informatikrecht, Data and Information Law
50 | pet, Privacy Enhancing Technologies, N/A
51 | sse, Security for Systems Engineering, N/A
52 | mobsec, Mobile Security, N/A
53 | ws, Statistik und Wahrscheinlichkeitstheorie, Statistics and Probability Theory
54 | sepm, Software Engineering und Projetkmanagement, Software Engineering and Project Management
55 | vs, Verteilte Systeme, Distributed Systems
56 | psv, Programm- und Systemverifikation, Program and System Verification
57 | sqs, Software-Qualitätssicherung, Software Quality Assurance
58 | til, Theoretische Informatik und Logik, Theoretical Computer Science and Logics
59 | wa, Wissenschaftliches Arbeiten, Scientific Research and Writing
60 | ana2, Analysis 2 für Informatik, Analysis 2
61 | dopp, Datenorientierte Programmierparadigmen, Data-oriented Programming Paradigms
62 | gir, Grundlagen des Information Retrieval, Introduction to Information Retrieval
63 | pcer, Parallel Computing Einführung paralleles Rechnen, N/A
64 | gmod, Geschäftsprozessmodellierung, Enterprise Modeling
65 | eis, Enterprise Information Systems, N/A
66 | its, IT Strategie, IT Strategy
67 | pw, Privates Wirtschaftsrecht, Private Law
68 | bo, Betriebswirtschaftliche Optimierung, Optimization in Business and Economics
69 | im, Innovationsmanagement und Marketing, Innovation Management and Marketing
70 | gmakroö, Grundlagen der Makroökonomie, Principles of Macroeconomics
71 | if, Investition und Finanzierung 1, Investment and Financing 1
72 | if2, Investition und Finanzierung 2, Investment and Financing 2
73 | edvv, EDV-Vertragsrecht, Law of IT-Contracts
74 | etil, Einführung in theoretische Informatik und Logik, Introduction to Theoretical Computer Science and Logics
75 | dst, Distributed Systems Technologies, N/A
76 | dync, Dynamic Compilation Dynamische Übersetzer, Dynamic Compilation
77 | foop, Fortgeschrittene objektorientierte Programmierung, Advanced Object-Oriented Programming
78 | ps, Programmiersprachen, Programming Languages
79 | seps, Seminar aus Programmiersprachen, Programming languages seminar
80 | aic, Advanced Internet Computing, N/A
81 | fmi, Formale Methoden der Informatik, Formal Methods in Computer Science
82 | ase, Advanced Software Engineering, N/A
83 | effprog, Effiziente Programme, Efficient Programs
84 | gpuac, GPU Architectures and Computing, N/A
85 | ffp, Fortgeschrittene funktionale Programmierung, Advanced Functional Programming
86 | dipl, Seminar für Diplomand_innen für Software Engineering & Internet Computing, Seminar for Master Students in Software Engineering & Internet Computing
87 | ts, Typsysteme, Type Systems
88 | cc, Cryptocurrencies, N/A
89 | genai, Generative AI, N/A
90 | zkb, Zwischen Karriere und Barriere, Developing a career - coping with obstacles
--------------------------------------------------------------------------------
/app/resources/generate_courses.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # dependencies = [
3 | # "beautifulsoup4==4.14.2",
4 | # "httpx==0.28.1",
5 | # "aiohttp==3.13.0",
6 | # ]
7 | # ///
8 |
9 |
10 | import argparse
11 | import asyncio
12 | import csv
13 | import random
14 | import re
15 | from dataclasses import dataclass
16 | from datetime import datetime
17 |
18 | import aiohttp
19 | import yarl
20 | from bs4 import BeautifulSoup
21 |
22 |
23 | def TissClient() -> aiohttp.ClientSession:
24 | req_id = str(random.randint(0, 999))
25 | window_id = str(random.randint(1000, 9999))
26 |
27 | async def url_rewriter_middleware(
28 | req: aiohttp.ClientRequest, handler: aiohttp.ClientHandlerType
29 | ) -> aiohttp.ClientResponse:
30 | url = str(req.url)
31 | req.url = yarl.URL(
32 | url + (("&" if "?" in url else "?") + f"dswid={window_id}&dsrid={req_id}")
33 | )
34 | return await handler(req)
35 |
36 | client = aiohttp.ClientSession(
37 | connector=aiohttp.TCPConnector(limit=0, limit_per_host=200),
38 | cookies={f"dsrwid-{req_id}": f"{window_id}", "TISS_LANG": "en"},
39 | middlewares=(url_rewriter_middleware,),
40 | )
41 |
42 | return client
43 |
44 |
45 | session: None | aiohttp.ClientSession = None
46 |
47 | total_programs = 0
48 | counter_programs = 0
49 |
50 |
51 | async def fetch_program_courses(program_url) -> set[str]:
52 | html = await (await session.get(program_url)).text()
53 |
54 | global counter_programs
55 | counter_programs += 1
56 | print(f"\t[{counter_programs}/{total_programs}] Downloaded {program_url}")
57 |
58 | courses = re.findall(r"/course/courseDetails\.xhtml\?courseNr=\d+", html)
59 | return set(courses)
60 |
61 |
62 | @dataclass(frozen=True)
63 | class Course:
64 | id: str | None
65 | name: str | None
66 | tiss_url: str
67 | tuwel_url: str | None
68 | registration_start: datetime | None
69 | registration_end: datetime | None
70 | deregistration_end: datetime | None
71 |
72 |
73 | total_courses = 0
74 | counter_courses = 0
75 |
76 |
77 | async def fetch_course_info(course_url) -> Course:
78 | html = await (await session.get(course_url)).text()
79 |
80 | global counter_courses
81 | counter_courses += 1
82 | print(f"\t[{counter_courses}/{total_courses}] Downloaded {course_url}")
83 |
84 | soup = BeautifulSoup(html, "html.parser")
85 |
86 | course_soup = soup.find("span", class_="light")
87 | course_number = course_soup.get_text().strip() if course_soup else None
88 |
89 | h1_soup = soup.select_one("#contentInner > h1:nth-child(1)")
90 | name = (
91 | h1_soup.find_all(string=True, recursive=False)[1].strip()
92 | if h1_soup and len(h1_soup.find_all(string=True, recursive=False)) >= 2
93 | else None
94 | )
95 |
96 | tuwel_match = re.search(
97 | r"https://tuwel\.tuwien\.ac\.at/course/view\.php\?id=\d+",
98 | html,
99 | )
100 | tuwel_url = tuwel_match.group(0) if tuwel_match else None
101 |
102 | registration_tables = [
103 | t for t in soup.find_all("table") if "Deregistration end" in t.get_text()
104 | ]
105 |
106 | registration_start, registration_end, deregistration_end = [None] * 3
107 | if registration_tables != []:
108 | table = registration_tables[0]
109 | date_texts = [f.get_text().strip() for f in table.find_all("td")]
110 |
111 | def safe_parse_date(date_str, fmt="%d.%m.%Y %H:%M"):
112 | try:
113 | return datetime.strptime(date_str, fmt)
114 | except ValueError:
115 | return None
116 |
117 | registration_start, registration_end, deregistration_end = [
118 | safe_parse_date(t) for t in date_texts
119 | ]
120 |
121 | return Course(
122 | id=course_number,
123 | name=name,
124 | tiss_url=course_url,
125 | tuwel_url=tuwel_url,
126 | registration_start=registration_start,
127 | registration_end=registration_end,
128 | deregistration_end=deregistration_end,
129 | )
130 |
131 |
132 | async def main():
133 | parser = argparse.ArgumentParser(
134 | prog="generate_courses.py",
135 | description="Gather information about all courses from TISS.",
136 | )
137 | parser.add_argument(
138 | "--debug-small",
139 | action="store_true",
140 | help="Only download a small set for debuggin purposes, since the full"
141 | + " set can take minutes.",
142 | )
143 | args = parser.parse_args()
144 |
145 | # setup the session
146 | global session
147 | session = TissClient()
148 |
149 | # 1) Download programs list
150 | print("Downloading program list ...")
151 | programs_url = "https://tiss.tuwien.ac.at/curriculum/studyCodes.xhtml"
152 |
153 | programs_html = await (await session.get(programs_url)).text()
154 | programs = re.findall(
155 | r"/curriculum/public/curriculum\.xhtml\?key=\d+", programs_html
156 | )
157 | programs = ["https://tiss.tuwien.ac.at" + p for p in set(programs)]
158 |
159 | if args.debug_small:
160 | programs = programs[:10]
161 |
162 | print(f"Found {len(programs)} programs")
163 |
164 | # 2) Download all courses
165 | global total_programs
166 | total_programs = len(programs)
167 | print("Downloading courses list ...")
168 | courses = set().union(
169 | *await asyncio.gather(*[fetch_program_courses(p) for p in programs])
170 | )
171 | courses = ["https://tiss.tuwien.ac.at" + c for c in set(courses)]
172 |
173 | if args.debug_small:
174 | courses = courses[:100]
175 |
176 | print(f"Found {len(courses)} courses")
177 |
178 | # 3) Download all the course info
179 | global total_courses
180 | total_courses = len(courses)
181 | course_infos: list[Course] = await asyncio.gather(
182 | *[fetch_course_info(c) for c in courses]
183 | )
184 |
185 | print("Writing to file courses.csv")
186 | course_infos = sorted(course_infos, key=lambda c: c.tiss_url)
187 | with open("courses.csv", "w", newline="") as csvfile:
188 | writer = csv.writer(csvfile)
189 | writer.writerow(
190 | [
191 | "Number",
192 | "Name",
193 | "TISS",
194 | "TUWEL",
195 | "Registration Start",
196 | "Registration End",
197 | "Deregistration End",
198 | ]
199 | )
200 | for course in course_infos:
201 | writer.writerow(
202 | [
203 | course.id,
204 | course.name,
205 | course.tiss_url,
206 | course.tuwel_url,
207 | course.registration_start.isoformat()
208 | if course.registration_start
209 | else None,
210 | course.registration_end.isoformat()
211 | if course.registration_end
212 | else None,
213 | course.deregistration_end.isoformat()
214 | if course.deregistration_end
215 | else None,
216 | ]
217 | )
218 |
219 | await session.close()
220 | print("Done ✨")
221 |
222 |
223 | asyncio.run(main())
224 |
--------------------------------------------------------------------------------
/app/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Better TISS Calendar
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 |
32 |
33 |
34 |
35 |
36 |
37 | Better TISS Calendar
38 | Readable titles, room locations
and much more...
39 |
40 |
41 |
185.A91 Einführung in die Programmierung 1
42 |
📌 GM 1 Audi. Max.- ARCH-INF
43 |
Vorlesung
44 |
45 |
46 |
➔
47 |
48 |
50 |
51 |
EP1 VU
52 |
📌 Getreidemarkt 9
53 |
54 |
55 | Einführung in die Programmierung 1
56 |
57 |
62 |
66 |
67 | Floor: Erdgeschoss
68 |
69 |
70 |
Vorlesung
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
86 |
87 |
90 |
92 |
93 |
🔥???
94 |
95 |
96 |
97 |
98 |
99 |
Options:
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
118 |
Copied!
120 |
121 |
122 |
123 |
124 |
126 |
enter tiss url above
127 |
128 |
130 |
131 |
132 |
133 |
140 |
141 |
142 |
143 |
144 |
145 |
How to subscribe:
146 |
147 |
148 |
149 | Subscribing to bettercal works just like the orignial TISS one.
150 |
151 |
152 |
iPhone/iPad/Mac
153 |
154 | - Open Calendar app
155 | - Go to File → New Calendar Subscription (Mac) or tap Add Calendar → Add Subscription Calendar (iOS)
156 | - Pate the url from above
157 |
158 |
159 |
Google Calendar
160 |
161 | - Open Google Calendar (in the Web)
162 | - Click the + next to "Other calendars"
163 | - Paste the url form above
164 |
165 |
166 |
167 |
168 | Having problems? Contact the developer
169 |
170 |
171 |
172 |
173 |
174 |
175 |
185 |
186 |
187 |
188 |
189 |
190 |
--------------------------------------------------------------------------------
/app/format.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import html
3 | import re
4 | import string
5 | from dataclasses import dataclass
6 | from datetime import datetime, timedelta
7 | from functools import cache
8 |
9 | import icalendar
10 | from icalendar import Component
11 |
12 | summary_regex = re.compile("([0-9A-Z]{3}\\.[0-9A-Z]{3}) ([A-Z]{2}) (.*)")
13 | word_split_regex = re.compile(
14 | re.escape(string.punctuation) + re.escape(string.whitespace)
15 | )
16 |
17 |
18 | class MultiLangString:
19 | "A string to hold multiple languages"
20 |
21 | def __init__(self, de: str, en: str | None = None) -> None:
22 | self.de = de
23 | self.en = en if en is not None else de
24 |
25 |
26 | @dataclass(kw_only=True, slots=True)
27 | class Event:
28 | name: str
29 | shorthand: str | None = None
30 | lecture_type: str
31 | additional: str | None = None
32 | number: str
33 | description: str
34 | address: str | None = None
35 | room: str | None = None
36 | room_code: str | None = None
37 | floor: MultiLangString | None = None
38 | tiss_url: str
39 | tuwel_url: str | None = None
40 | room_url: str | None = None
41 | map_url: str | None = None
42 | lecturetube_url: str | None = None
43 |
44 | def plain_description(self, lang: str) -> str:
45 | text = ""
46 | if self.shorthand is not None:
47 | text += f"{self.name} {self.lecture_type}\n"
48 | text += f"{self.description}\n\n"
49 |
50 | if self.room:
51 | text += "Room:\n" if lang == "en" else "Raum:\n"
52 | text += self.room
53 | if self.floor is not None:
54 | text += f", {self.floor.en if lang == 'en' else self.floor.de}"
55 | text += "\n"
56 | if self.map_url is not None:
57 | text += f"{self.map_url}\n"
58 | text += "\n"
59 |
60 | details = [
61 | (self.tiss_url, "TISS"),
62 | (self.tuwel_url, "TUWEL"),
63 | (self.room_url, "Room-Info" if lang == "en" else "Raum-Info"),
64 | (self.lecturetube_url, "Lecture Tube"),
65 | ]
66 |
67 | for url, name in filter(lambda d: d[0] is not None, details):
68 | text += f"{name}:\n{url}\n\n"
69 |
70 | return text.strip()
71 |
72 | def html_description(self, lang: str) -> str:
73 | text = ""
74 |
75 | if self.shorthand is not None:
76 | text += (
77 | f"{html.escape(self.name)} {html.escape(self.lecture_type)}
"
78 | )
79 | text += f"{html.escape(self.description)}
"
80 |
81 | if self.room:
82 | text += "Room:
" if lang == "en" else "Raum:
"
83 |
84 | if self.map_url:
85 | text += f'{html.escape(self.room)}'
86 | else:
87 | text += html.escape(self.room)
88 |
89 | if self.floor is not None:
90 | text += (
91 | f", {html.escape(self.floor.en if lang == 'en' else self.floor.de)}"
92 | )
93 |
94 | text += "
"
95 |
96 | details = [
97 | (self.tiss_url, "TISS"),
98 | (self.tuwel_url, "TUWEL"),
99 | (self.room_url, "Room-Info" if lang == "en" else "Raum-Info"),
100 | (self.lecturetube_url, "Lecture Tube"),
101 | ]
102 | details = [d for d in details if d[0] is not None]
103 |
104 | if details != []:
105 | text += "Details:
"
106 | for url, name in details:
107 | text += f'- {name}
'
108 | text += "
"
109 |
110 | return text
111 |
112 |
113 | def improve_calendar(
114 | cal: Component,
115 | use_shorthand: bool = True,
116 | google_cal: bool = False,
117 | locale: str | None = None,
118 | ) -> Component:
119 | if locale is None:
120 | locale = "de"
121 |
122 | seen_lecture_numbers: set[str] = set()
123 |
124 | for component in cal.walk():
125 | if component.name != "VEVENT" or not summary_regex.match(
126 | component.get("summary")
127 | ):
128 | continue
129 |
130 | # Parse the event and enrich it
131 | event = event_from_ical(component)
132 | seen_lecture_numbers.add(event.number)
133 | if use_shorthand:
134 | event.shorthand = create_shorthand(event.name)
135 | event = add_location(event)
136 |
137 | # Serialize the summary
138 | summary = event.shorthand if event.shorthand is not None else event.name
139 | summary += f" {event.lecture_type}"
140 | if event.additional is not None:
141 | summary += " - " + event.additional
142 | component.pop("summary")
143 | component.add("summary", summary)
144 |
145 | # Add tuwel
146 | if course := read_courses().get(event.number, None):
147 | event.tuwel_url = course.tuwel_url
148 | # Serialize the address
149 | if event.address is not None:
150 | component.pop("location")
151 | component.add("location", event.address)
152 |
153 | # Serialize the description
154 | plain_description = event.plain_description(locale)
155 | html_description = event.html_description(locale)
156 |
157 | component.pop("description")
158 | if google_cal:
159 | # So google calendar ignored the standard that says that the
160 | # description should only contain plain text. Then when some clients
161 | # (rightfully so) didn't support html in the description where the
162 | # standard says that there should be no html, they blamed it on the
163 | # clients and gaslight them.
164 | # Normally, I would congratulate such a bold and wrongfully confident
165 | # move but now I need to adapt to it in my code and I am pissed.
166 | component.add("description", html_description)
167 | else:
168 | component.add("description", plain_description)
169 | component.add("x-alt-desc;fmttype=text/html", html_description)
170 |
171 | # Insert signup dates
172 | for lecture in sorted(seen_lecture_numbers):
173 | course = read_courses().get(lecture, None)
174 | if course is None:
175 | continue
176 |
177 | # FIXME:
178 | # We cannot insert the url directly to the singup, because it requries
179 | # the semester and we don't know for which semester you are signed up for
180 | # (we could invent some heuristic though).
181 | # So for now just insert the default url to TISS, the user will figure
182 | # it out.
183 |
184 | name = course.name
185 |
186 | if course.registration_start is not None:
187 | signup = icalendar.Event()
188 | signup.add(
189 | "summary",
190 | f"Anmeldung {name}" if locale == "de" else f"Signup {name}",
191 | )
192 | signup.add("dtstart", course.registration_start)
193 | end = course.registration_start + timedelta(minutes=30)
194 |
195 | # Some signups start at midnight but are easy to oversee so lets
196 | # stretch them.
197 | if end.hour < 8:
198 | end = end.replace(hour=8, minute=0)
199 |
200 | signup.add("dtend", end)
201 | signup.add("categories", "COURSE")
202 |
203 | description = f"Registrations for {name} are now open."
204 | description_html = (
205 | f"Registrations for {name} are now open.\n"
206 | f'Register on Tiss'
207 | )
208 | if google_cal:
209 | signup.add("description", description_html)
210 | else:
211 | signup.add("description", description)
212 | signup.add("url", course.tiss_url)
213 | signup.add(
214 | "x-alt-desc;fmttype=text/html",
215 | f'Register on Tiss',
216 | )
217 |
218 | cal.add_component(signup)
219 |
220 | # Set some metadata
221 | cal.pop("prodid")
222 | cal.add("prodid", "-//flofriday//Better TISS CAL//EN")
223 | cal.add("name", "Better TISS")
224 | cal.add("x-wr-calname", "Better TISS")
225 |
226 | return cal
227 |
228 |
229 | def event_from_ical(component) -> Event:
230 | summary = component.get("summary")
231 | match = summary_regex.match(summary)
232 |
233 | [number, lecture_type, name] = match.groups() # type: ignore
234 | additional = ""
235 | if " - " in name:
236 | [name, additional] = name.rsplit(" - ", 1)
237 |
238 | room = component.get("location")
239 | description = component.get("description")
240 |
241 | # FIXME: We could also set the semester, but that would mean we would need
242 | # to calculate the semester.
243 | tiss_url = (
244 | "https://tiss.tuwien.ac.at/course/courseDetails.xhtml?courseNr="
245 | + number.strip().replace(".", "")
246 | )
247 |
248 | return Event(
249 | name=name,
250 | number=number,
251 | lecture_type=lecture_type,
252 | additional=additional if additional.strip() else None,
253 | room=room if room.strip() else None,
254 | description=description,
255 | tiss_url=tiss_url,
256 | )
257 |
258 |
259 | def create_shorthand(name: str) -> str:
260 | shorthands = read_shorthands()
261 | if name.lower() in shorthands:
262 | return shorthands[name.lower()].upper()
263 |
264 | return create_shorthand_fallback(name)
265 |
266 |
267 | def create_shorthand_fallback(name: str) -> str:
268 | # Shorthands are all the uppercase letters
269 | iter = filter(lambda c: c.isupper(), name)
270 | shorthand = "".join(iter)
271 | if is_valid_shorthand(shorthand):
272 | return shorthand
273 |
274 | # Shorthands are the first letters of all capitalized words.
275 | iter = word_split_regex.split(name)
276 | iter = filter(lambda w: len(w) > 1 and w[0].isupper(), iter)
277 | iter = map(lambda w: w[0], iter)
278 | shorthand = "".join(iter)
279 |
280 | # The generated shorthand can be somewhat bad so lets add some checks:
281 | if is_valid_shorthand(shorthand):
282 | return shorthand
283 |
284 | # Couldn't generate a shorthand, default to original
285 | return name
286 |
287 |
288 | def is_valid_shorthand(shorthand: str) -> bool:
289 | """The generated shorthand has to be meaning full without beeing offending.
290 |
291 | Requirements:
292 | - At least 2 Symbols
293 | - At most 6
294 | - No offending words
295 | """
296 |
297 | if len(shorthand) < 2 or len(shorthand) > 6:
298 | return False
299 |
300 | forbidden = ["SS", "NAZI"]
301 | return shorthand not in forbidden
302 |
303 |
304 | def add_location(event: Event) -> Event:
305 | rooms = read_rooms()
306 | if event.room not in rooms:
307 | return event
308 |
309 | (address, floor, code, url) = rooms[event.room]
310 | event.address = address
311 |
312 | # FIXME: the floor information in the dataset is all over the place.
313 | # We should create a better more universal dataset
314 | event.floor = floor if floor is not None else create_floor_fallback(code)
315 | event.room_code = code
316 | event.room_url = url
317 | event.map_url = f"https://maps.tuwien.ac.at/?q={code}#map"
318 | if code in read_lecturetube_available_rooms():
319 | event.lecturetube_url = (
320 | f"https://live.video.tuwien.ac.at/room/{code}/player.html"
321 | )
322 | return event
323 |
324 |
325 | def create_floor_fallback(room_code: str) -> MultiLangString | None:
326 | """The floor information is encoded in the room code.
327 |
328 | Format: TTFFRR[R]
329 | In that format TT is the trackt, FF the floor and RR the room specific
330 | code.
331 | """
332 |
333 | if len(room_code) < 6 or len(room_code) > 7:
334 | return None
335 |
336 | floor_code = room_code[2:4]
337 |
338 | if floor_code.isnumeric():
339 | floor = int(floor_code)
340 | return MultiLangString(f"{floor}. Stock", f"{floor}. Floor")
341 | elif floor_code == "EG":
342 | return MultiLangString("Erdgeschoß", "Ground Floor")
343 | elif floor_code == "DG":
344 | return MultiLangString("Dachgeschoß", "Roof Floor")
345 | elif floor_code[0] == "U" and floor_code[1].isnumeric():
346 | floor = int(floor_code[1])
347 | return MultiLangString(f"{floor}. Untergeschoß", f"{floor}. Underground Floor")
348 | else:
349 | return MultiLangString(floor_code)
350 |
351 |
352 | @dataclass(frozen=True)
353 | class Course:
354 | id: str | None
355 | name: str | None
356 | tiss_url: str
357 | tuwel_url: str | None
358 | registration_start: datetime | None
359 | registration_end: datetime | None
360 | deregistration_end: datetime | None
361 |
362 |
363 | @cache
364 | def read_courses() -> dict[str, Course]:
365 | id_to_course: dict[str, Course] = {}
366 |
367 | # FIXME: Probably refactor into something that can be reused to also process
368 | # all the other informations.
369 | with open("app/resources/courses.csv") as f:
370 | # The first line is a header
371 | reader = csv.reader(f)
372 | next(reader)
373 | for row in reader:
374 | (
375 | id,
376 | name,
377 | tiss,
378 | tuwel,
379 | registration_start,
380 | registration_end,
381 | deregistration_end,
382 | ) = row
383 |
384 | if id is None or id == "":
385 | continue
386 |
387 | name = name.strip() if name.strip() != "" else None
388 | tuwel = tuwel.strip() if tuwel.strip() != "" else None
389 | registration_start = (
390 | datetime.fromisoformat(registration_start)
391 | if registration_start.strip() != ""
392 | else None
393 | )
394 | registration_end = (
395 | datetime.fromisoformat(registration_end)
396 | if registration_end.strip() != ""
397 | else None
398 | )
399 | deregistration_end = (
400 | datetime.fromisoformat(deregistration_end)
401 | if deregistration_end.strip() != ""
402 | else None
403 | )
404 | course = Course(
405 | id,
406 | name,
407 | tiss,
408 | tuwel,
409 | registration_start,
410 | registration_end,
411 | deregistration_end,
412 | )
413 |
414 | id_to_course[id] = course
415 |
416 | return id_to_course
417 |
418 |
419 | @cache
420 | def read_shorthands() -> dict[str, str]:
421 | with open("app/resources/shorthands.csv") as f:
422 | # The first line is a header
423 | lines = f.readlines()[1:]
424 |
425 | shorthands = {}
426 | for line in lines:
427 | shorthand, lecture_de, lecture_en = line.split(",")
428 | lecture_de = lecture_de.lower().strip()
429 | lecture_en = lecture_en.lower().strip()
430 |
431 | if lecture_de != "" and lecture_de != "N/A":
432 | shorthands[lecture_de] = shorthand
433 | if lecture_en != "" and lecture_en != "N/A":
434 | shorthands[lecture_en] = shorthand
435 |
436 | return shorthands
437 |
438 |
439 | @cache
440 | def read_rooms() -> dict[str, tuple[str, MultiLangString, str, str]]:
441 | upper_floor_patttern = re.compile(r"(\d). ?(Stock|Obergescho(ss|ß)|OG)")
442 | ground_floor_patttern = re.compile(r"(EG|Erdgescho(ss|ß))")
443 | roof_floor_patttern = re.compile(r"(DG|Erdgescho(ss|ß))")
444 |
445 | with open("app/resources/rooms.csv") as f:
446 | reader = csv.reader(f)
447 |
448 | rooms = {}
449 | for fields in reader:
450 | name = fields[0]
451 | address = fields[6].split(",")[0].strip()
452 |
453 | # The floor information is in any of the address fields but never
454 | # the first one
455 | floor_fields = fields[6].split(",")[1:]
456 | keywords = ["OG", "UG", "DG", "EG", "Stock", "geschoß", "geschoss"]
457 | floor_fields = [
458 | field for field in floor_fields if any([kw in field for kw in keywords])
459 | ]
460 |
461 | # Parsing the floor description for localization and consistency
462 | floor = None
463 | if floor_fields != []:
464 | floor_description = floor_fields[0].strip()
465 |
466 | if (match := upper_floor_patttern.match(floor_description)) is not None:
467 | number = int(match.group(1))
468 | floor = MultiLangString(f"{number}. Stock", f"{number}. Floor")
469 | elif ground_floor_patttern.match(floor_description) is not None:
470 | floor = MultiLangString("Erdgeschoß", "Ground Floor")
471 | elif roof_floor_patttern.match(floor_description) is not None:
472 | floor = MultiLangString("Dachgeschoß", "Roof Floor")
473 | else:
474 | floor = MultiLangString(floor_description)
475 |
476 | code = fields[7].strip()
477 | url = fields[8].strip()
478 |
479 | rooms[name] = (address, floor, code, url)
480 |
481 | return rooms
482 |
483 |
484 | @cache
485 | def read_lecturetube_available_rooms() -> set[str]:
486 | available_rooms = set()
487 |
488 | with open("app/resources/lecturetube_availability.csv") as f:
489 | reader = csv.reader(f)
490 |
491 | for fields in reader:
492 | available_rooms.add(fields[0])
493 | return available_rooms
494 |
--------------------------------------------------------------------------------
/tests/calendar_de.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:-//Tiss Events Calendar//iCal4j 1.0//EN
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | BEGIN:VTIMEZONE
6 | TZID:Europe/Vienna
7 | LAST-MODIFIED:20201010T011803Z
8 | TZURL:http://tzurl.org/zoneinfo/Europe/Vienna
9 | X-LIC-LOCATION:Europe/Vienna
10 | X-PROLEPTIC-TZNAME:LMT
11 | BEGIN:STANDARD
12 | TZNAME:CET
13 | TZOFFSETFROM:+010521
14 | TZOFFSETTO:+0100
15 | DTSTART:18930401T000000
16 | END:STANDARD
17 | BEGIN:DAYLIGHT
18 | TZNAME:CEST
19 | TZOFFSETFROM:+0100
20 | TZOFFSETTO:+0200
21 | DTSTART:19160430T230000
22 | RDATE:19200405T020000
23 | RDATE:19400401T020000
24 | RDATE:19430329T020000
25 | RDATE:19440403T020000
26 | RDATE:19450402T020000
27 | RDATE:19460414T020000
28 | RDATE:19470406T020000
29 | RDATE:19480418T020000
30 | RDATE:19800406T000000
31 | END:DAYLIGHT
32 | BEGIN:STANDARD
33 | TZNAME:CET
34 | TZOFFSETFROM:+0200
35 | TZOFFSETTO:+0100
36 | DTSTART:19161001T010000
37 | RDATE:19200913T030000
38 | RDATE:19421102T030000
39 | RDATE:19431004T030000
40 | RDATE:19441002T030000
41 | RDATE:19450412T030000
42 | RDATE:19461007T030000
43 | RDATE:19800928T000000
44 | END:STANDARD
45 | BEGIN:DAYLIGHT
46 | TZNAME:CEST
47 | TZOFFSETFROM:+0100
48 | TZOFFSETTO:+0200
49 | DTSTART:19170416T020000
50 | RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
51 | END:DAYLIGHT
52 | BEGIN:STANDARD
53 | TZNAME:CET
54 | TZOFFSETFROM:+0200
55 | TZOFFSETTO:+0100
56 | DTSTART:19170917T030000
57 | RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
58 | END:STANDARD
59 | BEGIN:STANDARD
60 | TZNAME:CET
61 | TZOFFSETFROM:+0100
62 | TZOFFSETTO:+0100
63 | DTSTART:19200101T000000
64 | RDATE:19460101T000000
65 | END:STANDARD
66 | BEGIN:STANDARD
67 | TZNAME:CET
68 | TZOFFSETFROM:+0200
69 | TZOFFSETTO:+0100
70 | DTSTART:19471005T030000
71 | RRULE:FREQ=YEARLY;UNTIL=19481003T010000Z;BYMONTH=10;BYDAY=1SU
72 | END:STANDARD
73 | BEGIN:DAYLIGHT
74 | TZNAME:CEST
75 | TZOFFSETFROM:+0100
76 | TZOFFSETTO:+0200
77 | DTSTART:19810329T020000
78 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
79 | END:DAYLIGHT
80 | BEGIN:STANDARD
81 | TZNAME:CET
82 | TZOFFSETFROM:+0200
83 | TZOFFSETTO:+0100
84 | DTSTART:19810927T030000
85 | RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
86 | END:STANDARD
87 | BEGIN:STANDARD
88 | TZNAME:CET
89 | TZOFFSETFROM:+0200
90 | TZOFFSETTO:+0100
91 | DTSTART:19961027T030000
92 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
93 | END:STANDARD
94 | END:VTIMEZONE
95 | BEGIN:VEVENT
96 | DTSTAMP:20230824T192206Z
97 | DTSTART;TZID=Europe/Vienna:20230526T110000
98 | DTEND;TZID=Europe/Vienna:20230526T130000
99 | SUMMARY:185.208 VU Programmiersprachen
100 | LOCATION:EI 5 Hochenegg HS
101 | CATEGORIES:COURSE
102 | DESCRIPTION:Vorlesung
103 | UID:20240303T172756Z-4743490@tiss.tuwien.ac.at
104 | END:VEVENT
105 | BEGIN:VEVENT
106 | DTSTAMP:20230824T192206Z
107 | DTSTART;TZID=Europe/Vienna:20230602T110000
108 | DTEND;TZID=Europe/Vienna:20230602T130000
109 | SUMMARY:185.208 VU Programmiersprachen
110 | LOCATION:EI 5 Hochenegg HS
111 | CATEGORIES:COURSE
112 | DESCRIPTION:Vorlesung
113 | UID:20240303T172757Z-4743491@tiss.tuwien.ac.at
114 | END:VEVENT
115 | BEGIN:VEVENT
116 | DTSTAMP:20230824T192206Z
117 | DTSTART;TZID=Europe/Vienna:20230605T140000
118 | DTEND;TZID=Europe/Vienna:20230605T180000
119 | SUMMARY:185.307 SE Seminar aus Programmiersprachen
120 | LOCATION:EI 6 Eckert HS
121 | CATEGORIES:COURSE
122 | DESCRIPTION:Seminar
123 | UID:20240303T172758Z-4804905@tiss.tuwien.ac.at
124 | END:VEVENT
125 | BEGIN:VEVENT
126 | DTSTAMP:20230824T192206Z
127 | DTSTART;TZID=Europe/Vienna:20230616T110000
128 | DTEND;TZID=Europe/Vienna:20230616T130000
129 | SUMMARY:185.208 VU Programmiersprachen
130 | LOCATION:EI 5 Hochenegg HS
131 | CATEGORIES:COURSE
132 | DESCRIPTION:Vorlesung
133 | UID:20240303T172759Z-4743492@tiss.tuwien.ac.at
134 | END:VEVENT
135 | BEGIN:VEVENT
136 | DTSTAMP:20230824T192206Z
137 | DTSTART;TZID=Europe/Vienna:20230616T143000
138 | DTEND;TZID=Europe/Vienna:20230616T163000
139 | SUMMARY:184.260 VU Distributed Systems Technologies - G6
140 | LOCATION:Zoom
141 | CATEGORIES:GROUP
142 | DESCRIPTION:Assignment 3 - Diskussion
143 | UID:20240303T172800Z-4806598@tiss.tuwien.ac.at
144 | END:VEVENT
145 | BEGIN:VEVENT
146 | DTSTAMP:20230824T192206Z
147 | DTSTART;TZID=Europe/Vienna:20230627T110000
148 | DTEND;TZID=Europe/Vienna:20230627T130000
149 | SUMMARY:184.260 VU Distributed Systems Technologies (Prüfung) - Prüfung
150 | LOCATION:EI 7 Hörsaal - ETIT
151 | CATEGORIES:EXAM
152 | DESCRIPTION:184.260 Distributed Systems Technologies Prüfung
153 | UID:20240303T172801Z-4715071@tiss.tuwien.ac.at
154 | END:VEVENT
155 | BEGIN:VEVENT
156 | DTSTAMP:20251011T114455Z
157 | DTSTART;TZID=Europe/Vienna:20251001T160000
158 | DTEND;TZID=Europe/Vienna:20251001T180000
159 | SUMMARY:194.190 VU Low-Level Programming
160 | LOCATION:EI 1 Petritsch HS
161 | CATEGORIES:COURSE
162 | DESCRIPTION:Vorlesungen und Übungspräsentationen
163 | UID:20251025T030104Z-5268788@tiss.tuwien.ac.at
164 | END:VEVENT
165 | BEGIN:VEVENT
166 | DTSTAMP:20251011T114455Z
167 | DTSTART;TZID=Europe/Vienna:20251002T110000
168 | DTEND;TZID=Europe/Vienna:20251002T120000
169 | SUMMARY:194.193 VU AI Programming
170 | LOCATION:FAV Hörsaal 1 Helmut Veith - INF
171 | CATEGORIES:COURSE
172 | DESCRIPTION:Kick-off
173 | UID:20251025T030105Z-5297642@tiss.tuwien.ac.at
174 | END:VEVENT
175 | BEGIN:VEVENT
176 | DTSTAMP:20251011T114455Z
177 | DTSTART;TZID=Europe/Vienna:20251008T150000
178 | DTEND;TZID=Europe/Vienna:20251008T160000
179 | SUMMARY:194.207 VU Generative AI
180 | LOCATION:https://tuwien.zoom.us/j/68659854514?pwd=rGPyiiUGa2FPP9KyDiQaVIc
181 | HuWtn8o.1
182 | CATEGORIES:COURSE
183 | DESCRIPTION:Vorbesprechung
184 | UID:20251025T030106Z-5342139@tiss.tuwien.ac.at
185 | END:VEVENT
186 | BEGIN:VEVENT
187 | DTSTAMP:20251011T114455Z
188 | DTSTART;TZID=Europe/Vienna:20251008T160000
189 | DTEND;TZID=Europe/Vienna:20251008T180000
190 | SUMMARY:194.190 VU Low-Level Programming
191 | LOCATION:EI 1 Petritsch HS
192 | CATEGORIES:COURSE
193 | DESCRIPTION:Vorlesungen und Übungspräsentationen
194 | UID:20251025T030107Z-5268789@tiss.tuwien.ac.at
195 | END:VEVENT
196 | BEGIN:VEVENT
197 | DTSTAMP:20251011T114455Z
198 | DTSTART;TZID=Europe/Vienna:20251009T100000
199 | DTEND;TZID=Europe/Vienna:20251009T120000
200 | SUMMARY:194.193 VU AI Programming
201 | LOCATION:FAV Hörsaal 2
202 | CATEGORIES:COURSE
203 | DESCRIPTION:Vorlesung
204 | UID:20251025T030108Z-5297571@tiss.tuwien.ac.at
205 | END:VEVENT
206 | BEGIN:VEVENT
207 | DTSTAMP:20251011T114455Z
208 | DTSTART;TZID=Europe/Vienna:20251010T140000
209 | DTEND;TZID=Europe/Vienna:20251010T160000
210 | SUMMARY:187.250 VO Zwischen Karriere und Barriere
211 | LOCATION:HS 18 Czuber - MB
212 | CATEGORIES:COURSE
213 | DESCRIPTION:Inhaltliche und organisatorische Einführung
214 | UID:20251025T030109Z-5317900@tiss.tuwien.ac.at
215 | END:VEVENT
216 | BEGIN:VEVENT
217 | DTSTAMP:20251011T114455Z
218 | DTSTART;TZID=Europe/Vienna:20251015T100000
219 | DTEND;TZID=Europe/Vienna:20251015T120000
220 | SUMMARY:192.065 VU Cryptocurrencies
221 | LOCATION:EI 8 Pötzl HS - QUER
222 | CATEGORIES:COURSE
223 | DESCRIPTION:VU 192.065 Cryptocurrencies (first lecture)
224 | UID:20251025T030110Z-5297619@tiss.tuwien.ac.at
225 | END:VEVENT
226 | BEGIN:VEVENT
227 | DTSTAMP:20251011T114455Z
228 | DTSTART;TZID=Europe/Vienna:20251015T150000
229 | DTEND;TZID=Europe/Vienna:20251015T170000
230 | SUMMARY:194.207 VU Generative AI
231 | LOCATION:Informatikhörsaal - ARCH-INF
232 | CATEGORIES:COURSE
233 | DESCRIPTION:Vorlesung
234 | UID:20251025T030111Z-5340287@tiss.tuwien.ac.at
235 | END:VEVENT
236 | BEGIN:VEVENT
237 | DTSTAMP:20251011T114455Z
238 | DTSTART;TZID=Europe/Vienna:20251015T160000
239 | DTEND;TZID=Europe/Vienna:20251015T180000
240 | SUMMARY:194.190 VU Low-Level Programming
241 | LOCATION:EI 1 Petritsch HS
242 | CATEGORIES:COURSE
243 | DESCRIPTION:Vorlesungen und Übungspräsentationen
244 | UID:20251025T030112Z-5268790@tiss.tuwien.ac.at
245 | END:VEVENT
246 | BEGIN:VEVENT
247 | DTSTAMP:20251011T114455Z
248 | DTSTART;TZID=Europe/Vienna:20251016T100000
249 | DTEND;TZID=Europe/Vienna:20251016T120000
250 | SUMMARY:194.193 VU AI Programming
251 | LOCATION:FAV Hörsaal 2
252 | CATEGORIES:COURSE
253 | DESCRIPTION:Vorlesung
254 | UID:20251025T030113Z-5297572@tiss.tuwien.ac.at
255 | END:VEVENT
256 | BEGIN:VEVENT
257 | DTSTAMP:20251011T114455Z
258 | DTSTART;TZID=Europe/Vienna:20251022T100000
259 | DTEND;TZID=Europe/Vienna:20251022T120000
260 | SUMMARY:192.065 VU Cryptocurrencies
261 | LOCATION:EI 8 Pötzl HS - QUER
262 | CATEGORIES:COURSE
263 | DESCRIPTION:VU 192.065 Cryptocurrencies
264 | UID:20251025T030114Z-5297660@tiss.tuwien.ac.at
265 | END:VEVENT
266 | BEGIN:VEVENT
267 | DTSTAMP:20251011T114455Z
268 | DTSTART;TZID=Europe/Vienna:20251022T150000
269 | DTEND;TZID=Europe/Vienna:20251022T170000
270 | SUMMARY:194.207 VU Generative AI
271 | LOCATION:Informatikhörsaal - ARCH-INF
272 | CATEGORIES:COURSE
273 | DESCRIPTION:Vorlesung
274 | UID:20251025T030115Z-5340288@tiss.tuwien.ac.at
275 | END:VEVENT
276 | BEGIN:VEVENT
277 | DTSTAMP:20251011T114455Z
278 | DTSTART;TZID=Europe/Vienna:20251022T160000
279 | DTEND;TZID=Europe/Vienna:20251022T180000
280 | SUMMARY:194.190 VU Low-Level Programming
281 | LOCATION:EI 1 Petritsch HS
282 | CATEGORIES:COURSE
283 | DESCRIPTION:Vorlesungen und Übungspräsentationen
284 | UID:20251025T030116Z-5268791@tiss.tuwien.ac.at
285 | END:VEVENT
286 | BEGIN:VEVENT
287 | DTSTAMP:20251011T114455Z
288 | DTSTART;TZID=Europe/Vienna:20251023T100000
289 | DTEND;TZID=Europe/Vienna:20251023T120000
290 | SUMMARY:194.193 VU AI Programming
291 | LOCATION:FAV Hörsaal 2
292 | CATEGORIES:COURSE
293 | DESCRIPTION:Vorlesung
294 | UID:20251025T030117Z-5297573@tiss.tuwien.ac.at
295 | END:VEVENT
296 | BEGIN:VEVENT
297 | DTSTAMP:20251011T114455Z
298 | DTSTART;TZID=Europe/Vienna:20251029T100000
299 | DTEND;TZID=Europe/Vienna:20251029T120000
300 | SUMMARY:192.065 VU Cryptocurrencies
301 | LOCATION:EI 8 Pötzl HS - QUER
302 | CATEGORIES:COURSE
303 | DESCRIPTION:VU 192.065 Cryptocurrencies
304 | UID:20251025T030118Z-5297661@tiss.tuwien.ac.at
305 | END:VEVENT
306 | BEGIN:VEVENT
307 | DTSTAMP:20251011T114455Z
308 | DTSTART;TZID=Europe/Vienna:20251029T150000
309 | DTEND;TZID=Europe/Vienna:20251029T170000
310 | SUMMARY:194.207 VU Generative AI
311 | LOCATION:Informatikhörsaal - ARCH-INF
312 | CATEGORIES:COURSE
313 | DESCRIPTION:Vorlesung
314 | UID:20251025T030119Z-5340289@tiss.tuwien.ac.at
315 | END:VEVENT
316 | BEGIN:VEVENT
317 | DTSTAMP:20251011T114455Z
318 | DTSTART;TZID=Europe/Vienna:20251029T160000
319 | DTEND;TZID=Europe/Vienna:20251029T180000
320 | SUMMARY:194.190 VU Low-Level Programming
321 | LOCATION:EI 1 Petritsch HS
322 | CATEGORIES:COURSE
323 | DESCRIPTION:Vorlesungen und Übungspräsentationen
324 | UID:20251025T030120Z-5268792@tiss.tuwien.ac.at
325 | END:VEVENT
326 | BEGIN:VEVENT
327 | DTSTAMP:20251011T114455Z
328 | DTSTART;TZID=Europe/Vienna:20251030T100000
329 | DTEND;TZID=Europe/Vienna:20251030T120000
330 | SUMMARY:194.193 VU AI Programming
331 | LOCATION:FAV Hörsaal 2
332 | CATEGORIES:COURSE
333 | DESCRIPTION:Vorlesung
334 | UID:20251025T030121Z-5297574@tiss.tuwien.ac.at
335 | END:VEVENT
336 | BEGIN:VEVENT
337 | DTSTAMP:20251011T114455Z
338 | DTSTART;TZID=Europe/Vienna:20251105T100000
339 | DTEND;TZID=Europe/Vienna:20251105T120000
340 | SUMMARY:192.065 VU Cryptocurrencies
341 | LOCATION:EI 8 Pötzl HS - QUER
342 | CATEGORIES:COURSE
343 | DESCRIPTION:VU 192.065 Cryptocurrencies
344 | UID:20251025T030122Z-5297662@tiss.tuwien.ac.at
345 | END:VEVENT
346 | BEGIN:VEVENT
347 | DTSTAMP:20251011T114455Z
348 | DTSTART;TZID=Europe/Vienna:20251105T150000
349 | DTEND;TZID=Europe/Vienna:20251105T170000
350 | SUMMARY:194.207 VU Generative AI
351 | LOCATION:Informatikhörsaal - ARCH-INF
352 | CATEGORIES:COURSE
353 | DESCRIPTION:Vorlesung
354 | UID:20251025T030123Z-5340290@tiss.tuwien.ac.at
355 | END:VEVENT
356 | BEGIN:VEVENT
357 | DTSTAMP:20251011T114455Z
358 | DTSTART;TZID=Europe/Vienna:20251105T160000
359 | DTEND;TZID=Europe/Vienna:20251105T180000
360 | SUMMARY:194.190 VU Low-Level Programming
361 | LOCATION:EI 1 Petritsch HS
362 | CATEGORIES:COURSE
363 | DESCRIPTION:Vorlesungen und Übungspräsentationen
364 | UID:20251025T030124Z-5268793@tiss.tuwien.ac.at
365 | END:VEVENT
366 | BEGIN:VEVENT
367 | DTSTAMP:20251011T114455Z
368 | DTSTART;TZID=Europe/Vienna:20251106T100000
369 | DTEND;TZID=Europe/Vienna:20251106T120000
370 | SUMMARY:194.193 VU AI Programming
371 | LOCATION:FAV Hörsaal 2
372 | CATEGORIES:COURSE
373 | DESCRIPTION:Vorlesung
374 | UID:20251025T030125Z-5297575@tiss.tuwien.ac.at
375 | END:VEVENT
376 | BEGIN:VEVENT
377 | DTSTAMP:20251011T114455Z
378 | DTSTART;TZID=Europe/Vienna:20251107T120000
379 | DTEND;TZID=Europe/Vienna:20251107T150000
380 | SUMMARY:187.250 VO Zwischen Karriere und Barriere
381 | LOCATION:HS 18 Czuber - MB
382 | CATEGORIES:COURSE
383 | DESCRIPTION:2. VO
384 | UID:20251025T030126Z-5317901@tiss.tuwien.ac.at
385 | END:VEVENT
386 | BEGIN:VEVENT
387 | DTSTAMP:20251011T114455Z
388 | DTSTART;TZID=Europe/Vienna:20251112T100000
389 | DTEND;TZID=Europe/Vienna:20251112T120000
390 | SUMMARY:192.065 VU Cryptocurrencies
391 | LOCATION:EI 8 Pötzl HS - QUER
392 | CATEGORIES:COURSE
393 | DESCRIPTION:VU 192.065 Cryptocurrencies
394 | UID:20251025T030127Z-5297663@tiss.tuwien.ac.at
395 | END:VEVENT
396 | BEGIN:VEVENT
397 | DTSTAMP:20251011T114455Z
398 | DTSTART;TZID=Europe/Vienna:20251112T150000
399 | DTEND;TZID=Europe/Vienna:20251112T170000
400 | SUMMARY:194.207 VU Generative AI
401 | LOCATION:Informatikhörsaal - ARCH-INF
402 | CATEGORIES:COURSE
403 | DESCRIPTION:Vorlesung
404 | UID:20251025T030128Z-5340291@tiss.tuwien.ac.at
405 | END:VEVENT
406 | BEGIN:VEVENT
407 | DTSTAMP:20251011T114455Z
408 | DTSTART;TZID=Europe/Vienna:20251112T160000
409 | DTEND;TZID=Europe/Vienna:20251112T180000
410 | SUMMARY:194.190 VU Low-Level Programming
411 | LOCATION:EI 1 Petritsch HS
412 | CATEGORIES:COURSE
413 | DESCRIPTION:Vorlesungen und Übungspräsentationen
414 | UID:20251025T030129Z-5268794@tiss.tuwien.ac.at
415 | END:VEVENT
416 | BEGIN:VEVENT
417 | DTSTAMP:20251011T114455Z
418 | DTSTART;TZID=Europe/Vienna:20251113T100000
419 | DTEND;TZID=Europe/Vienna:20251113T120000
420 | SUMMARY:194.193 VU AI Programming
421 | LOCATION:FAV Hörsaal 2
422 | CATEGORIES:COURSE
423 | DESCRIPTION:Vorlesung
424 | UID:20251025T030130Z-5297576@tiss.tuwien.ac.at
425 | END:VEVENT
426 | BEGIN:VEVENT
427 | DTSTAMP:20251011T114455Z
428 | DTSTART;TZID=Europe/Vienna:20251119T100000
429 | DTEND;TZID=Europe/Vienna:20251119T120000
430 | SUMMARY:192.065 VU Cryptocurrencies
431 | LOCATION:EI 8 Pötzl HS - QUER
432 | CATEGORIES:COURSE
433 | DESCRIPTION:VU 192.065 Cryptocurrencies
434 | UID:20251025T030131Z-5297664@tiss.tuwien.ac.at
435 | END:VEVENT
436 | BEGIN:VEVENT
437 | DTSTAMP:20251011T114455Z
438 | DTSTART;TZID=Europe/Vienna:20251119T150000
439 | DTEND;TZID=Europe/Vienna:20251119T170000
440 | SUMMARY:194.207 VU Generative AI
441 | LOCATION:Informatikhörsaal - ARCH-INF
442 | CATEGORIES:COURSE
443 | DESCRIPTION:Vorlesung
444 | UID:20251025T030132Z-5340292@tiss.tuwien.ac.at
445 | END:VEVENT
446 | BEGIN:VEVENT
447 | DTSTAMP:20251011T114455Z
448 | DTSTART;TZID=Europe/Vienna:20251119T160000
449 | DTEND;TZID=Europe/Vienna:20251119T180000
450 | SUMMARY:194.190 VU Low-Level Programming
451 | LOCATION:EI 1 Petritsch HS
452 | CATEGORIES:COURSE
453 | DESCRIPTION:Vorlesungen und Übungspräsentationen
454 | UID:20251025T030133Z-5268795@tiss.tuwien.ac.at
455 | END:VEVENT
456 | BEGIN:VEVENT
457 | DTSTAMP:20251011T114455Z
458 | DTSTART;TZID=Europe/Vienna:20251120T100000
459 | DTEND;TZID=Europe/Vienna:20251120T120000
460 | SUMMARY:194.193 VU AI Programming
461 | LOCATION:Zoom
462 | CATEGORIES:COURSE
463 | DESCRIPTION:Assignment Discussion Session A1 & A2
464 | UID:20251025T030134Z-5340499@tiss.tuwien.ac.at
465 | END:VEVENT
466 | BEGIN:VEVENT
467 | DTSTAMP:20251011T114455Z
468 | DTSTART;TZID=Europe/Vienna:20251121T120000
469 | DTEND;TZID=Europe/Vienna:20251121T150000
470 | SUMMARY:187.250 VO Zwischen Karriere und Barriere
471 | LOCATION:HS 18 Czuber - MB
472 | CATEGORIES:COURSE
473 | DESCRIPTION:3. VO
474 | UID:20251025T030135Z-5317902@tiss.tuwien.ac.at
475 | END:VEVENT
476 | BEGIN:VEVENT
477 | DTSTAMP:20251011T114455Z
478 | DTSTART;TZID=Europe/Vienna:20251126T100000
479 | DTEND;TZID=Europe/Vienna:20251126T120000
480 | SUMMARY:192.065 VU Cryptocurrencies
481 | LOCATION:EI 8 Pötzl HS - QUER
482 | CATEGORIES:COURSE
483 | DESCRIPTION:VU 192.065 Cryptocurrencies
484 | UID:20251025T030136Z-5297665@tiss.tuwien.ac.at
485 | END:VEVENT
486 | BEGIN:VEVENT
487 | DTSTAMP:20251011T114455Z
488 | DTSTART;TZID=Europe/Vienna:20251126T150000
489 | DTEND;TZID=Europe/Vienna:20251126T170000
490 | SUMMARY:194.207 VU Generative AI
491 | LOCATION:Informatikhörsaal - ARCH-INF
492 | CATEGORIES:COURSE
493 | DESCRIPTION:Vorlesung
494 | UID:20251025T030137Z-5340293@tiss.tuwien.ac.at
495 | END:VEVENT
496 | BEGIN:VEVENT
497 | DTSTAMP:20251011T114455Z
498 | DTSTART;TZID=Europe/Vienna:20251126T160000
499 | DTEND;TZID=Europe/Vienna:20251126T180000
500 | SUMMARY:194.190 VU Low-Level Programming
501 | LOCATION:EI 1 Petritsch HS
502 | CATEGORIES:COURSE
503 | DESCRIPTION:Vorlesungen und Übungspräsentationen
504 | UID:20251025T030138Z-5268796@tiss.tuwien.ac.at
505 | END:VEVENT
506 | BEGIN:VEVENT
507 | DTSTAMP:20251011T114455Z
508 | DTSTART;TZID=Europe/Vienna:20251127T100000
509 | DTEND;TZID=Europe/Vienna:20251127T120000
510 | SUMMARY:194.193 VU AI Programming
511 | LOCATION:FAV Hörsaal 2
512 | CATEGORIES:COURSE
513 | DESCRIPTION:Vorlesung
514 | UID:20251025T030139Z-5297578@tiss.tuwien.ac.at
515 | END:VEVENT
516 | BEGIN:VEVENT
517 | DTSTAMP:20251011T114455Z
518 | DTSTART;TZID=Europe/Vienna:20251203T100000
519 | DTEND;TZID=Europe/Vienna:20251203T120000
520 | SUMMARY:192.065 VU Cryptocurrencies
521 | LOCATION:EI 8 Pötzl HS - QUER
522 | CATEGORIES:COURSE
523 | DESCRIPTION:VU 192.065 Cryptocurrencies
524 | UID:20251025T030140Z-5297666@tiss.tuwien.ac.at
525 | END:VEVENT
526 | BEGIN:VEVENT
527 | DTSTAMP:20251011T114455Z
528 | DTSTART;TZID=Europe/Vienna:20251203T160000
529 | DTEND;TZID=Europe/Vienna:20251203T180000
530 | SUMMARY:194.190 VU Low-Level Programming
531 | LOCATION:EI 1 Petritsch HS
532 | CATEGORIES:COURSE
533 | DESCRIPTION:Vorlesungen und Übungspräsentationen
534 | UID:20251025T030141Z-5268797@tiss.tuwien.ac.at
535 | END:VEVENT
536 | BEGIN:VEVENT
537 | DTSTAMP:20251011T114455Z
538 | DTSTART;TZID=Europe/Vienna:20251204T100000
539 | DTEND;TZID=Europe/Vienna:20251204T120000
540 | SUMMARY:194.193 VU AI Programming
541 | LOCATION:FAV Hörsaal 2
542 | CATEGORIES:COURSE
543 | DESCRIPTION:Vorlesung
544 | UID:20251025T030142Z-5297579@tiss.tuwien.ac.at
545 | END:VEVENT
546 | BEGIN:VEVENT
547 | DTSTAMP:20251011T114455Z
548 | DTSTART;TZID=Europe/Vienna:20251205T120000
549 | DTEND;TZID=Europe/Vienna:20251205T150000
550 | SUMMARY:187.250 VO Zwischen Karriere und Barriere
551 | LOCATION:HS 18 Czuber - MB
552 | CATEGORIES:COURSE
553 | DESCRIPTION:4. VO
554 | UID:20251025T030143Z-5317903@tiss.tuwien.ac.at
555 | END:VEVENT
556 | BEGIN:VEVENT
557 | DTSTAMP:20251011T114455Z
558 | DTSTART;TZID=Europe/Vienna:20251210T100000
559 | DTEND;TZID=Europe/Vienna:20251210T120000
560 | SUMMARY:192.065 VU Cryptocurrencies
561 | LOCATION:EI 8 Pötzl HS - QUER
562 | CATEGORIES:COURSE
563 | DESCRIPTION:VU 192.065 Cryptocurrencies
564 | UID:20251025T030144Z-5297667@tiss.tuwien.ac.at
565 | END:VEVENT
566 | BEGIN:VEVENT
567 | DTSTAMP:20251011T114455Z
568 | DTSTART;TZID=Europe/Vienna:20251210T150000
569 | DTEND;TZID=Europe/Vienna:20251210T170000
570 | SUMMARY:194.207 VU Generative AI
571 | LOCATION:Informatikhörsaal - ARCH-INF
572 | CATEGORIES:COURSE
573 | DESCRIPTION:Vorlesung
574 | UID:20251025T030145Z-5340294@tiss.tuwien.ac.at
575 | END:VEVENT
576 | BEGIN:VEVENT
577 | DTSTAMP:20251011T114455Z
578 | DTSTART;TZID=Europe/Vienna:20251210T160000
579 | DTEND;TZID=Europe/Vienna:20251210T180000
580 | SUMMARY:194.190 VU Low-Level Programming
581 | LOCATION:EI 1 Petritsch HS
582 | CATEGORIES:COURSE
583 | DESCRIPTION:Vorlesungen und Übungspräsentationen
584 | UID:20251025T030146Z-5268798@tiss.tuwien.ac.at
585 | END:VEVENT
586 | BEGIN:VEVENT
587 | DTSTAMP:20251011T114455Z
588 | DTSTART;TZID=Europe/Vienna:20251211T100000
589 | DTEND;TZID=Europe/Vienna:20251211T120000
590 | SUMMARY:194.193 VU AI Programming
591 | LOCATION:Zoom
592 | CATEGORIES:COURSE
593 | DESCRIPTION:Assignment Discussion Session A3 & A4
594 | UID:20251025T030147Z-5340800@tiss.tuwien.ac.at
595 | END:VEVENT
596 | BEGIN:VEVENT
597 | DTSTAMP:20251011T114455Z
598 | DTSTART;TZID=Europe/Vienna:20251211T100000
599 | DTEND;TZID=Europe/Vienna:20251211T120000
600 | SUMMARY:194.193 VU AI Programming
601 | LOCATION:FAV Hörsaal 2
602 | CATEGORIES:COURSE
603 | DESCRIPTION:Vorlesung
604 | UID:20251025T030148Z-5297640@tiss.tuwien.ac.at
605 | END:VEVENT
606 | BEGIN:VEVENT
607 | DTSTAMP:20251011T114455Z
608 | DTSTART;TZID=Europe/Vienna:20251217T100000
609 | DTEND;TZID=Europe/Vienna:20251217T120000
610 | SUMMARY:192.065 VU Cryptocurrencies
611 | LOCATION:EI 8 Pötzl HS - QUER
612 | CATEGORIES:COURSE
613 | DESCRIPTION:VU 192.065 Cryptocurrencies
614 | UID:20251025T030149Z-5297668@tiss.tuwien.ac.at
615 | END:VEVENT
616 | BEGIN:VEVENT
617 | DTSTAMP:20251011T114455Z
618 | DTSTART;TZID=Europe/Vienna:20251217T150000
619 | DTEND;TZID=Europe/Vienna:20251217T170000
620 | SUMMARY:194.207 VU Generative AI
621 | LOCATION:Informatikhörsaal - ARCH-INF
622 | CATEGORIES:COURSE
623 | DESCRIPTION:Vorlesung
624 | UID:20251025T030150Z-5340295@tiss.tuwien.ac.at
625 | END:VEVENT
626 | BEGIN:VEVENT
627 | DTSTAMP:20251011T114455Z
628 | DTSTART;TZID=Europe/Vienna:20251217T160000
629 | DTEND;TZID=Europe/Vienna:20251217T180000
630 | SUMMARY:194.190 VU Low-Level Programming
631 | LOCATION:EI 1 Petritsch HS
632 | CATEGORIES:COURSE
633 | DESCRIPTION:Vorlesungen und Übungspräsentationen
634 | UID:20251025T030151Z-5268799@tiss.tuwien.ac.at
635 | END:VEVENT
636 | BEGIN:VEVENT
637 | DTSTAMP:20251011T114455Z
638 | DTSTART;TZID=Europe/Vienna:20251218T100000
639 | DTEND;TZID=Europe/Vienna:20251218T120000
640 | SUMMARY:194.193 VU AI Programming
641 | LOCATION:FAV Hörsaal 2
642 | CATEGORIES:COURSE
643 | DESCRIPTION:Vorlesung
644 | UID:20251025T030152Z-5297641@tiss.tuwien.ac.at
645 | END:VEVENT
646 | BEGIN:VEVENT
647 | DTSTAMP:20251011T114455Z
648 | DTSTART;TZID=Europe/Vienna:20260107T100000
649 | DTEND;TZID=Europe/Vienna:20260107T120000
650 | SUMMARY:192.065 VU Cryptocurrencies
651 | LOCATION:EI 8 Pötzl HS - QUER
652 | CATEGORIES:COURSE
653 | DESCRIPTION:VU 192.065 Cryptocurrencies
654 | UID:20251025T030153Z-5297669@tiss.tuwien.ac.at
655 | END:VEVENT
656 | BEGIN:VEVENT
657 | DTSTAMP:20251011T114455Z
658 | DTSTART;TZID=Europe/Vienna:20260107T150000
659 | DTEND;TZID=Europe/Vienna:20260107T170000
660 | SUMMARY:194.207 VU Generative AI
661 | LOCATION:Informatikhörsaal - ARCH-INF
662 | CATEGORIES:COURSE
663 | DESCRIPTION:Vorlesung
664 | UID:20251025T030154Z-5340296@tiss.tuwien.ac.at
665 | END:VEVENT
666 | BEGIN:VEVENT
667 | DTSTAMP:20251011T114455Z
668 | DTSTART;TZID=Europe/Vienna:20260107T160000
669 | DTEND;TZID=Europe/Vienna:20260107T180000
670 | SUMMARY:194.190 VU Low-Level Programming
671 | LOCATION:EI 1 Petritsch HS
672 | CATEGORIES:COURSE
673 | DESCRIPTION:Vorlesungen und Übungspräsentationen
674 | UID:20251025T030155Z-5268940@tiss.tuwien.ac.at
675 | END:VEVENT
676 | BEGIN:VEVENT
677 | DTSTAMP:20251011T114455Z
678 | DTSTART;TZID=Europe/Vienna:20260114T100000
679 | DTEND;TZID=Europe/Vienna:20260114T120000
680 | SUMMARY:192.065 VU Cryptocurrencies
681 | LOCATION:EI 8 Pötzl HS - QUER
682 | CATEGORIES:COURSE
683 | DESCRIPTION:VU 192.065 Cryptocurrencies
684 | UID:20251025T030156Z-5297670@tiss.tuwien.ac.at
685 | END:VEVENT
686 | BEGIN:VEVENT
687 | DTSTAMP:20251011T114455Z
688 | DTSTART;TZID=Europe/Vienna:20260114T150000
689 | DTEND;TZID=Europe/Vienna:20260114T170000
690 | SUMMARY:194.207 VU Generative AI
691 | LOCATION:Informatikhörsaal - ARCH-INF
692 | CATEGORIES:COURSE
693 | DESCRIPTION:Vorlesung
694 | UID:20251025T030157Z-5340297@tiss.tuwien.ac.at
695 | END:VEVENT
696 | BEGIN:VEVENT
697 | DTSTAMP:20251011T114455Z
698 | DTSTART;TZID=Europe/Vienna:20260114T160000
699 | DTEND;TZID=Europe/Vienna:20260114T180000
700 | SUMMARY:194.190 VU Low-Level Programming
701 | LOCATION:EI 1 Petritsch HS
702 | CATEGORIES:COURSE
703 | DESCRIPTION:Vorlesungen und Übungspräsentationen
704 | UID:20251025T030158Z-5268941@tiss.tuwien.ac.at
705 | END:VEVENT
706 | BEGIN:VEVENT
707 | DTSTAMP:20251011T114455Z
708 | DTSTART;TZID=Europe/Vienna:20260121T100000
709 | DTEND;TZID=Europe/Vienna:20260121T120000
710 | SUMMARY:192.065 VU Cryptocurrencies
711 | LOCATION:EI 8 Pötzl HS - QUER
712 | CATEGORIES:COURSE
713 | DESCRIPTION:VU 192.065 Cryptocurrencies
714 | UID:20251025T030159Z-5297671@tiss.tuwien.ac.at
715 | END:VEVENT
716 | BEGIN:VEVENT
717 | DTSTAMP:20251011T114455Z
718 | DTSTART;TZID=Europe/Vienna:20260121T160000
719 | DTEND;TZID=Europe/Vienna:20260121T180000
720 | SUMMARY:194.190 VU Low-Level Programming
721 | LOCATION:EI 1 Petritsch HS
722 | CATEGORIES:COURSE
723 | DESCRIPTION:Vorlesungen und Übungspräsentationen
724 | UID:20251025T030200Z-5268942@tiss.tuwien.ac.at
725 | END:VEVENT
726 | BEGIN:VEVENT
727 | DTSTAMP:20251011T114455Z
728 | DTSTART;TZID=Europe/Vienna:20260128T100000
729 | DTEND;TZID=Europe/Vienna:20260128T120000
730 | SUMMARY:192.065 VU Cryptocurrencies
731 | LOCATION:EI 8 Pötzl HS - QUER
732 | CATEGORIES:COURSE
733 | DESCRIPTION:VU 192.065 Cryptocurrencies
734 | UID:20251025T030201Z-5297672@tiss.tuwien.ac.at
735 | END:VEVENT
736 | BEGIN:VEVENT
737 | DTSTAMP:20251011T114455Z
738 | DTSTART;TZID=Europe/Vienna:20260128T150000
739 | DTEND;TZID=Europe/Vienna:20260128T170000
740 | SUMMARY:194.207 VU Generative AI
741 | LOCATION:Informatikhörsaal - ARCH-INF
742 | CATEGORIES:COURSE
743 | DESCRIPTION:Vorlesung
744 | UID:20251025T030202Z-5340298@tiss.tuwien.ac.at
745 | END:VEVENT
746 | BEGIN:VEVENT
747 | DTSTAMP:20251011T114455Z
748 | DTSTART;TZID=Europe/Vienna:20260128T160000
749 | DTEND;TZID=Europe/Vienna:20260128T180000
750 | SUMMARY:194.190 VU Low-Level Programming
751 | LOCATION:EI 1 Petritsch HS
752 | CATEGORIES:COURSE
753 | DESCRIPTION:Vorlesungen und Übungspräsentationen
754 | UID:20251025T030203Z-5268943@tiss.tuwien.ac.at
755 | END:VEVENT
756 | BEGIN:VEVENT
757 | DTSTAMP:20230824T192206Z
758 | DTSTART;TZID=Europe/Vienna:20230627T160000
759 | DTEND;TZID=Europe/Vienna:20230627T180000
760 | SUMMARY:185.A50 VU Dynamic Compilation Dynamische Übersetzer
761 | LOCATION:CompLang Bibliothek
762 | CATEGORIES:COURSE
763 | DESCRIPTION:Projektpräsentation
764 | UID:20240303T172802Z-4831568@tiss.tuwien.ac.at
765 | END:VEVENT
766 | BEGIN:VEVENT
767 | DTSTAMP:20230824T192206Z
768 | DTSTART;VALUE=DATE:20230529
769 | DTEND;VALUE=DATE:20230531
770 | SUMMARY:Pfingsten\, vorlesungsfrei
771 | CATEGORIES:HOLIDAY
772 | UID:20240303T172803Z-10544@tiss.tuwien.ac.at
773 | END:VEVENT
774 | BEGIN:VEVENT
775 | DTSTAMP:20230824T192206Z
776 | DTSTART;VALUE=DATE:20230608
777 | DTEND;VALUE=DATE:20230609
778 | SUMMARY:Fronleichnam\, vorlesungsfrei
779 | CATEGORIES:HOLIDAY
780 | UID:20240303T172804Z-10547@tiss.tuwien.ac.at
781 | END:VEVENT
782 | BEGIN:VEVENT
783 | DTSTAMP:20230824T192206Z
784 | DTSTART;VALUE=DATE:20230701
785 | DTEND;VALUE=DATE:20231001
786 | SUMMARY:Sommerferien\, vorlesungsfrei
787 | CATEGORIES:HOLIDAY
788 | UID:20240303T172805Z-10548@tiss.tuwien.ac.at
789 | END:VEVENT
790 | BEGIN:VEVENT
791 | DTSTAMP:20230824T192206Z
792 | DTSTART;VALUE=DATE:20231026
793 | DTEND;VALUE=DATE:20231027
794 | SUMMARY:Nationalfeiertag\, vorlesungsfrei
795 | CATEGORIES:HOLIDAY
796 | UID:20240303T172806Z-10560@tiss.tuwien.ac.at
797 | END:VEVENT
798 | BEGIN:VEVENT
799 | DTSTAMP:20230824T192206Z
800 | DTSTART;VALUE=DATE:20231101
801 | DTEND;VALUE=DATE:20231102
802 | SUMMARY:Allerheiligen\, vorlesungsfrei
803 | CATEGORIES:HOLIDAY
804 | UID:20240303T172807Z-10561@tiss.tuwien.ac.at
805 | END:VEVENT
806 | BEGIN:VEVENT
807 | DTSTAMP:20230824T192206Z
808 | DTSTART;VALUE=DATE:20231102
809 | DTEND;VALUE=DATE:20231103
810 | SUMMARY:Allerseelen\, vorlesungsfrei
811 | CATEGORIES:HOLIDAY
812 | UID:20240303T172808Z-10562@tiss.tuwien.ac.at
813 | END:VEVENT
814 | BEGIN:VEVENT
815 | DTSTAMP:20230824T192206Z
816 | DTSTART;VALUE=DATE:20231115
817 | DTEND;VALUE=DATE:20231116
818 | SUMMARY:Landespatron\, vorlesungsfrei
819 | CATEGORIES:HOLIDAY
820 | UID:20240303T172809Z-10563@tiss.tuwien.ac.at
821 | END:VEVENT
822 | BEGIN:VEVENT
823 | DTSTAMP:20230824T192206Z
824 | DTSTART;VALUE=DATE:20231208
825 | DTEND;VALUE=DATE:20231209
826 | SUMMARY:Mariä Empfängnis
827 | CATEGORIES:HOLIDAY
828 | UID:20240303T172810Z-10580@tiss.tuwien.ac.at
829 | END:VEVENT
830 | BEGIN:VEVENT
831 | DTSTAMP:20230824T192206Z
832 | DTSTART;VALUE=DATE:20231222
833 | DTEND;VALUE=DATE:20240106
834 | SUMMARY:Weihnachtsferien\, vorlesungsfrei
835 | CATEGORIES:HOLIDAY
836 | UID:20240303T172811Z-10564@tiss.tuwien.ac.at
837 | END:VEVENT
838 | BEGIN:VEVENT
839 | DTSTAMP:20230824T192206Z
840 | DTSTART;VALUE=DATE:20240126
841 | DTEND;VALUE=DATE:20240301
842 | SUMMARY:Semesterferien\, vorlesungsfrei
843 | CATEGORIES:HOLIDAY
844 | UID:20240303T172812Z-10565@tiss.tuwien.ac.at
845 | END:VEVENT
846 | BEGIN:VEVENT
847 | DTSTAMP:20230824T192206Z
848 | DTSTART;VALUE=DATE:20240325
849 | DTEND;VALUE=DATE:20240406
850 | SUMMARY:Osterferien\, vorlesungsfrei
851 | CATEGORIES:HOLIDAY
852 | UID:20240303T172813Z-10566@tiss.tuwien.ac.at
853 | END:VEVENT
854 | BEGIN:VEVENT
855 | DTSTAMP:20230824T192206Z
856 | DTSTART;VALUE=DATE:20240501
857 | DTEND;VALUE=DATE:20240502
858 | SUMMARY:Staatsfeiertag\, vorlesungsfrei
859 | CATEGORIES:HOLIDAY
860 | UID:20240303T172814Z-10567@tiss.tuwien.ac.at
861 | END:VEVENT
862 | BEGIN:VEVENT
863 | DTSTAMP:20230824T192206Z
864 | DTSTART;VALUE=DATE:20240509
865 | DTEND;VALUE=DATE:20240510
866 | SUMMARY:Christi Himmelfahrt\, vorlesungsfrei
867 | CATEGORIES:HOLIDAY
868 | UID:20240303T172815Z-10568@tiss.tuwien.ac.at
869 | END:VEVENT
870 | BEGIN:VEVENT
871 | DTSTAMP:20230824T192206Z
872 | DTSTART;VALUE=DATE:20240510
873 | DTEND;VALUE=DATE:20240511
874 | SUMMARY:Rektorstag\, vorlesungsfrei
875 | CATEGORIES:HOLIDAY
876 | UID:20240303T172816Z-10569@tiss.tuwien.ac.at
877 | END:VEVENT
878 | BEGIN:VEVENT
879 | DTSTAMP:20230824T192206Z
880 | DTSTART;VALUE=DATE:20240520
881 | DTEND;VALUE=DATE:20240522
882 | SUMMARY:Pfingstferien\, vorlesungsfrei
883 | CATEGORIES:HOLIDAY
884 | UID:20240303T172817Z-10570@tiss.tuwien.ac.at
885 | END:VEVENT
886 | BEGIN:VEVENT
887 | DTSTAMP:20230824T192206Z
888 | DTSTART;VALUE=DATE:20240530
889 | DTEND;VALUE=DATE:20240531
890 | SUMMARY:Fronleichnam\, vorlesungsfrei
891 | CATEGORIES:HOLIDAY
892 | UID:20240303T172818Z-10571@tiss.tuwien.ac.at
893 | END:VEVENT
894 | BEGIN:VEVENT
895 | DTSTAMP:20230824T192206Z
896 | DTSTART;VALUE=DATE:20240629
897 | DTEND;VALUE=DATE:20241001
898 | SUMMARY:Sommerferien\, vorlesungsfrei
899 | CATEGORIES:HOLIDAY
900 | UID:20240303T172819Z-10572@tiss.tuwien.ac.at
901 | END:VEVENT
902 | END:VCALENDAR
903 |
--------------------------------------------------------------------------------
/tests/calendar_en.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:-//Tiss Events Calendar//iCal4j 1.0//EN
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | BEGIN:VTIMEZONE
6 | TZID:Europe/Vienna
7 | LAST-MODIFIED:20201010T011803Z
8 | TZURL:http://tzurl.org/zoneinfo/Europe/Vienna
9 | X-LIC-LOCATION:Europe/Vienna
10 | X-PROLEPTIC-TZNAME:LMT
11 | BEGIN:STANDARD
12 | TZNAME:CET
13 | TZOFFSETFROM:+010521
14 | TZOFFSETTO:+0100
15 | DTSTART:18930401T000000
16 | END:STANDARD
17 | BEGIN:DAYLIGHT
18 | TZNAME:CEST
19 | TZOFFSETFROM:+0100
20 | TZOFFSETTO:+0200
21 | DTSTART:19160430T230000
22 | RDATE:19200405T020000
23 | RDATE:19400401T020000
24 | RDATE:19430329T020000
25 | RDATE:19440403T020000
26 | RDATE:19450402T020000
27 | RDATE:19460414T020000
28 | RDATE:19470406T020000
29 | RDATE:19480418T020000
30 | RDATE:19800406T000000
31 | END:DAYLIGHT
32 | BEGIN:STANDARD
33 | TZNAME:CET
34 | TZOFFSETFROM:+0200
35 | TZOFFSETTO:+0100
36 | DTSTART:19161001T010000
37 | RDATE:19200913T030000
38 | RDATE:19421102T030000
39 | RDATE:19431004T030000
40 | RDATE:19441002T030000
41 | RDATE:19450412T030000
42 | RDATE:19461007T030000
43 | RDATE:19800928T000000
44 | END:STANDARD
45 | BEGIN:DAYLIGHT
46 | TZNAME:CEST
47 | TZOFFSETFROM:+0100
48 | TZOFFSETTO:+0200
49 | DTSTART:19170416T020000
50 | RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO
51 | END:DAYLIGHT
52 | BEGIN:STANDARD
53 | TZNAME:CET
54 | TZOFFSETFROM:+0200
55 | TZOFFSETTO:+0100
56 | DTSTART:19170917T030000
57 | RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO
58 | END:STANDARD
59 | BEGIN:STANDARD
60 | TZNAME:CET
61 | TZOFFSETFROM:+0100
62 | TZOFFSETTO:+0100
63 | DTSTART:19200101T000000
64 | RDATE:19460101T000000
65 | END:STANDARD
66 | BEGIN:STANDARD
67 | TZNAME:CET
68 | TZOFFSETFROM:+0200
69 | TZOFFSETTO:+0100
70 | DTSTART:19471005T030000
71 | RRULE:FREQ=YEARLY;UNTIL=19481003T010000Z;BYMONTH=10;BYDAY=1SU
72 | END:STANDARD
73 | BEGIN:DAYLIGHT
74 | TZNAME:CEST
75 | TZOFFSETFROM:+0100
76 | TZOFFSETTO:+0200
77 | DTSTART:19810329T020000
78 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
79 | END:DAYLIGHT
80 | BEGIN:STANDARD
81 | TZNAME:CET
82 | TZOFFSETFROM:+0200
83 | TZOFFSETTO:+0100
84 | DTSTART:19810927T030000
85 | RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU
86 | END:STANDARD
87 | BEGIN:STANDARD
88 | TZNAME:CET
89 | TZOFFSETFROM:+0200
90 | TZOFFSETTO:+0100
91 | DTSTART:19961027T030000
92 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
93 | END:STANDARD
94 | END:VTIMEZONE
95 | BEGIN:VEVENT
96 | DTSTAMP:20230824T192114Z
97 | DTSTART;TZID=Europe/Vienna:20230526T110000
98 | DTEND;TZID=Europe/Vienna:20230526T130000
99 | SUMMARY:185.208 VU Programming Languages
100 | LOCATION:EI 5 Hochenegg HS
101 | CATEGORIES:COURSE
102 | DESCRIPTION:Lecture
103 | UID:20240303T171550Z-4743490@tiss.tuwien.ac.at
104 | END:VEVENT
105 | BEGIN:VEVENT
106 | DTSTAMP:20230824T192114Z
107 | DTSTART;TZID=Europe/Vienna:20230602T110000
108 | DTEND;TZID=Europe/Vienna:20230602T130000
109 | SUMMARY:185.208 VU Programming Languages
110 | LOCATION:EI 5 Hochenegg HS
111 | CATEGORIES:COURSE
112 | DESCRIPTION:Lecture
113 | UID:20240303T171551Z-4743491@tiss.tuwien.ac.at
114 | END:VEVENT
115 | BEGIN:VEVENT
116 | DTSTAMP:20230824T192114Z
117 | DTSTART;TZID=Europe/Vienna:20230605T140000
118 | DTEND;TZID=Europe/Vienna:20230605T180000
119 | SUMMARY:185.307 SE Programming languages seminar
120 | LOCATION:EI 6 Eckert HS
121 | CATEGORIES:COURSE
122 | DESCRIPTION:Seminar
123 | UID:20240303T171552Z-4804905@tiss.tuwien.ac.at
124 | END:VEVENT
125 | BEGIN:VEVENT
126 | DTSTAMP:20230824T192114Z
127 | DTSTART;TZID=Europe/Vienna:20230616T110000
128 | DTEND;TZID=Europe/Vienna:20230616T130000
129 | SUMMARY:185.208 VU Programming Languages
130 | LOCATION:EI 5 Hochenegg HS
131 | CATEGORIES:COURSE
132 | DESCRIPTION:Lecture
133 | UID:20240303T171553Z-4743492@tiss.tuwien.ac.at
134 | END:VEVENT
135 | BEGIN:VEVENT
136 | DTSTAMP:20230824T192114Z
137 | DTSTART;TZID=Europe/Vienna:20230616T143000
138 | DTEND;TZID=Europe/Vienna:20230616T163000
139 | SUMMARY:184.260 VU Distributed Systems Technologies - G6
140 | LOCATION:Zoom
141 | CATEGORIES:GROUP
142 | DESCRIPTION:Assignment 3 - Discussion
143 | UID:20240303T171554Z-4806598@tiss.tuwien.ac.at
144 | END:VEVENT
145 | BEGIN:VEVENT
146 | DTSTAMP:20230824T192114Z
147 | DTSTART;TZID=Europe/Vienna:20230627T110000
148 | DTEND;TZID=Europe/Vienna:20230627T130000
149 | SUMMARY:184.260 VU Distributed Systems Technologies (Exam) - Prüfung
150 | LOCATION:EI 7 Hörsaal - ETIT
151 | CATEGORIES:EXAM
152 | DESCRIPTION:184.260 Distributed Systems Technologies Prüfung
153 | UID:20240303T171555Z-4715071@tiss.tuwien.ac.at
154 | END:VEVENT
155 | BEGIN:VEVENT
156 | DTSTAMP:20230824T192114Z
157 | DTSTART;TZID=Europe/Vienna:20230627T160000
158 | DTEND;TZID=Europe/Vienna:20230627T180000
159 | SUMMARY:185.A50 VU Dynamic Compilation
160 | LOCATION:complang library
161 | CATEGORIES:COURSE
162 | DESCRIPTION:project presentation
163 | UID:20240303T171556Z-4831568@tiss.tuwien.ac.at
164 | END:VEVENT
165 | BEGIN:VEVENT
166 | DTSTAMP:20251011T114444Z
167 | DTSTART;TZID=Europe/Vienna:20251001T160000
168 | DTEND;TZID=Europe/Vienna:20251001T180000
169 | SUMMARY:194.190 VU Low-Level Programming
170 | LOCATION:EI 1 Petritsch HS
171 | CATEGORIES:COURSE
172 | DESCRIPTION:Lectures and presentations of project results
173 | UID:20251025T004521Z-5268788@tiss.tuwien.ac.at
174 | END:VEVENT
175 | BEGIN:VEVENT
176 | DTSTAMP:20251011T114444Z
177 | DTSTART;TZID=Europe/Vienna:20251002T110000
178 | DTEND;TZID=Europe/Vienna:20251002T120000
179 | SUMMARY:194.193 VU AI Programming
180 | LOCATION:FAV Hörsaal 1 Helmut Veith - INF
181 | CATEGORIES:COURSE
182 | DESCRIPTION:Kick-off
183 | UID:20251025T004522Z-5297642@tiss.tuwien.ac.at
184 | END:VEVENT
185 | BEGIN:VEVENT
186 | DTSTAMP:20251011T114444Z
187 | DTSTART;TZID=Europe/Vienna:20251008T150000
188 | DTEND;TZID=Europe/Vienna:20251008T160000
189 | SUMMARY:194.207 VU Generative AI
190 | LOCATION:https://tuwien.zoom.us/j/68659854514?pwd=rGPyiiUGa2FPP9KyDiQaVIc
191 | HuWtn8o.1
192 | CATEGORIES:COURSE
193 | DESCRIPTION:Preliminary Meeting
194 | UID:20251025T004523Z-5342139@tiss.tuwien.ac.at
195 | END:VEVENT
196 | BEGIN:VEVENT
197 | DTSTAMP:20251011T114444Z
198 | DTSTART;TZID=Europe/Vienna:20251008T160000
199 | DTEND;TZID=Europe/Vienna:20251008T180000
200 | SUMMARY:194.190 VU Low-Level Programming
201 | LOCATION:EI 1 Petritsch HS
202 | CATEGORIES:COURSE
203 | DESCRIPTION:Lectures and presentations of project results
204 | UID:20251025T004524Z-5268789@tiss.tuwien.ac.at
205 | END:VEVENT
206 | BEGIN:VEVENT
207 | DTSTAMP:20251011T114444Z
208 | DTSTART;TZID=Europe/Vienna:20251009T100000
209 | DTEND;TZID=Europe/Vienna:20251009T120000
210 | SUMMARY:194.193 VU AI Programming
211 | LOCATION:FAV Hörsaal 2
212 | CATEGORIES:COURSE
213 | DESCRIPTION:Lecture
214 | UID:20251025T004525Z-5297571@tiss.tuwien.ac.at
215 | END:VEVENT
216 | BEGIN:VEVENT
217 | DTSTAMP:20251011T114444Z
218 | DTSTART;TZID=Europe/Vienna:20251010T140000
219 | DTEND;TZID=Europe/Vienna:20251010T160000
220 | SUMMARY:187.250 VO Developing a career - coping with obstacles
221 | LOCATION:HS 18 Czuber - MB
222 | CATEGORIES:COURSE
223 | DESCRIPTION:Introduction and organisation
224 | UID:20251025T004526Z-5317900@tiss.tuwien.ac.at
225 | END:VEVENT
226 | BEGIN:VEVENT
227 | DTSTAMP:20251011T114444Z
228 | DTSTART;TZID=Europe/Vienna:20251015T100000
229 | DTEND;TZID=Europe/Vienna:20251015T120000
230 | SUMMARY:192.065 VU Cryptocurrencies
231 | LOCATION:EI 8 Pötzl HS - QUER
232 | CATEGORIES:COURSE
233 | DESCRIPTION:VU 192.065 Cryptocurrencies (first lecture)
234 | UID:20251025T004527Z-5297619@tiss.tuwien.ac.at
235 | END:VEVENT
236 | BEGIN:VEVENT
237 | DTSTAMP:20251011T114444Z
238 | DTSTART;TZID=Europe/Vienna:20251015T150000
239 | DTEND;TZID=Europe/Vienna:20251015T170000
240 | SUMMARY:194.207 VU Generative AI
241 | LOCATION:Informatikhörsaal - ARCH-INF
242 | CATEGORIES:COURSE
243 | DESCRIPTION:Lecture
244 | UID:20251025T004528Z-5340287@tiss.tuwien.ac.at
245 | END:VEVENT
246 | BEGIN:VEVENT
247 | DTSTAMP:20251011T114444Z
248 | DTSTART;TZID=Europe/Vienna:20251015T160000
249 | DTEND;TZID=Europe/Vienna:20251015T180000
250 | SUMMARY:194.190 VU Low-Level Programming
251 | LOCATION:EI 1 Petritsch HS
252 | CATEGORIES:COURSE
253 | DESCRIPTION:Lectures and presentations of project results
254 | UID:20251025T004529Z-5268790@tiss.tuwien.ac.at
255 | END:VEVENT
256 | BEGIN:VEVENT
257 | DTSTAMP:20251011T114444Z
258 | DTSTART;TZID=Europe/Vienna:20251016T100000
259 | DTEND;TZID=Europe/Vienna:20251016T120000
260 | SUMMARY:194.193 VU AI Programming
261 | LOCATION:FAV Hörsaal 2
262 | CATEGORIES:COURSE
263 | DESCRIPTION:Lecture
264 | UID:20251025T004530Z-5297572@tiss.tuwien.ac.at
265 | END:VEVENT
266 | BEGIN:VEVENT
267 | DTSTAMP:20251011T114444Z
268 | DTSTART;TZID=Europe/Vienna:20251022T100000
269 | DTEND;TZID=Europe/Vienna:20251022T120000
270 | SUMMARY:192.065 VU Cryptocurrencies
271 | LOCATION:EI 8 Pötzl HS - QUER
272 | CATEGORIES:COURSE
273 | DESCRIPTION:VU 192.065 Cryptocurrencies
274 | UID:20251025T004531Z-5297660@tiss.tuwien.ac.at
275 | END:VEVENT
276 | BEGIN:VEVENT
277 | DTSTAMP:20251011T114444Z
278 | DTSTART;TZID=Europe/Vienna:20251022T150000
279 | DTEND;TZID=Europe/Vienna:20251022T170000
280 | SUMMARY:194.207 VU Generative AI
281 | LOCATION:Informatikhörsaal - ARCH-INF
282 | CATEGORIES:COURSE
283 | DESCRIPTION:Lecture
284 | UID:20251025T004532Z-5340288@tiss.tuwien.ac.at
285 | END:VEVENT
286 | BEGIN:VEVENT
287 | DTSTAMP:20251011T114444Z
288 | DTSTART;TZID=Europe/Vienna:20251022T160000
289 | DTEND;TZID=Europe/Vienna:20251022T180000
290 | SUMMARY:194.190 VU Low-Level Programming
291 | LOCATION:EI 1 Petritsch HS
292 | CATEGORIES:COURSE
293 | DESCRIPTION:Lectures and presentations of project results
294 | UID:20251025T004533Z-5268791@tiss.tuwien.ac.at
295 | END:VEVENT
296 | BEGIN:VEVENT
297 | DTSTAMP:20251011T114444Z
298 | DTSTART;TZID=Europe/Vienna:20251023T100000
299 | DTEND;TZID=Europe/Vienna:20251023T120000
300 | SUMMARY:194.193 VU AI Programming
301 | LOCATION:FAV Hörsaal 2
302 | CATEGORIES:COURSE
303 | DESCRIPTION:Lecture
304 | UID:20251025T004534Z-5297573@tiss.tuwien.ac.at
305 | END:VEVENT
306 | BEGIN:VEVENT
307 | DTSTAMP:20251011T114444Z
308 | DTSTART;TZID=Europe/Vienna:20251029T100000
309 | DTEND;TZID=Europe/Vienna:20251029T120000
310 | SUMMARY:192.065 VU Cryptocurrencies
311 | LOCATION:EI 8 Pötzl HS - QUER
312 | CATEGORIES:COURSE
313 | DESCRIPTION:VU 192.065 Cryptocurrencies
314 | UID:20251025T004535Z-5297661@tiss.tuwien.ac.at
315 | END:VEVENT
316 | BEGIN:VEVENT
317 | DTSTAMP:20251011T114444Z
318 | DTSTART;TZID=Europe/Vienna:20251029T150000
319 | DTEND;TZID=Europe/Vienna:20251029T170000
320 | SUMMARY:194.207 VU Generative AI
321 | LOCATION:Informatikhörsaal - ARCH-INF
322 | CATEGORIES:COURSE
323 | DESCRIPTION:Lecture
324 | UID:20251025T004536Z-5340289@tiss.tuwien.ac.at
325 | END:VEVENT
326 | BEGIN:VEVENT
327 | DTSTAMP:20251011T114444Z
328 | DTSTART;TZID=Europe/Vienna:20251029T160000
329 | DTEND;TZID=Europe/Vienna:20251029T180000
330 | SUMMARY:194.190 VU Low-Level Programming
331 | LOCATION:EI 1 Petritsch HS
332 | CATEGORIES:COURSE
333 | DESCRIPTION:Lectures and presentations of project results
334 | UID:20251025T004537Z-5268792@tiss.tuwien.ac.at
335 | END:VEVENT
336 | BEGIN:VEVENT
337 | DTSTAMP:20251011T114444Z
338 | DTSTART;TZID=Europe/Vienna:20251030T100000
339 | DTEND;TZID=Europe/Vienna:20251030T120000
340 | SUMMARY:194.193 VU AI Programming
341 | LOCATION:FAV Hörsaal 2
342 | CATEGORIES:COURSE
343 | DESCRIPTION:Lecture
344 | UID:20251025T004538Z-5297574@tiss.tuwien.ac.at
345 | END:VEVENT
346 | BEGIN:VEVENT
347 | DTSTAMP:20251011T114444Z
348 | DTSTART;TZID=Europe/Vienna:20251105T100000
349 | DTEND;TZID=Europe/Vienna:20251105T120000
350 | SUMMARY:192.065 VU Cryptocurrencies
351 | LOCATION:EI 8 Pötzl HS - QUER
352 | CATEGORIES:COURSE
353 | DESCRIPTION:VU 192.065 Cryptocurrencies
354 | UID:20251025T004539Z-5297662@tiss.tuwien.ac.at
355 | END:VEVENT
356 | BEGIN:VEVENT
357 | DTSTAMP:20251011T114444Z
358 | DTSTART;TZID=Europe/Vienna:20251105T150000
359 | DTEND;TZID=Europe/Vienna:20251105T170000
360 | SUMMARY:194.207 VU Generative AI
361 | LOCATION:Informatikhörsaal - ARCH-INF
362 | CATEGORIES:COURSE
363 | DESCRIPTION:Lecture
364 | UID:20251025T004540Z-5340290@tiss.tuwien.ac.at
365 | END:VEVENT
366 | BEGIN:VEVENT
367 | DTSTAMP:20251011T114444Z
368 | DTSTART;TZID=Europe/Vienna:20251105T160000
369 | DTEND;TZID=Europe/Vienna:20251105T180000
370 | SUMMARY:194.190 VU Low-Level Programming
371 | LOCATION:EI 1 Petritsch HS
372 | CATEGORIES:COURSE
373 | DESCRIPTION:Lectures and presentations of project results
374 | UID:20251025T004541Z-5268793@tiss.tuwien.ac.at
375 | END:VEVENT
376 | BEGIN:VEVENT
377 | DTSTAMP:20251011T114444Z
378 | DTSTART;TZID=Europe/Vienna:20251106T100000
379 | DTEND;TZID=Europe/Vienna:20251106T120000
380 | SUMMARY:194.193 VU AI Programming
381 | LOCATION:FAV Hörsaal 2
382 | CATEGORIES:COURSE
383 | DESCRIPTION:Lecture
384 | UID:20251025T004542Z-5297575@tiss.tuwien.ac.at
385 | END:VEVENT
386 | BEGIN:VEVENT
387 | DTSTAMP:20251011T114444Z
388 | DTSTART;TZID=Europe/Vienna:20251107T120000
389 | DTEND;TZID=Europe/Vienna:20251107T150000
390 | SUMMARY:187.250 VO Developing a career - coping with obstacles
391 | LOCATION:HS 18 Czuber - MB
392 | CATEGORIES:COURSE
393 | DESCRIPTION:2nd lecture
394 | UID:20251025T004543Z-5317901@tiss.tuwien.ac.at
395 | END:VEVENT
396 | BEGIN:VEVENT
397 | DTSTAMP:20251011T114444Z
398 | DTSTART;TZID=Europe/Vienna:20251112T100000
399 | DTEND;TZID=Europe/Vienna:20251112T120000
400 | SUMMARY:192.065 VU Cryptocurrencies
401 | LOCATION:EI 8 Pötzl HS - QUER
402 | CATEGORIES:COURSE
403 | DESCRIPTION:VU 192.065 Cryptocurrencies
404 | UID:20251025T004544Z-5297663@tiss.tuwien.ac.at
405 | END:VEVENT
406 | BEGIN:VEVENT
407 | DTSTAMP:20251011T114444Z
408 | DTSTART;TZID=Europe/Vienna:20251112T150000
409 | DTEND;TZID=Europe/Vienna:20251112T170000
410 | SUMMARY:194.207 VU Generative AI
411 | LOCATION:Informatikhörsaal - ARCH-INF
412 | CATEGORIES:COURSE
413 | DESCRIPTION:Lecture
414 | UID:20251025T004545Z-5340291@tiss.tuwien.ac.at
415 | END:VEVENT
416 | BEGIN:VEVENT
417 | DTSTAMP:20251011T114444Z
418 | DTSTART;TZID=Europe/Vienna:20251112T160000
419 | DTEND;TZID=Europe/Vienna:20251112T180000
420 | SUMMARY:194.190 VU Low-Level Programming
421 | LOCATION:EI 1 Petritsch HS
422 | CATEGORIES:COURSE
423 | DESCRIPTION:Lectures and presentations of project results
424 | UID:20251025T004546Z-5268794@tiss.tuwien.ac.at
425 | END:VEVENT
426 | BEGIN:VEVENT
427 | DTSTAMP:20251011T114444Z
428 | DTSTART;TZID=Europe/Vienna:20251113T100000
429 | DTEND;TZID=Europe/Vienna:20251113T120000
430 | SUMMARY:194.193 VU AI Programming
431 | LOCATION:FAV Hörsaal 2
432 | CATEGORIES:COURSE
433 | DESCRIPTION:Lecture
434 | UID:20251025T004547Z-5297576@tiss.tuwien.ac.at
435 | END:VEVENT
436 | BEGIN:VEVENT
437 | DTSTAMP:20251011T114444Z
438 | DTSTART;TZID=Europe/Vienna:20251119T100000
439 | DTEND;TZID=Europe/Vienna:20251119T120000
440 | SUMMARY:192.065 VU Cryptocurrencies
441 | LOCATION:EI 8 Pötzl HS - QUER
442 | CATEGORIES:COURSE
443 | DESCRIPTION:VU 192.065 Cryptocurrencies
444 | UID:20251025T004548Z-5297664@tiss.tuwien.ac.at
445 | END:VEVENT
446 | BEGIN:VEVENT
447 | DTSTAMP:20251011T114444Z
448 | DTSTART;TZID=Europe/Vienna:20251119T150000
449 | DTEND;TZID=Europe/Vienna:20251119T170000
450 | SUMMARY:194.207 VU Generative AI
451 | LOCATION:Informatikhörsaal - ARCH-INF
452 | CATEGORIES:COURSE
453 | DESCRIPTION:Lecture
454 | UID:20251025T004549Z-5340292@tiss.tuwien.ac.at
455 | END:VEVENT
456 | BEGIN:VEVENT
457 | DTSTAMP:20251011T114444Z
458 | DTSTART;TZID=Europe/Vienna:20251119T160000
459 | DTEND;TZID=Europe/Vienna:20251119T180000
460 | SUMMARY:194.190 VU Low-Level Programming
461 | LOCATION:EI 1 Petritsch HS
462 | CATEGORIES:COURSE
463 | DESCRIPTION:Lectures and presentations of project results
464 | UID:20251025T004550Z-5268795@tiss.tuwien.ac.at
465 | END:VEVENT
466 | BEGIN:VEVENT
467 | DTSTAMP:20251011T114444Z
468 | DTSTART;TZID=Europe/Vienna:20251120T100000
469 | DTEND;TZID=Europe/Vienna:20251120T120000
470 | SUMMARY:194.193 VU AI Programming
471 | LOCATION:Zoom
472 | CATEGORIES:COURSE
473 | DESCRIPTION:Assignment Discussion Session A1 & A2
474 | UID:20251025T004551Z-5340499@tiss.tuwien.ac.at
475 | END:VEVENT
476 | BEGIN:VEVENT
477 | DTSTAMP:20251011T114444Z
478 | DTSTART;TZID=Europe/Vienna:20251121T120000
479 | DTEND;TZID=Europe/Vienna:20251121T150000
480 | SUMMARY:187.250 VO Developing a career - coping with obstacles
481 | LOCATION:HS 18 Czuber - MB
482 | CATEGORIES:COURSE
483 | DESCRIPTION:3rd lecture
484 | UID:20251025T004552Z-5317902@tiss.tuwien.ac.at
485 | END:VEVENT
486 | BEGIN:VEVENT
487 | DTSTAMP:20251011T114444Z
488 | DTSTART;TZID=Europe/Vienna:20251126T100000
489 | DTEND;TZID=Europe/Vienna:20251126T120000
490 | SUMMARY:192.065 VU Cryptocurrencies
491 | LOCATION:EI 8 Pötzl HS - QUER
492 | CATEGORIES:COURSE
493 | DESCRIPTION:VU 192.065 Cryptocurrencies
494 | UID:20251025T004553Z-5297665@tiss.tuwien.ac.at
495 | END:VEVENT
496 | BEGIN:VEVENT
497 | DTSTAMP:20251011T114444Z
498 | DTSTART;TZID=Europe/Vienna:20251126T150000
499 | DTEND;TZID=Europe/Vienna:20251126T170000
500 | SUMMARY:194.207 VU Generative AI
501 | LOCATION:Informatikhörsaal - ARCH-INF
502 | CATEGORIES:COURSE
503 | DESCRIPTION:Lecture
504 | UID:20251025T004554Z-5340293@tiss.tuwien.ac.at
505 | END:VEVENT
506 | BEGIN:VEVENT
507 | DTSTAMP:20251011T114444Z
508 | DTSTART;TZID=Europe/Vienna:20251126T160000
509 | DTEND;TZID=Europe/Vienna:20251126T180000
510 | SUMMARY:194.190 VU Low-Level Programming
511 | LOCATION:EI 1 Petritsch HS
512 | CATEGORIES:COURSE
513 | DESCRIPTION:Lectures and presentations of project results
514 | UID:20251025T004555Z-5268796@tiss.tuwien.ac.at
515 | END:VEVENT
516 | BEGIN:VEVENT
517 | DTSTAMP:20251011T114444Z
518 | DTSTART;TZID=Europe/Vienna:20251127T100000
519 | DTEND;TZID=Europe/Vienna:20251127T120000
520 | SUMMARY:194.193 VU AI Programming
521 | LOCATION:FAV Hörsaal 2
522 | CATEGORIES:COURSE
523 | DESCRIPTION:Lecture
524 | UID:20251025T004556Z-5297578@tiss.tuwien.ac.at
525 | END:VEVENT
526 | BEGIN:VEVENT
527 | DTSTAMP:20251011T114444Z
528 | DTSTART;TZID=Europe/Vienna:20251203T100000
529 | DTEND;TZID=Europe/Vienna:20251203T120000
530 | SUMMARY:192.065 VU Cryptocurrencies
531 | LOCATION:EI 8 Pötzl HS - QUER
532 | CATEGORIES:COURSE
533 | DESCRIPTION:VU 192.065 Cryptocurrencies
534 | UID:20251025T004557Z-5297666@tiss.tuwien.ac.at
535 | END:VEVENT
536 | BEGIN:VEVENT
537 | DTSTAMP:20251011T114444Z
538 | DTSTART;TZID=Europe/Vienna:20251203T160000
539 | DTEND;TZID=Europe/Vienna:20251203T180000
540 | SUMMARY:194.190 VU Low-Level Programming
541 | LOCATION:EI 1 Petritsch HS
542 | CATEGORIES:COURSE
543 | DESCRIPTION:Lectures and presentations of project results
544 | UID:20251025T004558Z-5268797@tiss.tuwien.ac.at
545 | END:VEVENT
546 | BEGIN:VEVENT
547 | DTSTAMP:20251011T114444Z
548 | DTSTART;TZID=Europe/Vienna:20251204T100000
549 | DTEND;TZID=Europe/Vienna:20251204T120000
550 | SUMMARY:194.193 VU AI Programming
551 | LOCATION:FAV Hörsaal 2
552 | CATEGORIES:COURSE
553 | DESCRIPTION:Lecture
554 | UID:20251025T004559Z-5297579@tiss.tuwien.ac.at
555 | END:VEVENT
556 | BEGIN:VEVENT
557 | DTSTAMP:20251011T114444Z
558 | DTSTART;TZID=Europe/Vienna:20251205T120000
559 | DTEND;TZID=Europe/Vienna:20251205T150000
560 | SUMMARY:187.250 VO Developing a career - coping with obstacles
561 | LOCATION:HS 18 Czuber - MB
562 | CATEGORIES:COURSE
563 | DESCRIPTION:4th lecture
564 | UID:20251025T004600Z-5317903@tiss.tuwien.ac.at
565 | END:VEVENT
566 | BEGIN:VEVENT
567 | DTSTAMP:20251011T114444Z
568 | DTSTART;TZID=Europe/Vienna:20251210T100000
569 | DTEND;TZID=Europe/Vienna:20251210T120000
570 | SUMMARY:192.065 VU Cryptocurrencies
571 | LOCATION:EI 8 Pötzl HS - QUER
572 | CATEGORIES:COURSE
573 | DESCRIPTION:VU 192.065 Cryptocurrencies
574 | UID:20251025T004601Z-5297667@tiss.tuwien.ac.at
575 | END:VEVENT
576 | BEGIN:VEVENT
577 | DTSTAMP:20251011T114444Z
578 | DTSTART;TZID=Europe/Vienna:20251210T150000
579 | DTEND;TZID=Europe/Vienna:20251210T170000
580 | SUMMARY:194.207 VU Generative AI
581 | LOCATION:Informatikhörsaal - ARCH-INF
582 | CATEGORIES:COURSE
583 | DESCRIPTION:Lecture
584 | UID:20251025T004602Z-5340294@tiss.tuwien.ac.at
585 | END:VEVENT
586 | BEGIN:VEVENT
587 | DTSTAMP:20251011T114444Z
588 | DTSTART;TZID=Europe/Vienna:20251210T160000
589 | DTEND;TZID=Europe/Vienna:20251210T180000
590 | SUMMARY:194.190 VU Low-Level Programming
591 | LOCATION:EI 1 Petritsch HS
592 | CATEGORIES:COURSE
593 | DESCRIPTION:Lectures and presentations of project results
594 | UID:20251025T004603Z-5268798@tiss.tuwien.ac.at
595 | END:VEVENT
596 | BEGIN:VEVENT
597 | DTSTAMP:20251011T114444Z
598 | DTSTART;TZID=Europe/Vienna:20251211T100000
599 | DTEND;TZID=Europe/Vienna:20251211T120000
600 | SUMMARY:194.193 VU AI Programming
601 | LOCATION:Zoom
602 | CATEGORIES:COURSE
603 | DESCRIPTION:Assignment Discussion Session A3 & A4
604 | UID:20251025T004604Z-5340800@tiss.tuwien.ac.at
605 | END:VEVENT
606 | BEGIN:VEVENT
607 | DTSTAMP:20251011T114444Z
608 | DTSTART;TZID=Europe/Vienna:20251211T100000
609 | DTEND;TZID=Europe/Vienna:20251211T120000
610 | SUMMARY:194.193 VU AI Programming
611 | LOCATION:FAV Hörsaal 2
612 | CATEGORIES:COURSE
613 | DESCRIPTION:Lecture
614 | UID:20251025T004605Z-5297640@tiss.tuwien.ac.at
615 | END:VEVENT
616 | BEGIN:VEVENT
617 | DTSTAMP:20251011T114444Z
618 | DTSTART;TZID=Europe/Vienna:20251217T100000
619 | DTEND;TZID=Europe/Vienna:20251217T120000
620 | SUMMARY:192.065 VU Cryptocurrencies
621 | LOCATION:EI 8 Pötzl HS - QUER
622 | CATEGORIES:COURSE
623 | DESCRIPTION:VU 192.065 Cryptocurrencies
624 | UID:20251025T004606Z-5297668@tiss.tuwien.ac.at
625 | END:VEVENT
626 | BEGIN:VEVENT
627 | DTSTAMP:20251011T114444Z
628 | DTSTART;TZID=Europe/Vienna:20251217T150000
629 | DTEND;TZID=Europe/Vienna:20251217T170000
630 | SUMMARY:194.207 VU Generative AI
631 | LOCATION:Informatikhörsaal - ARCH-INF
632 | CATEGORIES:COURSE
633 | DESCRIPTION:Lecture
634 | UID:20251025T004607Z-5340295@tiss.tuwien.ac.at
635 | END:VEVENT
636 | BEGIN:VEVENT
637 | DTSTAMP:20251011T114444Z
638 | DTSTART;TZID=Europe/Vienna:20251217T160000
639 | DTEND;TZID=Europe/Vienna:20251217T180000
640 | SUMMARY:194.190 VU Low-Level Programming
641 | LOCATION:EI 1 Petritsch HS
642 | CATEGORIES:COURSE
643 | DESCRIPTION:Lectures and presentations of project results
644 | UID:20251025T004608Z-5268799@tiss.tuwien.ac.at
645 | END:VEVENT
646 | BEGIN:VEVENT
647 | DTSTAMP:20251011T114444Z
648 | DTSTART;TZID=Europe/Vienna:20251218T100000
649 | DTEND;TZID=Europe/Vienna:20251218T120000
650 | SUMMARY:194.193 VU AI Programming
651 | LOCATION:FAV Hörsaal 2
652 | CATEGORIES:COURSE
653 | DESCRIPTION:Lecture
654 | UID:20251025T004609Z-5297641@tiss.tuwien.ac.at
655 | END:VEVENT
656 | BEGIN:VEVENT
657 | DTSTAMP:20251011T114444Z
658 | DTSTART;TZID=Europe/Vienna:20260107T100000
659 | DTEND;TZID=Europe/Vienna:20260107T120000
660 | SUMMARY:192.065 VU Cryptocurrencies
661 | LOCATION:EI 8 Pötzl HS - QUER
662 | CATEGORIES:COURSE
663 | DESCRIPTION:VU 192.065 Cryptocurrencies
664 | UID:20251025T004610Z-5297669@tiss.tuwien.ac.at
665 | END:VEVENT
666 | BEGIN:VEVENT
667 | DTSTAMP:20251011T114444Z
668 | DTSTART;TZID=Europe/Vienna:20260107T150000
669 | DTEND;TZID=Europe/Vienna:20260107T170000
670 | SUMMARY:194.207 VU Generative AI
671 | LOCATION:Informatikhörsaal - ARCH-INF
672 | CATEGORIES:COURSE
673 | DESCRIPTION:Lecture
674 | UID:20251025T004611Z-5340296@tiss.tuwien.ac.at
675 | END:VEVENT
676 | BEGIN:VEVENT
677 | DTSTAMP:20251011T114444Z
678 | DTSTART;TZID=Europe/Vienna:20260107T160000
679 | DTEND;TZID=Europe/Vienna:20260107T180000
680 | SUMMARY:194.190 VU Low-Level Programming
681 | LOCATION:EI 1 Petritsch HS
682 | CATEGORIES:COURSE
683 | DESCRIPTION:Lectures and presentations of project results
684 | UID:20251025T004612Z-5268940@tiss.tuwien.ac.at
685 | END:VEVENT
686 | BEGIN:VEVENT
687 | DTSTAMP:20251011T114444Z
688 | DTSTART;TZID=Europe/Vienna:20260114T100000
689 | DTEND;TZID=Europe/Vienna:20260114T120000
690 | SUMMARY:192.065 VU Cryptocurrencies
691 | LOCATION:EI 8 Pötzl HS - QUER
692 | CATEGORIES:COURSE
693 | DESCRIPTION:VU 192.065 Cryptocurrencies
694 | UID:20251025T004613Z-5297670@tiss.tuwien.ac.at
695 | END:VEVENT
696 | BEGIN:VEVENT
697 | DTSTAMP:20251011T114444Z
698 | DTSTART;TZID=Europe/Vienna:20260114T150000
699 | DTEND;TZID=Europe/Vienna:20260114T170000
700 | SUMMARY:194.207 VU Generative AI
701 | LOCATION:Informatikhörsaal - ARCH-INF
702 | CATEGORIES:COURSE
703 | DESCRIPTION:Lecture
704 | UID:20251025T004614Z-5340297@tiss.tuwien.ac.at
705 | END:VEVENT
706 | BEGIN:VEVENT
707 | DTSTAMP:20251011T114444Z
708 | DTSTART;TZID=Europe/Vienna:20260114T160000
709 | DTEND;TZID=Europe/Vienna:20260114T180000
710 | SUMMARY:194.190 VU Low-Level Programming
711 | LOCATION:EI 1 Petritsch HS
712 | CATEGORIES:COURSE
713 | DESCRIPTION:Lectures and presentations of project results
714 | UID:20251025T004615Z-5268941@tiss.tuwien.ac.at
715 | END:VEVENT
716 | BEGIN:VEVENT
717 | DTSTAMP:20251011T114444Z
718 | DTSTART;TZID=Europe/Vienna:20260121T100000
719 | DTEND;TZID=Europe/Vienna:20260121T120000
720 | SUMMARY:192.065 VU Cryptocurrencies
721 | LOCATION:EI 8 Pötzl HS - QUER
722 | CATEGORIES:COURSE
723 | DESCRIPTION:VU 192.065 Cryptocurrencies
724 | UID:20251025T004616Z-5297671@tiss.tuwien.ac.at
725 | END:VEVENT
726 | BEGIN:VEVENT
727 | DTSTAMP:20251011T114444Z
728 | DTSTART;TZID=Europe/Vienna:20260121T160000
729 | DTEND;TZID=Europe/Vienna:20260121T180000
730 | SUMMARY:194.190 VU Low-Level Programming
731 | LOCATION:EI 1 Petritsch HS
732 | CATEGORIES:COURSE
733 | DESCRIPTION:Lectures and presentations of project results
734 | UID:20251025T004617Z-5268942@tiss.tuwien.ac.at
735 | END:VEVENT
736 | BEGIN:VEVENT
737 | DTSTAMP:20251011T114444Z
738 | DTSTART;TZID=Europe/Vienna:20260128T100000
739 | DTEND;TZID=Europe/Vienna:20260128T120000
740 | SUMMARY:192.065 VU Cryptocurrencies
741 | LOCATION:EI 8 Pötzl HS - QUER
742 | CATEGORIES:COURSE
743 | DESCRIPTION:VU 192.065 Cryptocurrencies
744 | UID:20251025T004618Z-5297672@tiss.tuwien.ac.at
745 | END:VEVENT
746 | BEGIN:VEVENT
747 | DTSTAMP:20251011T114444Z
748 | DTSTART;TZID=Europe/Vienna:20260128T150000
749 | DTEND;TZID=Europe/Vienna:20260128T170000
750 | SUMMARY:194.207 VU Generative AI
751 | LOCATION:Informatikhörsaal - ARCH-INF
752 | CATEGORIES:COURSE
753 | DESCRIPTION:Lecture
754 | UID:20251025T004619Z-5340298@tiss.tuwien.ac.at
755 | END:VEVENT
756 | BEGIN:VEVENT
757 | DTSTAMP:20251011T114444Z
758 | DTSTART;TZID=Europe/Vienna:20260128T160000
759 | DTEND;TZID=Europe/Vienna:20260128T180000
760 | SUMMARY:194.190 VU Low-Level Programming
761 | LOCATION:EI 1 Petritsch HS
762 | CATEGORIES:COURSE
763 | DESCRIPTION:Lectures and presentations of project results
764 | UID:20251025T004620Z-5268943@tiss.tuwien.ac.at
765 | END:VEVENT
766 | BEGIN:VEVENT
767 | DTSTAMP:20230824T192114Z
768 | DTSTART;VALUE=DATE:20230529
769 | DTEND;VALUE=DATE:20230531
770 | SUMMARY:Pentecost Holidays\, no lectures
771 | CATEGORIES:HOLIDAY
772 | UID:20240303T171557Z-10544@tiss.tuwien.ac.at
773 | END:VEVENT
774 | BEGIN:VEVENT
775 | DTSTAMP:20230824T192114Z
776 | DTSTART;VALUE=DATE:20230608
777 | DTEND;VALUE=DATE:20230609
778 | SUMMARY:Corpus Christi\, no lectures
779 | CATEGORIES:HOLIDAY
780 | UID:20240303T171558Z-10547@tiss.tuwien.ac.at
781 | END:VEVENT
782 | BEGIN:VEVENT
783 | DTSTAMP:20230824T192114Z
784 | DTSTART;VALUE=DATE:20230701
785 | DTEND;VALUE=DATE:20231001
786 | SUMMARY:Summer Holidays\, no lectures
787 | CATEGORIES:HOLIDAY
788 | UID:20240303T171559Z-10548@tiss.tuwien.ac.at
789 | END:VEVENT
790 | BEGIN:VEVENT
791 | DTSTAMP:20230824T192114Z
792 | DTSTART;VALUE=DATE:20231026
793 | DTEND;VALUE=DATE:20231027
794 | SUMMARY:National holiday\, no lectures
795 | CATEGORIES:HOLIDAY
796 | UID:20240303T171600Z-10560@tiss.tuwien.ac.at
797 | END:VEVENT
798 | BEGIN:VEVENT
799 | DTSTAMP:20230824T192114Z
800 | DTSTART;VALUE=DATE:20231101
801 | DTEND;VALUE=DATE:20231102
802 | SUMMARY:All Saints' Day\, no lectures
803 | CATEGORIES:HOLIDAY
804 | UID:20240303T171601Z-10561@tiss.tuwien.ac.at
805 | END:VEVENT
806 | BEGIN:VEVENT
807 | DTSTAMP:20230824T192114Z
808 | DTSTART;VALUE=DATE:20231102
809 | DTEND;VALUE=DATE:20231103
810 | SUMMARY:All Souls' Day\, no lectures
811 | CATEGORIES:HOLIDAY
812 | UID:20240303T171602Z-10562@tiss.tuwien.ac.at
813 | END:VEVENT
814 | BEGIN:VEVENT
815 | DTSTAMP:20230824T192114Z
816 | DTSTART;VALUE=DATE:20231115
817 | DTEND;VALUE=DATE:20231116
818 | SUMMARY:Country Patron Day\, no lectures
819 | CATEGORIES:HOLIDAY
820 | UID:20240303T171603Z-10563@tiss.tuwien.ac.at
821 | END:VEVENT
822 | BEGIN:VEVENT
823 | DTSTAMP:20230824T192114Z
824 | DTSTART;VALUE=DATE:20231208
825 | DTEND;VALUE=DATE:20231209
826 | SUMMARY:Mariä Empfängnis
827 | CATEGORIES:HOLIDAY
828 | UID:20240303T171604Z-10580@tiss.tuwien.ac.at
829 | END:VEVENT
830 | BEGIN:VEVENT
831 | DTSTAMP:20230824T192114Z
832 | DTSTART;VALUE=DATE:20231222
833 | DTEND;VALUE=DATE:20240106
834 | SUMMARY:Christmas holidays\, no lectures
835 | CATEGORIES:HOLIDAY
836 | UID:20240303T171605Z-10564@tiss.tuwien.ac.at
837 | END:VEVENT
838 | BEGIN:VEVENT
839 | DTSTAMP:20230824T192114Z
840 | DTSTART;VALUE=DATE:20240126
841 | DTEND;VALUE=DATE:20240301
842 | SUMMARY:Semester break\, no lectures
843 | CATEGORIES:HOLIDAY
844 | UID:20240303T171606Z-10565@tiss.tuwien.ac.at
845 | END:VEVENT
846 | BEGIN:VEVENT
847 | DTSTAMP:20230824T192114Z
848 | DTSTART;VALUE=DATE:20240325
849 | DTEND;VALUE=DATE:20240406
850 | SUMMARY:Easter holidays\, no lectures
851 | CATEGORIES:HOLIDAY
852 | UID:20240303T171607Z-10566@tiss.tuwien.ac.at
853 | END:VEVENT
854 | BEGIN:VEVENT
855 | DTSTAMP:20230824T192114Z
856 | DTSTART;VALUE=DATE:20240501
857 | DTEND;VALUE=DATE:20240502
858 | SUMMARY:Labor day\, no lectures
859 | CATEGORIES:HOLIDAY
860 | UID:20240303T171608Z-10567@tiss.tuwien.ac.at
861 | END:VEVENT
862 | BEGIN:VEVENT
863 | DTSTAMP:20230824T192114Z
864 | DTSTART;VALUE=DATE:20240509
865 | DTEND;VALUE=DATE:20240510
866 | SUMMARY:Ascension Day\, no lectures
867 | CATEGORIES:HOLIDAY
868 | UID:20240303T171609Z-10568@tiss.tuwien.ac.at
869 | END:VEVENT
870 | BEGIN:VEVENT
871 | DTSTAMP:20230824T192114Z
872 | DTSTART;VALUE=DATE:20240510
873 | DTEND;VALUE=DATE:20240511
874 | SUMMARY:Rectors Day\, no lectures
875 | CATEGORIES:HOLIDAY
876 | UID:20240303T171610Z-10569@tiss.tuwien.ac.at
877 | END:VEVENT
878 | BEGIN:VEVENT
879 | DTSTAMP:20230824T192114Z
880 | DTSTART;VALUE=DATE:20240520
881 | DTEND;VALUE=DATE:20240522
882 | SUMMARY:Pentecost holidays\, no lectures
883 | CATEGORIES:HOLIDAY
884 | UID:20240303T171611Z-10570@tiss.tuwien.ac.at
885 | END:VEVENT
886 | BEGIN:VEVENT
887 | DTSTAMP:20230824T192114Z
888 | DTSTART;VALUE=DATE:20240530
889 | DTEND;VALUE=DATE:20240531
890 | SUMMARY:Corpus Christi\, no lectures
891 | CATEGORIES:HOLIDAY
892 | UID:20240303T171612Z-10571@tiss.tuwien.ac.at
893 | END:VEVENT
894 | BEGIN:VEVENT
895 | DTSTAMP:20230824T192114Z
896 | DTSTART;VALUE=DATE:20240629
897 | DTEND;VALUE=DATE:20241001
898 | SUMMARY:Summer holidays\, no lectures
899 | CATEGORIES:HOLIDAY
900 | UID:20240303T171613Z-10572@tiss.tuwien.ac.at
901 | END:VEVENT
902 | END:VCALENDAR
903 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 3
3 | requires-python = "==3.14.*"
4 |
5 | [[package]]
6 | name = "better-tiss-calendar"
7 | version = "0.0.1"
8 | source = { virtual = "." }
9 | dependencies = [
10 | { name = "flask" },
11 | { name = "gunicorn" },
12 | { name = "icalendar" },
13 | { name = "requests" },
14 | ]
15 |
16 | [package.dev-dependencies]
17 | dev = [
18 | { name = "pyright" },
19 | { name = "pytest" },
20 | { name = "pytest-mock" },
21 | { name = "ruff" },
22 | { name = "syrupy" },
23 | { name = "ty" },
24 | ]
25 |
26 | [package.metadata]
27 | requires-dist = [
28 | { name = "flask", specifier = ">=3.1.2" },
29 | { name = "gunicorn", specifier = ">=23.0.0" },
30 | { name = "icalendar", specifier = ">=6.3.2" },
31 | { name = "requests", specifier = ">=2.32.5" },
32 | ]
33 |
34 | [package.metadata.requires-dev]
35 | dev = [
36 | { name = "pyright", specifier = ">=1.1.407" },
37 | { name = "pytest", specifier = ">=9.0.2" },
38 | { name = "pytest-mock", specifier = ">=3.15.1" },
39 | { name = "ruff", specifier = ">=0.14.10" },
40 | { name = "syrupy", specifier = ">=5.0.0" },
41 | { name = "ty", specifier = ">=0.0.4" },
42 | ]
43 |
44 | [[package]]
45 | name = "blinker"
46 | version = "1.9.0"
47 | source = { registry = "https://pypi.org/simple" }
48 | sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
49 | wheels = [
50 | { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
51 | ]
52 |
53 | [[package]]
54 | name = "certifi"
55 | version = "2025.6.15"
56 | source = { registry = "https://pypi.org/simple" }
57 | sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
58 | wheels = [
59 | { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
60 | ]
61 |
62 | [[package]]
63 | name = "charset-normalizer"
64 | version = "3.4.2"
65 | source = { registry = "https://pypi.org/simple" }
66 | sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
67 | wheels = [
68 | { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
69 | ]
70 |
71 | [[package]]
72 | name = "click"
73 | version = "8.2.1"
74 | source = { registry = "https://pypi.org/simple" }
75 | dependencies = [
76 | { name = "colorama", marker = "sys_platform == 'win32'" },
77 | ]
78 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
79 | wheels = [
80 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
81 | ]
82 |
83 | [[package]]
84 | name = "colorama"
85 | version = "0.4.6"
86 | source = { registry = "https://pypi.org/simple" }
87 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
88 | wheels = [
89 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
90 | ]
91 |
92 | [[package]]
93 | name = "flask"
94 | version = "3.1.2"
95 | source = { registry = "https://pypi.org/simple" }
96 | dependencies = [
97 | { name = "blinker" },
98 | { name = "click" },
99 | { name = "itsdangerous" },
100 | { name = "jinja2" },
101 | { name = "markupsafe" },
102 | { name = "werkzeug" },
103 | ]
104 | sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
105 | wheels = [
106 | { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
107 | ]
108 |
109 | [[package]]
110 | name = "gunicorn"
111 | version = "23.0.0"
112 | source = { registry = "https://pypi.org/simple" }
113 | dependencies = [
114 | { name = "packaging" },
115 | ]
116 | sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
117 | wheels = [
118 | { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
119 | ]
120 |
121 | [[package]]
122 | name = "icalendar"
123 | version = "6.3.2"
124 | source = { registry = "https://pypi.org/simple" }
125 | dependencies = [
126 | { name = "python-dateutil" },
127 | { name = "tzdata" },
128 | ]
129 | sdist = { url = "https://files.pythonhosted.org/packages/5d/70/458092b3e7c15783423fe64d07e63ea3311a597e723be6a1060513e3db93/icalendar-6.3.2.tar.gz", hash = "sha256:e0c10ecbfcebe958d33af7d491f6e6b7580d11d475f2eeb29532d0424f9110a1", size = 178422, upload-time = "2025-11-05T12:49:32.286Z" }
130 | wheels = [
131 | { url = "https://files.pythonhosted.org/packages/06/ee/2ff96bb5bd88fe03ab90aedf5180f96dc0f3ae4648ca264b473055bcaaff/icalendar-6.3.2-py3-none-any.whl", hash = "sha256:d400e9c9bb8c025e5a3c77c236941bb690494be52528a0b43cc7e8b7c9505064", size = 242403, upload-time = "2025-11-05T12:49:30.691Z" },
132 | ]
133 |
134 | [[package]]
135 | name = "idna"
136 | version = "3.10"
137 | source = { registry = "https://pypi.org/simple" }
138 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
139 | wheels = [
140 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
141 | ]
142 |
143 | [[package]]
144 | name = "iniconfig"
145 | version = "2.1.0"
146 | source = { registry = "https://pypi.org/simple" }
147 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
148 | wheels = [
149 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
150 | ]
151 |
152 | [[package]]
153 | name = "itsdangerous"
154 | version = "2.2.0"
155 | source = { registry = "https://pypi.org/simple" }
156 | sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
157 | wheels = [
158 | { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
159 | ]
160 |
161 | [[package]]
162 | name = "jinja2"
163 | version = "3.1.6"
164 | source = { registry = "https://pypi.org/simple" }
165 | dependencies = [
166 | { name = "markupsafe" },
167 | ]
168 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
169 | wheels = [
170 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
171 | ]
172 |
173 | [[package]]
174 | name = "markupsafe"
175 | version = "3.0.2"
176 | source = { registry = "https://pypi.org/simple" }
177 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
178 |
179 | [[package]]
180 | name = "nodeenv"
181 | version = "1.9.1"
182 | source = { registry = "https://pypi.org/simple" }
183 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
184 | wheels = [
185 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
186 | ]
187 |
188 | [[package]]
189 | name = "packaging"
190 | version = "25.0"
191 | source = { registry = "https://pypi.org/simple" }
192 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
193 | wheels = [
194 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
195 | ]
196 |
197 | [[package]]
198 | name = "pluggy"
199 | version = "1.6.0"
200 | source = { registry = "https://pypi.org/simple" }
201 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
202 | wheels = [
203 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
204 | ]
205 |
206 | [[package]]
207 | name = "pygments"
208 | version = "2.19.1"
209 | source = { registry = "https://pypi.org/simple" }
210 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
211 | wheels = [
212 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
213 | ]
214 |
215 | [[package]]
216 | name = "pyright"
217 | version = "1.1.407"
218 | source = { registry = "https://pypi.org/simple" }
219 | dependencies = [
220 | { name = "nodeenv" },
221 | { name = "typing-extensions" },
222 | ]
223 | sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
224 | wheels = [
225 | { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
226 | ]
227 |
228 | [[package]]
229 | name = "pytest"
230 | version = "9.0.2"
231 | source = { registry = "https://pypi.org/simple" }
232 | dependencies = [
233 | { name = "colorama", marker = "sys_platform == 'win32'" },
234 | { name = "iniconfig" },
235 | { name = "packaging" },
236 | { name = "pluggy" },
237 | { name = "pygments" },
238 | ]
239 | sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
240 | wheels = [
241 | { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
242 | ]
243 |
244 | [[package]]
245 | name = "pytest-mock"
246 | version = "3.15.1"
247 | source = { registry = "https://pypi.org/simple" }
248 | dependencies = [
249 | { name = "pytest" },
250 | ]
251 | sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
252 | wheels = [
253 | { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
254 | ]
255 |
256 | [[package]]
257 | name = "python-dateutil"
258 | version = "2.9.0.post0"
259 | source = { registry = "https://pypi.org/simple" }
260 | dependencies = [
261 | { name = "six" },
262 | ]
263 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
264 | wheels = [
265 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
266 | ]
267 |
268 | [[package]]
269 | name = "requests"
270 | version = "2.32.5"
271 | source = { registry = "https://pypi.org/simple" }
272 | dependencies = [
273 | { name = "certifi" },
274 | { name = "charset-normalizer" },
275 | { name = "idna" },
276 | { name = "urllib3" },
277 | ]
278 | sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
279 | wheels = [
280 | { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
281 | ]
282 |
283 | [[package]]
284 | name = "ruff"
285 | version = "0.14.10"
286 | source = { registry = "https://pypi.org/simple" }
287 | sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
288 | wheels = [
289 | { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
290 | { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
291 | { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
292 | { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
293 | { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
294 | { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
295 | { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
296 | { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
297 | { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
298 | { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
299 | { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
300 | { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
301 | { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
302 | { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
303 | { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
304 | { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
305 | { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
306 | { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
307 | ]
308 |
309 | [[package]]
310 | name = "six"
311 | version = "1.17.0"
312 | source = { registry = "https://pypi.org/simple" }
313 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
314 | wheels = [
315 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
316 | ]
317 |
318 | [[package]]
319 | name = "syrupy"
320 | version = "5.0.0"
321 | source = { registry = "https://pypi.org/simple" }
322 | dependencies = [
323 | { name = "pytest" },
324 | ]
325 | sdist = { url = "https://files.pythonhosted.org/packages/c1/90/1a442d21527009d4b40f37fe50b606ebb68a6407142c2b5cc508c34b696b/syrupy-5.0.0.tar.gz", hash = "sha256:3282fe963fa5d4d3e47231b16d1d4d0f4523705e8199eeb99a22a1bc9f5942f2", size = 48881, upload-time = "2025-09-28T21:15:12.783Z" }
326 | wheels = [
327 | { url = "https://files.pythonhosted.org/packages/9d/9a/6c68aad2ccfce6e2eeebbf5bb709d0240592eb51ff142ec4c8fbf3c2460a/syrupy-5.0.0-py3-none-any.whl", hash = "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546", size = 49087, upload-time = "2025-09-28T21:15:11.639Z" },
328 | ]
329 |
330 | [[package]]
331 | name = "ty"
332 | version = "0.0.4"
333 | source = { registry = "https://pypi.org/simple" }
334 | sdist = { url = "https://files.pythonhosted.org/packages/48/d9/97d5808e851f790e58f8a54efb5c7b9f404640baf9e295f424846040b316/ty-0.0.4.tar.gz", hash = "sha256:2ea47a0089d74730658ec4e988c8ef476a1e9bd92df3e56709c4003c2895ff3b", size = 4780289, upload-time = "2025-12-19T00:13:53.12Z" }
335 | wheels = [
336 | { url = "https://files.pythonhosted.org/packages/b1/94/b32a962243cc8a16e8dc74cf1fe75e8bb013d0e13e71bb540e2c86214b61/ty-0.0.4-py3-none-linux_armv6l.whl", hash = "sha256:5225da65a8d1defeb21ee9d74298b1b97c6cbab36e235a310c1430d9079e4b6a", size = 9762399, upload-time = "2025-12-19T00:14:11.261Z" },
337 | { url = "https://files.pythonhosted.org/packages/d1/d2/7c76e0c22ddfc2fcd4a3458a65f87ce074070eb1c68c07ee475cc2b6ea68/ty-0.0.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f87770d7988f470b795a2043185082fa959dbe1979a11b4bfe20f1214d37bd6e", size = 9590410, upload-time = "2025-12-19T00:13:55.759Z" },
338 | { url = "https://files.pythonhosted.org/packages/a5/84/de4b1fc85669faca3622071d5a3f3ec7bfb239971f368c28fae461d3398a/ty-0.0.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf68b8ea48674a289d733b4786aecc259242a2d9a920b3ec8583db18c67496a", size = 9131113, upload-time = "2025-12-19T00:14:08.593Z" },
339 | { url = "https://files.pythonhosted.org/packages/a7/ff/b5bf385b6983be56a470856bbcbac1b7e816bcd765a7e9d39ab2399e387d/ty-0.0.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc396d76a57e527393cae4ee8faf23b93be3df9e93202f39925721a7a2bb7b8", size = 9599152, upload-time = "2025-12-19T00:13:40.484Z" },
340 | { url = "https://files.pythonhosted.org/packages/36/d6/9880ba106f2f20d13e6a5dca5d5ca44bfb3782936ee67ff635f89a2959c0/ty-0.0.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c893b968d2f9964a4d4db9992c9ba66b01f411b1f48dffcde08622e19cd6ab97", size = 9585368, upload-time = "2025-12-19T00:14:00.994Z" },
341 | { url = "https://files.pythonhosted.org/packages/3f/53/503cfc18bc4c7c4e02f89dd43debc41a6e343b41eb43df658dfb493a386d/ty-0.0.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:526c925b80d68a53c165044d2370fcfc0def1f119f7b7e483ee61d24da6fb891", size = 9998412, upload-time = "2025-12-19T00:14:18.653Z" },
342 | { url = "https://files.pythonhosted.org/packages/1d/bd/dd2d3e29834da5add2eda0ab5b433171ce9ce9a248c364d2e237f82073d7/ty-0.0.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:857f605a7fa366b6c6e6f38abc311d0606be513c2bee8977b5c8fd4bde1a82d5", size = 10853890, upload-time = "2025-12-19T00:13:50.891Z" },
343 | { url = "https://files.pythonhosted.org/packages/07/fe/28ba3be1672e6b8df46e43de66a02dc076ffba7853d391a5466421886225/ty-0.0.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4cc981aa3ebdac2c233421b1e58c80b0df6a8e6e6fa8b9e69fbdfd2f82768af", size = 10587263, upload-time = "2025-12-19T00:14:21.577Z" },
344 | { url = "https://files.pythonhosted.org/packages/26/9c/bb598772043f686afe5bc26cb386020709c1a0bcc164bc22ad9da2b4f55d/ty-0.0.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b03b2708b0bf67c76424a860f848aebaa4772c05529170c3761bfcaea93ec199", size = 10401204, upload-time = "2025-12-19T00:13:43.453Z" },
345 | { url = "https://files.pythonhosted.org/packages/ac/18/71765e9d63669bf09461c3fea84a7a63232ccb0e83b84676f07b987fc217/ty-0.0.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:469890e885544beb129c21e2f8f15321f0573d094aec13da68593c5f86389ff9", size = 10129713, upload-time = "2025-12-19T00:14:13.725Z" },
346 | { url = "https://files.pythonhosted.org/packages/c3/2d/c03eba570aa85e9c361de5ed36d60b9ab139e93ee91057f455ab4af48e54/ty-0.0.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:abfd928d09567e12068aeca875e920def3badf1978896f474aa4b85b552703c4", size = 9586203, upload-time = "2025-12-19T00:14:03.423Z" },
347 | { url = "https://files.pythonhosted.org/packages/61/f1/8c3c82a8df69bd4417c77be4f895d043db26dd47bfcc90b33dc109cd0096/ty-0.0.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44b8e94f9d64df12eae4cf8031c5ca9a4c610b57092b26ad3d68d91bcc7af122", size = 9608230, upload-time = "2025-12-19T00:13:58.252Z" },
348 | { url = "https://files.pythonhosted.org/packages/51/0c/d8ba3a85c089c246ef6bd49d0f0b40bc0f9209bb819e8c02ccbea5cb4d57/ty-0.0.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9d6a439813e21a06769daf858105818c385d88018929d4a56970d4ddd5cd3df2", size = 9725125, upload-time = "2025-12-19T00:14:05.996Z" },
349 | { url = "https://files.pythonhosted.org/packages/4d/38/e30f64ad1e40905c766576ec70cffc69163591a5842ce14652672f6ab394/ty-0.0.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c3cfcf26cfe6c828e91d7a529cc2dda37bc3b51ba06909c9be07002a6584af52", size = 10237174, upload-time = "2025-12-19T00:14:23.858Z" },
350 | { url = "https://files.pythonhosted.org/packages/cb/d7/8d650aa0be8936dd3ed74e2b0655230e2904caa6077c30c16a089b523cff/ty-0.0.4-py3-none-win32.whl", hash = "sha256:58bbf70dd27af6b00dedbdebeec92d5993aa238664f96fa5c0064930f7a0d30b", size = 9188434, upload-time = "2025-12-19T00:13:45.875Z" },
351 | { url = "https://files.pythonhosted.org/packages/82/d7/9fc0c81cf0b0d281ac9c18bfbdb4d6bae2173503ba79e40b210ab41c2c8b/ty-0.0.4-py3-none-win_amd64.whl", hash = "sha256:7c2db0f96218f08c140bd9d3fcbb1b3c8c5c4f0c9b0a5624487f0a2bf4b76163", size = 10019313, upload-time = "2025-12-19T00:14:15.968Z" },
352 | { url = "https://files.pythonhosted.org/packages/5f/b8/3e3246738eed1cd695c5964a401f3b9c757d20ac21fdae06281af9f40ef6/ty-0.0.4-py3-none-win_arm64.whl", hash = "sha256:69f14fc98e4a847afa9f8c5d5234d008820dbc09c7dcdb3ac1ba16628f5132df", size = 9561857, upload-time = "2025-12-19T00:13:48.382Z" },
353 | ]
354 |
355 | [[package]]
356 | name = "typing-extensions"
357 | version = "4.14.0"
358 | source = { registry = "https://pypi.org/simple" }
359 | sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
360 | wheels = [
361 | { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
362 | ]
363 |
364 | [[package]]
365 | name = "tzdata"
366 | version = "2025.2"
367 | source = { registry = "https://pypi.org/simple" }
368 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
369 | wheels = [
370 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
371 | ]
372 |
373 | [[package]]
374 | name = "urllib3"
375 | version = "2.5.0"
376 | source = { registry = "https://pypi.org/simple" }
377 | sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
378 | wheels = [
379 | { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
380 | ]
381 |
382 | [[package]]
383 | name = "werkzeug"
384 | version = "3.1.3"
385 | source = { registry = "https://pypi.org/simple" }
386 | dependencies = [
387 | { name = "markupsafe" },
388 | ]
389 | sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
390 | wheels = [
391 | { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
392 | ]
393 |
--------------------------------------------------------------------------------