├── .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 | ![Screenshot](screenshot.png) 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 | ![Architecture Diagramm](architecture.png) 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 |
66 |
67 |
68 |
69 | Made with ❤️ by 70 | flofriday 71 |
72 |
73 | on 74 | GitHub 77 |
78 |
79 |
80 |
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 |
58 | Details: Lecture, 59 | TUWEL, 60 | Room Schedule 61 |
62 |
63 | Room: 1 Audi. Max.- 64 | ARCH-INF 65 |
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 |
  1. Open Calendar app
  2. 155 |
  3. Go to File → New Calendar Subscription (Mac) or tap Add Calendar → Add Subscription Calendar (iOS)
  4. 156 |
  5. Pate the url from above
  6. 157 |
158 | 159 | Google Calendar 160 |
    161 |
  1. Open Google Calendar (in the Web)
  2. 162 |
  3. Click the + next to "Other calendars"
  4. 163 |
  5. Paste the url form above
  6. 164 |
165 |
166 |
167 |

168 | Having problems? Contact the developer 169 |

170 |
171 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
Made with ❤️ by flofriday
179 |
180 | on GitHub 181 |
182 |
183 |
184 |
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:
" 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 | --------------------------------------------------------------------------------