pax_global_header00006660000000000000000000000064152413403360014513gustar00rootroot0000000000000052 comment=138c8453db34fbb584bbcbc592fd7bd4b972e8ef collective-icalendar-138c845/000077500000000000000000000000001524134033600160455ustar00rootroot00000000000000collective-icalendar-138c845/.claude/000077500000000000000000000000001524134033600173605ustar00rootroot00000000000000collective-icalendar-138c845/.claude/settings.json000066400000000000000000000010351524134033600221120ustar00rootroot00000000000000{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "id": "ai-prompt-auto-commit", "type": "command", "command": "input=$(cat); ts=$(date +%Y-%m-%dT%H-%M-%S); model=$(printf '%s' \"$input\" | jq -r '.model // \"claude-sonnet-4-6\"'); root=$(git rev-parse --show-toplevel 2>/dev/null || pwd); mkdir -p \"$root/.prompts\"; printf '%s' \"$input\" | jq -r '.prompt' > \"$root/.prompts/${ts}_${model}.txt\"", "version": "0.0.9" } ] } ] } } collective-icalendar-138c845/.coveragerc000066400000000000000000000002571524134033600201720ustar00rootroot00000000000000# Exclude type checking block # See https://github.com/nedbat/coveragepy/issues/831#issuecomment-517778185 [report] exclude_lines = pragma: no cover if TYPE_CHECKING:collective-icalendar-138c845/.github/000077500000000000000000000000001524134033600174055ustar00rootroot00000000000000collective-icalendar-138c845/.github/CODEOWNERS000066400000000000000000000023511524134033600210010ustar00rootroot00000000000000# CODEOWNERS file for automatic PR review assignment # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Default owners * @niccokunzmann @stevepiercy # Documentation, except docstrings in `src` *.md @stevepiercy *.rst @stevepiercy /.readthedocs.yml @stevepiercy @niccokunzmann /.vale.ini @stevepiercy /CONTRIBUTING.md @niccokunzmann @stevepiercy /docs/ @stevepiercy /docs/tutorials/ @stevepiercy @peskypotato /Makefile @stevepiercy /styles @stevepiercy # Core calendar logic /src/ @niccokunzmann @SashankBhamidi @stevepiercy @angatha # Tests /src/icalendar/tests/ @niccokunzmann @SashankBhamidi # Fuzzing /src/icalendar/fuzzing/ @ennamarie19 @niccokunzmann @SashankBhamidi # CI and infrastructure /.coveragerc @niccokunzmann /.github/ @niccokunzmann @SashankBhamidi @stevepiercy /.pre-commit-config.yaml @niccokunzmann @SashankBhamidi @stevepiercy /bootstrap.py @niccokunzmann @stevepiercy /buildout.cfg @niccokunzmann @stevepiercy /pyproject.toml @niccokunzmann @stevepiercy /tox.ini @niccokunzmann @stevepiercy # Security /SECURITY.md @niccokunzmann @stevepiercy @angatha # Release stuff /CHANGES.rst @niccokunzmann @stevepiercy /MANIFEST.in @niccokunzmann collective-icalendar-138c845/.github/FUNDING.yml000066400000000000000000000015011524134033600212170ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: python-icalendar ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] collective-icalendar-138c845/.github/ISSUE_TEMPLATE/000077500000000000000000000000001524134033600215705ustar00rootroot00000000000000collective-icalendar-138c845/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000031001524134033600242540ustar00rootroot00000000000000--- name: Bug report about: Create a bug report to help us improve icalendar title: '[Bug] ' labels: '' assignees: '' type: 'Bug' --- ## Describe the bug ## To reproduce ```python import icalendar ### ... ``` Output: ```pycon ``` ## Expected behavior ## Environment - [ ] Operating system: - [ ] Python version: - [ ] `icalendar` version: ## Additional context - [ ] I tested it with the latest version. For example, I used the command `pip3 install --upgrade https://github.com/collective/icalendar.git`. - [ ] I attached the `.ics` sample file, or there is no `.ics` sample file. To remove private information, use icalendar-anonymizer either through its [website](https://icalendar-anonymizer.com/) or [install it locally](https://docs.icalendar-anonymizer.com/latest/installation.html).collective-icalendar-138c845/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002711524134033600235600ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Discussions url: https://github.com/collective/icalendar/discussions about: For questions, ideas, and community discussions collective-icalendar-138c845/.github/ISSUE_TEMPLATE/empty-issue.md000066400000000000000000000012701524134033600243760ustar00rootroot00000000000000--- name: Blank issue about: Unstructured template for anything not covered by bug reports title: '' labels: '' assignees: '' --- collective-icalendar-138c845/.github/assistant-guidelines.md000066400000000000000000000046771524134033600241040ustar00rootroot00000000000000--- version: "0.0.8" --- # Assistant guidelines for `.prompts` storage These guidelines tell the assistant exactly how to write prompt files in this repository. - Store every user prompt in the `.prompts/` directory. - Use one file per prompt. - When a user prompt is received, automatically create a new file in `.prompts/`. - File names must follow the pattern: `YYYY-MM-DDTHH-MM-SS-XXX_raptor-mini-preview.txt` - `_` and `T` are important. - `XXX` is a zero-padded sequence number. - `YYYY-MM-DD` is the current date. - `HH-MM-SS` is the current time. - `raptor-mini-preview` is the model name. Replace this model name with the actual AI model that you use. - Use `date +%Y-%m-%d%H-%M-%S` to get the current date and time. - File content must contain only the raw prompt text from the user. - Do not add headings, bullets, annotations, or any markup. - Do not store assistant responses or metadata inside these files. - If a file name already exists, choose the next available sequence number. - If `.prompts/` does not exist, create it. - `.prompts/committed` is reserved for prompts that are already included in commit messages. - Do not create new user prompt files in `.prompts/committed`. - Do not duplicate `.prompts/committed` entries as files directly in `.prompts/`. - Do not use `/memories/` or any workspace-local memory for this storage rule. - For a fresh clone, use this file as the canonical instruction source. - If the user asks to store new prompts, then assure there is one file for each and all of the uncommitted prompts of that session. - Read the latest prompt file and find the prompts in your memory. Then store all prompts that came after this. - Use the following command to find the latest prompt file: ```shell (find .prompts -maxdepth 1 -type f -printf '%f\n'; find .prompts/committed/ -maxdepth 1 -type f -printf '%f\n' ) | sort ``` - Check that prompts that are in the `.prompts/` directory are not duplicates. - Create a new file for each prompt that was prompted to you by the user in this session if it does not already exist. - Follow all above guidelines. - Saying "Store all prompts" or "Store new prompts" or "dump prompts" or "save prompts" means this: > Read the last stored prompt that you stored and from that prompt onward, for this chat, create a new prompt file according to the assistant guidelines for each prompt that I have written since. collective-icalendar-138c845/.github/dependabot.yml000066400000000000000000000010061524134033600222320ustar00rootroot00000000000000# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: github-actions directory: / groups: github-actions: patterns: - "*" # Group all Actions updates into a single larger pull request schedule: interval: weekly collective-icalendar-138c845/.github/pull_request_template.md000066400000000000000000000044511524134033600243520ustar00rootroot00000000000000` delimiters, inclusively, may be removed. --> ## Linked issue - Closes #ISSUE_NUMBER - See #ISSUE_NUMBER ## Description ## Checklist - [ ] I added a change log entry, following the instructions in [Change log entry format](https://icalendar.readthedocs.io/en/latest/contribute/#change-log-entry-format). - [ ] I followed icalendar's [Artificial intelligence policy](https://icalendar.readthedocs.io/en/latest/contribute/index.html#artificial-intelligence-policy) and disclosed my [Responsible AI use](https://icalendar.readthedocs.io/en/latest/contribute/index.html#responsible-ai-use) in my commit messages, if applicable. - [ ] I added or updated tests, if applicable. - [ ] I ran and ensured all tests pass locally by following [Run tests](https://icalendar.readthedocs.io/en/latest/contribute/development.html#run-tests). - [ ] I added or edited documentation as necessary, both as docstrings to be rendered in the API documentation and narrative documentation, following the [Style guide](https://icalendar.readthedocs.io/en/latest/contribute/documentation/style-guide.html). ## Additional information Upload screenshots, videos, links to documentation, or any other relevant information. collective-icalendar-138c845/.github/workflows/000077500000000000000000000000001524134033600214425ustar00rootroot00000000000000collective-icalendar-138c845/.github/workflows/cifuzz.yml000066400000000000000000000023561524134033600235050ustar00rootroot00000000000000name: CIFuzz on: [pull_request] permissions: {} jobs: Fuzzing: runs-on: ubuntu-latest permissions: security-events: write steps: - name: Build Fuzzers id: build uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master with: oss-fuzz-project-name: 'icalendar' language: python - name: Run Fuzzers uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master with: oss-fuzz-project-name: 'icalendar' language: python fuzz-seconds: 600 output-sarif: true - name: Upload Crash uses: actions/upload-artifact@v7 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Sarif if: always() && steps.build.outcome == 'success' uses: github/codeql-action/upload-sarif@v4 with: # Path to SARIF file relative to the root of the repository sarif_file: cifuzz-sarif/results.sarif checkout_path: cifuzz-sarif check-distribution: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Do not include Apache files in the distribution. run: | src/icalendar/tests/test_create_release.sh collective-icalendar-138c845/.github/workflows/documentation.yml000066400000000000000000000032201524134033600250330ustar00rootroot00000000000000name: Check news fragments filenames, build documentation, check links, spelling, grammar, and style on: push: branches: - main paths: - 'docs/**' # Build pull requests pull_request: paths: - 'docs/**' - 'Makefile' - 'src/icalendar/*.py' - 'src/icalendar/cal/*.py' - 'src/icalendar/prop/*.py' - 'src/icalendar/timezone/*.py' - 'styles/**' - '.github/workflows/documentation.yml' - '.readthedocs.yaml' - '.vale.ini' - 'uv.lock' - 'news/*.*' jobs: docs: name: Documentation runs-on: ubuntu-latest strategy: matrix: python-version: ['3.11', '3.14'] steps: - uses: actions/checkout@v7 with: fetch-depth: 0 fetch-tags: true - name: Install uv uses: astral-sh/setup-uv@v7 with: # Install a specific version of uv. version: "0.9.3" - name: Set up Python uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Create Python virtual environment, install requirements run: make .venv PYTHONVERSION=${{ matrix.python-version }} - name: Check news fragments filenames run: make changes-check PYTHONVERSION=${{ matrix.python-version }} - name: Build HTML documentation run: make html PYTHONVERSION=${{ matrix.python-version }} - name: Run vale run: make vale VALEOPTS="--minAlertLevel='warning'" PYTHONVERSION=${{ matrix.python-version }} - name: Check for broken links run: make linkcheckbroken PYTHONVERSION=${{ matrix.python-version }} collective-icalendar-138c845/.github/workflows/generate_matrix.py000077500000000000000000000105301524134033600251740ustar00rootroot00000000000000#!/usr/bin/env python3 """Generate the test matrix. Parameters: 0: The branch or tag that triggered the workflow 1: The state of the pull request Documentation: - for python versions https://github.com/actions/setup-python/tree/v6/?tab=readme-ov-file#basic-usage - for pull request approval https://stackoverflow.com/a/70441157/1320237 Run tests with: pytest .github/workflows/test_generate_matrix.py """ import json import re import sys # # Configuration # PYTHON_MINOR_VERSION_MIN = 10 # Update this when a new Python minor version is released PYTHON_MINOR_VERSION_MAX = 14 COMMAND_TEST = "uv run tox -e py" def generate_matrix(git_ref, review): """Generate a matrix of test runs. Parameters: arg_ref: The branch or tag that triggered the workflow arg_pr: The state of the pull request """ # # Analyze the reference # runs_on_main = git_ref == "refs/heads/main" runs_on_stable = bool(re.match(r"refs/heads/\d+\.x", git_ref)) runs_on_release = git_ref.startswith("refs/heads/release-") runs_on_tag = git_ref.startswith("refs/tags/v") # # Analyze the trigger # # Analyze the review that triggered the workflow review_submitted = review != "" review_approved = review == "approved" # Which push to a branch or tag triggered the workflow # The trigger is either a push or a review, never both triggered_by_push_to_main = runs_on_main and not review_submitted triggered_by_push_to_release = runs_on_release and not review_submitted triggered_by_push_to_stable = runs_on_stable and not review_submitted triggered_by_push_to_tag = runs_on_tag and not review_submitted run_all_jobs = ( triggered_by_push_to_tag or triggered_by_push_to_main or triggered_by_push_to_release or triggered_by_push_to_stable or review_approved ) run_no_jobs = review_submitted and not review_approved # # Debug conditions # for k, v in sorted(locals().items()): if isinstance(v, (bool, str, int)) and not k.startswith("_"): print(f"{k} = {v!r}") # noqa: T201 # # Create matrix # matrix: list[dict[str, str | bool]] = [] # # minimal set of jobs # # RULE: always test the lowest allowed version matrix.append( { "python_version": f"3.{PYTHON_MINOR_VERSION_MIN}", "skip": run_no_jobs, } ) # RULE: use lowest allowed version for nopytz matrix.append( { "test_command": "uv run tox -e nopytz", "test_name": f"3.{PYTHON_MINOR_VERSION_MIN} (nopytz)", "python_version": f"3.{PYTHON_MINOR_VERSION_MIN}", "skip": run_no_jobs, } ) # RULE: always test the latest version matrix.append( { "python_version": f"3.{PYTHON_MINOR_VERSION_MAX}", "skip": run_no_jobs, } ) # # create full job list if required # for minor_python_version in range( PYTHON_MINOR_VERSION_MIN + 1, PYTHON_MINOR_VERSION_MAX ): matrix.append( { "python_version": f"3.{minor_python_version}", "skip": not run_all_jobs, } ) # pypy is slow but good to test matrix.insert( 0, { "python_version": "pypy3.11", "test_name": "pypy3", "skip": not run_all_jobs, }, ) # # Generate tests names for the matrix # # Note: If you change test names, also make sure that the # branch protection rules are adjusted to the new names. # https://github.com/collective/icalendar/settings/branches # for run in matrix: run.setdefault("test_name", run["python_version"]) run.setdefault("test_command", COMMAND_TEST) include = list(matrix) skipped = [run["test_name"] for run in matrix if run["skip"]] return {"include": include, "skipped": skipped} if __name__ == "__main__": # # always debug to stderr # stdout = sys.stdout sys.stdout = sys.stderr # # Parse parameters # arg_ref = sys.argv[1] # github.ref arg_review = sys.argv[2] # github.event.review.state matrix = generate_matrix(arg_ref, arg_review) print(json.dumps(matrix, indent=2)) # noqa: T201 stdout.write(json.dumps(matrix)) collective-icalendar-138c845/.github/workflows/pr-quality.yml000066400000000000000000000024421524134033600242760ustar00rootroot00000000000000# This workflow checks the quality of pull requests. # For configuration options, see: # https://github.com/marketplace/actions/anti-slop name: PR Quality permissions: contents: read issues: read pull-requests: write on: pull_request_target: types: [opened, reopened] jobs: anti-slop: runs-on: ubuntu-latest steps: - uses: peakoss/anti-slop@v0 with: max-failures: 1 max-description-length: 1500 max-emoji-count: 1 max-code-references: 3 require-linked-issue: true require-pr-template: true strict-pr-template-sections: Linked issue,Description,Checklist optional-pr-template-sections: 'Additional information' blocked-commit-authors: github-automation@zo.computer max-daily-forks: 3 exempt-users: abe-101,angatha,niccokunzmann,sashankbhamidi,stevepiercy,maurits,tobixen success-add-pr-labels: 'PR quality: good' failure-add-pr-labels: 'ai-suspicion' failure-pr-message: 'This pull request did not pass quality checks and AI use is suspected. Please review [Contribute](https://icalendar.readthedocs.io/en/latest/contribute/index.html) and make any necessary amendments.' close-pr: false min-profile-completeness: 1 collective-icalendar-138c845/.github/workflows/profile-contributors.yml000066400000000000000000000040401524134033600263560ustar00rootroot00000000000000# Run gh-profiler against the author of each new issue and PR. # See https://github.com/ehmatthes/gh-profiler name: Profile contributors on: issues: types: [opened] pull_request_target: types: [opened] permissions: contents: read issues: write pull-requests: write jobs: profile-author: runs-on: ubuntu-latest steps: - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 - name: Install gh-profiler run: uv tool install gh-profiler==0.8.3 - name: Determine target login and issue number id: meta run: | set -euo pipefail if [ "${{ github.event_name }}" = "issues" ]; then echo "login=${{ github.event.issue.user.login }}" >> "$GITHUB_OUTPUT" echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" else echo "login=${{ github.event.pull_request.user.login }}" >> "$GITHUB_OUTPUT" echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" fi - name: Run gh-profiler env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail gh-profiler "${{ steps.meta.outputs.login }}" --concise > concise.txt gh-profiler "${{ steps.meta.outputs.login }}" > full.txt - name: Comment on issue or PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail { echo "Profile summary:" echo echo '```text' cat concise.txt echo '```' echo echo '
' echo 'Full profile' echo echo '```text' cat full.txt echo '```' echo echo '
' } > body.txt jq -Rs '{body: .}' body.txt > payload.json gh api \ repos/${{ github.repository }}/issues/${{ steps.meta.outputs.number }}/comments \ --input payload.json collective-icalendar-138c845/.github/workflows/ruff.yml000066400000000000000000000022741524134033600231340ustar00rootroot00000000000000name: Check code style with ruff on: [push, pull_request] jobs: ruff: runs-on: ubuntu-latest env: MESSAGE: | Your code is not formatted! Please set up your development environment and run the formatting command. https://icalendar.readthedocs.io/en/stable/contribute/development.html#code-format steps: - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v7 with: python-version: 3.14 cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip pip install . --group formatting sudo apt install -y git-delta - name: Run the ruff formatter to change files run: ruff format . - name: Fail if we detect any changes on the file system run: | git diff | delta --light --keep-plus-minus-markers if ! git diff --exit-code --quiet; then echo "$MESSAGE" exit 1; fi env: FORCE_COLOR: '1' - name: Run ruff checks on the files run: | if ! ruff check; then echo "$MESSAGE" exit 1; fi collective-icalendar-138c845/.github/workflows/test_generate_matrix.py000066400000000000000000000075641524134033600262450ustar00rootroot00000000000000"""Test that we get the right output under all conditions. Run with: pytest .github/workflows/test_generate_matrix.py """ import pytest from generate_matrix import generate_matrix CASES_ALL = {"3.10", "3.11", "3.12", "3.13", "3.14", "3.10 (nopytz)", "pypy3"} CASES_MIN = {"3.10", "3.14", "3.10 (nopytz)"} CASES_0 = set() @pytest.fixture( params=[ # push ("", CASES_ALL, "refs/heads/main"), ("", CASES_ALL, "refs/heads/7.x"), ("", CASES_ALL, "refs/heads/6.x"), ("", CASES_ALL, "refs/heads/5.x"), ("", CASES_ALL, "refs/heads/release-1"), ("", CASES_ALL, "refs/tags/v7.0.2"), ("", CASES_MIN, "refs/heads/pr-branch"), # review ("changes_requested", CASES_0, "refs/heads/main"), ("changes_requested", CASES_0, "refs/heads/7.x"), ("changes_requested", CASES_0, "refs/heads/6.x"), ("changes_requested", CASES_0, "refs/heads/5.x"), ("changes_requested", CASES_0, "refs/heads/release-2"), ("changes_requested", CASES_0, "refs/tags/v7.0.2"), ("changes_requested", CASES_0, "refs/heads/pr-branch"), ("approved", CASES_ALL, "refs/heads/main"), ("approved", CASES_ALL, "refs/heads/7.x"), ("approved", CASES_ALL, "refs/heads/6.x"), ("approved", CASES_ALL, "refs/heads/5.x"), ("approved", CASES_ALL, "refs/heads/release-3"), ("approved", CASES_ALL, "refs/tags/v7.0.2"), ("approved", CASES_ALL, "refs/heads/pr-branch"), ], ) def cases(request): """All test cases.""" return request.param @pytest.fixture def arg_pr(cases): """The pr event name.""" return cases[0] @pytest.fixture def arg_ref(cases): """The branch or tag reference.""" return cases[2] @pytest.fixture def expected(cases): """All expected case names.""" return cases[1] @pytest.fixture def matrix(arg_ref, arg_pr): """The generated test matrix.""" matrix = generate_matrix(arg_ref, arg_pr) print("Cases:") for case in sorted(matrix["include"], key=lambda case: case["test_name"]): print(f"- running\t{case['test_name']}") for case in sorted(matrix["skipped"]): print(f"- skipped\t{case}") return matrix @pytest.fixture def skipped_names(matrix): """All skipped test cases.""" return set(matrix["skipped"]) @pytest.fixture def running_names(matrix): """All running test cases""" return {case["test_name"] for case in matrix["include"] if not case["skip"]} def test_count_test_runs(running_names, expected): """Check which values we get.""" assert running_names == expected, ( f"Expected {len(expected)} test runs, got {len(running_names)}" ) def test_running_is_not_skipped(running_names, skipped_names): """Running means not skipped.""" assert running_names.isdisjoint(skipped_names) def test_all_cases_are_always_included(running_names, skipped_names, matrix): """The matrix always needs to be complete or we might wait for required checks.""" assert running_names | skipped_names == CASES_ALL all_in_matrix = {case["test_name"] for case in matrix["include"]} assert all_in_matrix == CASES_ALL @pytest.mark.parametrize( "attribute", [ "test_name", "python_version", "skip", "test_command", ], ) def test_parameters_are_present(matrix, attribute): for case in matrix["include"]: assert attribute in case, f"Missing {attribute} in {case}" @pytest.mark.parametrize( ("arg_pr", "arg_ref"), [ # push ("", "refs/heads/main"), ("approved", "refs/heads/6.x"), ], ) def test_pypy_is_first(arg_ref, arg_pr): """PyPy takes longest to run, so it should be triggered first. See https://github.com/collective/icalendar/pull/1239#discussion_r2868711107 """ result = generate_matrix(arg_ref, arg_pr) assert result["include"][0]["test_name"] == "pypy3" collective-icalendar-138c845/.github/workflows/tests.yml000066400000000000000000000133461524134033600233360ustar00rootroot00000000000000name: tests on: push: branches: - main - 7.x tags: - v* pull_request: schedule: - cron: '14 7 * * 0' # run once a week on Sunday workflow_dispatch: pull_request_review: types: [submitted] jobs: generate-matrix: runs-on: ubuntu-latest outputs: # See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs#example-defining-outputs-for-a-job include: ${{ steps.set-matrix.outputs.include }} steps: - uses: actions/checkout@v7 - name: Set matrix id: set-matrix run: | matrix_json="$(python .github/workflows/generate_matrix.py '${{ github.ref }}' '${{ github.event.review.state }}' )" echo "include=$(echo "$matrix_json" | python -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["include"]))')" >> $GITHUB_OUTPUT run-tests: needs: generate-matrix strategy: matrix: include: ${{ fromJSON(needs.generate-matrix.outputs.include) }} runs-on: ubuntu-latest name: ${{ matrix.test_name }} steps: - name: Skip notice if: ${{ matrix.skip }} run: echo "Skipped ${{ matrix.test_name }} (not required for this trigger)" - uses: actions/checkout@v7 if: ${{ !matrix.skip }} - name: Install uv and Python ${{ matrix.python_version }} if: ${{ !matrix.skip }} uses: astral-sh/setup-uv@v7 with: enable-cache: true # Install a specific version of uv. version: "0.11.6" python-version: ${{ matrix.python_version }} - name: Install dependencies if: ${{ !matrix.skip }} run: | make dev PYTHONVERSION=${{ matrix.python_version }} uv tree - name: Test if: ${{ !matrix.skip }} run: ${{ matrix.test_command }} - name: Coveralls Parallel if: ${{ !matrix.skip }} uses: coverallsapp/github-action@v2 with: flag-name: run-${{ matrix.test_name }} parallel: true file: coverage.xml allow-empty: true fail-on-error: false coverage: # parallel test coverage upload # see https://coveralls-python.readthedocs.io/en/latest/usage/configuration.html#github-actions-support name: Submit test coverage needs: run-tests # always finalize coverage aftest tests ran # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-not-requiring-successful-dependent-jobs if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: parallel-finished: true format: cobertura fail-on-error: false test-distribution: name: Check built package runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build distribution files run: python -m build # same as in deploy-tag-to-pypi - name: Check distribution files run: twine check dist/* deploy-tag-to-pypi: # only deploy on tags, see https://stackoverflow.com/a/58478262/1320237 if: startsWith(github.ref, 'refs/tags/v') needs: - run-tests - test-distribution runs-on: ubuntu-latest # This environment stores the TWINE_USERNAME and TWINE_PASSWORD # see https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment environment: name: PyPI url: https://pypi.org/project/icalendar/ # after using the environment, we need to make the secrets available # see https://docs.github.com/en/actions/security-guides/encrypted-secrets#example-using-bash env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} steps: - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v7 with: python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: remove old files run: rm -rf dist/* - name: build distribution files run: python -m build # same as in test-distribution - name: deploy to pypi run: | # You will have to set the variables TWINE_USERNAME and TWINE_PASSWORD # You can use a token specific to your project by setting the user name to # __token__ and the password to the token given to you by the PyPI project. # sources: # - https://shambu2k.hashnode.dev/gitlab-to-pypi # - http://blog.octomy.org/2020/11/deploying-python-pacakges-to-pypi-using.html?m=1 if [ -z "$TWINE_USERNAME" ]; then echo "WARNING: TWINE_USERNAME not set!" fi if [ -z "$TWINE_PASSWORD" ]; then echo "WARNING: TWINE_PASSWORD not set!" fi twine check dist/* twine upload dist/* deploy-github-release: # only deploy on tags, see https://stackoverflow.com/a/58478262/1320237 if: startsWith(github.ref, 'refs/tags/v') needs: - run-tests - test-distribution runs-on: ubuntu-latest environment: name: github-release steps: - uses: actions/checkout@v7 - name: Create GitHub release from tag uses: ncipollo/release-action@v1 with: allowUpdates: true body: "To view the changes, please see the [Changelog](https://icalendar.readthedocs.io/en/latest/reference/changelog.html). This release can be installed from [PyPI](https://pypi.org/project/icalendar/#history)." generateReleaseNotes: false collective-icalendar-138c845/.github/workflows/zizmor.yml000066400000000000000000000013531524134033600235210ustar00rootroot00000000000000name: zizmor static analysis on: pull_request: push: branches: [main] permissions: contents: read jobs: zizmor: runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install zizmor run: pip install zizmor==1.29.0 - name: Run zizmor run: zizmor --format sarif .github/workflows/ > zizmor-results.sarif - name: Upload SARIF file uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 with: sarif_file: zizmor-results.sarif collective-icalendar-138c845/.gitignore000066400000000000000000000006301524134033600200340ustar00rootroot00000000000000*.mo *.py? *.sw? *~ .* _build/ bin/ build/ coverage-* develop-eggs/ dist/ eggs/ htmlcov/ include/ lib/ lib64 parts/ pip-selfcheck.json src/icalendar.egg-info/ !.gitattributes !.github !.gitignore venv /docs/styles/Microsoft/ /ical_fuzzer.pkg.spec /src/icalendar/_version.py uv.lock coverage.xml !.coveragerc !.well-known /.prompts/ /ical_fuzzer/ # from tutorials /example.ics !.claude/ !.claude/settings.jsoncollective-icalendar-138c845/.pre-commit-config.yaml000066400000000000000000000013131524134033600223240ustar00rootroot00000000000000default_install_hook_types: [pre-commit, prepare-commit-msg, post-commit] repos: - repo: https://github.com/pycalendar/ai-prompt-auto-commit rev: v0.0.9 hooks: - id: prepare-ai-repository - id: unstage-ai-prompts - id: append-ai-prompts - id: archive-ai-prompts - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.2 # also update pyproject.toml hooks: # Run the linter. - id: ruff-check args: [--config, "pyproject.toml", --fix] # Run the formatter. - id: ruff-format args: [--config, "pyproject.toml"] collective-icalendar-138c845/.readthedocs.yml000066400000000000000000000023571524134033600211420ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Optionally build your docs in additional formats such as PDF and ePub formats: all build: os: ubuntu-22.04 tools: python: "3.13" commands: # Cancel building pull requests when there aren't changes in the docs directory or YAML file. # You can add any other files or directories that you'd like here as well, # like your docs requirements file, or other files that will change your docs build. # # If there are no changes (git diff exits with 0) we force the command to return with 183. # This is a special exit code on Read the Docs that will cancel the build immediately. - | if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- . docs/ \ src/icalendar/**/*.py \ .github/workflows/documentation.yml \ .readthedocs.yml \ .vale.ini \ CHANGES.rst \ Makefile \ pyproject.toml \ ':!src/icalendar/fuzzing' \ ':!src/icalendar/tests' \ ':!src/icalendar/timezone'; then exit 183; fi - git fetch --tags || true - make rtd-pr-preview collective-icalendar-138c845/.vale.ini000066400000000000000000000006271524134033600175600ustar00rootroot00000000000000StylesPath = docs/styles MinAlertLevel = suggestion Vocab = Base,icalendar Packages = Microsoft [*] BasedOnStyles = Vale, Microsoft Microsoft.Contractions = suggestion Microsoft.GeneralURL = No Microsoft.Units = suggestion # Ignore Sphinx Python object roles, such as ":class:" and ":meth:", # to prevent false positives from Microsoft.Spacing on dotted Python names. TokenIgnores = :[a-z]+:\x60.+\x60 collective-icalendar-138c845/.well-known/000077500000000000000000000000001524134033600202205ustar00rootroot00000000000000collective-icalendar-138c845/.well-known/funding-manifest-urls000066400000000000000000000000751524134033600243660ustar00rootroot00000000000000https://github.com/collective/icalendar/raw/main/funding.jsoncollective-icalendar-138c845/CHANGES.rst000066400000000000000000003411301524134033600176510ustar00rootroot00000000000000========== Change log ========== .. py:currentmodule:: icalendar .. Do *NOT* add new change log entries to this file. Instead create a file in the news directory. For helpful instructions, see: https://icalendar.readthedocs.io/en/latest/contribute/#change-log-entry-format .. towncrier release notes start 7.3.0 (2026-08-19) ------------------ Removals and deprecations ~~~~~~~~~~~~~~~~~~~~~~~~~ - :attr:`~icalendar.prop.binary.vBinary.obj` is deprecated in favor of :attr:`~icalendar.prop.binary.vBinary.bytes`. It is retained as a lossy string view of the value for backward compatibility, and will be removed in icalendar 8. The ``test_ical_value_rejects_non_base64_characters`` test was removed because :attr:`vBinary.ical_value ` no longer validates or decodes Base64. @uwezkhan (`Issue #1356 `_) - `Issue #1362 `_ New features ~~~~~~~~~~~~ - Created an :meth:`~cal.journal.Journal.example` method for the :class:`~cal.journal.Journal` component and an :meth:`~cal.free_busy.FreeBusy.example` method for the :class:`~cal.free_busy.FreeBusy` component, added the :rfc:`5545` examples that they load, and documented their usage in both class docstrings. AI assistance: Kiro CLI with the claude-opus-5 model was used to draft this change. @Souptik96 (`Issue #743 `_) - Add :meth:`Alarm.new_display() `, :meth:`Alarm.new_audio() `, and :meth:`Alarm.new_email() ` factory methods for creating :rfc:`5545#section-3.6.6` VALARM components. @klouds27 (`Issue #865 `_) - :class:`~icalendar.prop.binary.vBinary` now stores the raw ``bytes`` of the value internally, so non-UTF-8 binary data round-trips losslessly. With the release of icalendar 7.1.0, and previous to this bug fix, :attr:`vBinary.ical_value ` Base64-decoded the raw stored bytes and raised :class:`ValueError` for non-Base64 input. :attr:`vBinary.ical_value ` now returns the raw stored bytes instead of Base64-decoding them, and no longer raises :class:`ValueError` for non-Base64 input. @uwezkhan (`Issue #1356 `_) - Added :attr:`icalendar.prop.binary.vBinary.bytes`, exposing the raw binary value of a BINARY property. @uwezkhan (`Issue #1356 `_) - Added the :exc:`~icalendar.error.ICalParsingError` exception as a public API for programmatic handling of iCalendar parsing failures. AI assistance: ChatGPT was used to help debug terminal errors encountered during development. @GAUTAMSANSWAL (`Issue #1536 `_) - Created a script to automatically generate the :mod:`~icalendar.timezone.windows_to_olson` module from the Unicode Consortium's CLDR GitHub repository. Added lxml as a new development dependency. Added the target ``wo`` to the ``Makefile`` to generate the file from the root of the repository with `make wo`. @stevepiercy (`Issue #1548 `_) - Added :attr:`vBinary.base64data `, a getter/setter for the Base64 string view of a BINARY property's value, so callers no longer need to call :func:`base64.b64encode` or :func:`base64.b64decode` manually. @gagana2023 (`Issue #1550 `_) - Added :attr:`Alarm.ACTION ` and :attr:`AlarmTime.action ` access to the :rfc:`5545#section-3.8.6.1` ``ACTION`` property. I used OpenAI Codex with GPT-5 to assist with this change, then reviewed and tested the result. @bm1016bm-svg (`Issue #1569 `_) - Record the exact CLDR commit used to generate the Windows-to-Olson timezone mapping and fetch the mapping from that pinned revision. I used AI to assist me with this change. @patrickswedish (`Issue #1575 `_) - Used :meth:`vBinary.from_ical() ` in :attr:`Image.data ` for consistent Base64 validation, removing the manual :func:`base64.b64decode` call. @klouds27 (`Issue #1638 `_) - Added :attr:`REQUEST_STATUS` property to :class:`~icalendar.event.Event`, :class:`~icalendar.todo.Todo`, :class:`~icalendar.journal.Journal`, and :class:`~icalendar.free_busy.FreeBusy` with ``request_status`` parameter in ``new()`` constructors per :rfc:`5545#section-3.8.8.3`. I used AI to assist with this change. @SemTiOne (`Issue #1666 `_) - Improved :exc:`~icalendar.error.ICalParsingError` by including optional value, line, and line number information in exception messages for more consistent iCalendar parsing error reporting. AI assistance: ChatGPT was used to help debug terminal errors encountered during development. @GAUTAMSANSWAL - Made :exc:`~icalendar.error.JCalParsingError` inherit from :exc:`~icalendar.error.InvalidCalendar` for consistency with :exc:`~icalendar.error.ICalParsingError`. AI assistance: ChatGPT was used to help debug terminal errors encountered during development. @GAUTAMSANSWAL Bug fixes ~~~~~~~~~ - Resolve the timezone of a "globally unique" :rfc:`5545#section-3.2.19` ``TZID`` such as ``/freeassociation.sourceforge.net/Europe/Berlin``, which is emitted by some clients, including libical and Evolution. :meth:`TZP.timezone ` now strips the leading vendor prefix and resolves the trailing Olson identifier, so the value is parsed as timezone-aware instead of being silently treated as naive. Prepared with AI assistance. @vjsai (`Issue #313 `_) - Fixed the `equivalent_timezone_ids.py` script after the call to generate the timezone Python code was removed. @gjabell (`Issue #1392 `_) - Values of unrecognized properties and ``X-`` properties without a ``VALUE`` parameter were altered by escaping when parsed, serialized, or converted to and from jCal, so they did not round-trip unchanged as :rfc:`7265` specifies. These values are now preserved verbatim. Additionally, the ``PROXIMITY`` property (:rfc:`9074`) was treated as an unknown value type instead of ``TEXT`` and is now recognized correctly. AI disclosure: I used Claude Code (Anthropic's Claude Opus) to draft and refine this change and its tests; I reviewed the output and validated the change locally. @lcampanella98 (`Issue #1445 `_) - Ship :file:`funding.json` in PyPI source distributions so packaging and test runs from the sdist no longer fail with :class:`FileNotFoundError`. @Hishamkhashman1 (`Issue #1493 `_) - Avoid folding content lines between escape prefixes and their escaped characters. AI disclosure: I used GPT-5 Codex to help draft and refine this change and its tests; I reviewed and validated the output locally. @kingrubic (`Issue #1501 `_) - Preserve ``URI`` value parameters on a jCal round-trip. :meth:`vUri.from_jcal ` previously passed the parameters positionally, but ``vUri.__new__`` takes ``encoding`` as its second positional parameter. As a result, the ``URI`` value parameters were consumed as ``encoding`` and dropped. For example, ``ATTACH;FMTTYPE=...`` or ``CONFERENCE;FEATURE=...`` lost their parameters. They are now passed as the ``params`` keyword. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4.8). @gaoflow (`Issue #1526 `_) - Updated the Windows timezone mapping for ``Mountain Standard Time (Mexico)`` to ``America/Mazatlan``, so dates after Mexico's 2022 timezone changes use the correct UTC-7 offset instead of UTC-6. Prepared with AI assistance. @RobHannay (`Issue #1546 `_) - ``IMAGE`` properties now use their explicitly declared ``VALUE`` type instead of always being parsed as unknown, while ``IMAGE`` without a ``VALUE`` parameter remains unknown. I used OpenAI Codex with GPT-5 to assist with this change. @floze-the-genius (`Issue #1561 `_) - Removed ``ATTACH`` from :attr:`Alarm.singletons `. An "email" ``VALARM`` may carry more than one ``ATTACH`` per :rfc:`5545#section-3.6.6`, so ``ATTACH`` belongs in ``multiple`` only; it was previously listed in both ``singletons`` and ``multiple``. Added a regression test that ``singletons`` and ``multiple`` are disjoint for every registered component. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4.8). @vtino17 (`Issue #1569 `_) - :attr:`Alarm.REPEAT `, :attr:`Alarm.repeat `, :attr:`Event.sequence `, and :attr:`Event.priority ` now reject non-integers (including booleans) with :exc:`TypeError` and negative values with :exc:`~icalendar.error.InvalidCalendar`. @heyncth (`Issue #1594 `_) - Added the missing ``BINARY`` member to :class:`icalendar.enums.VALUE`, per :rfc:`5545#section-3.2.20`. ``VALUE.BINARY`` previously raised ``AttributeError``. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4.8). @vtino17 (`Issue #1607 `_) - A ``PERIOD`` value written with dates instead of datetimes, such as ``RDATE;VALUE=PERIOD:19970101/19970102``, was accepted when a calendar was parsed and then raised an :exc:`AttributeError` when it was written out again, or a :exc:`TypeError` where only one half was a date. Such a value is now read as midnight, in the timezone of the other half where it has one. I used AI to assist me with this change. @lcampanella98 (`Issue #1633 `_) - When :func:`icalendar.use_pytz` was in effect, a date parsed with a ``TZID``, such as ``RDATE;TZID=America/New_York;VALUE=DATE:19970101``, was given the timezone's oldest known offset, its local mean time, so the value was minutes off. It is now read with the offset that applied on that date. Calendars read with zoneinfo, the default, were not affected. I used AI to assist me with this change. @lcampanella98 (`Issue #1633 `_) - Escape a lone ``\r`` as ``\n`` in :func:`icalendar.parser.string._escape_char`, used by :meth:`vText.to_ical `. A carriage return not followed by a line feed was previously left raw in the serialized content line, so a ``SUMMARY`` or ``DESCRIPTION`` built from untrusted text could carry a control character into the iCalendar stream and split the line for lenient consumers. ``\r\n`` and ``\n`` were already escaped, and the parameter escaper already mapped ``\r`` to ``^n``. @alhudz (`Pull Request #1462 `_) - Reject non-ASCII digits in the ``ordwk`` part of :class:`~icalendar.prop.recur.weekday.vWeekday` (``BYDAY``, ``BYWEEKDAY``, and ``WKST``). The value validation regular expression ``\d`` matches non-ASCII digits, such as the Arabic-Indic ``١٢``. A value such as ``١٢MO`` was silently accepted as ``relative == 12``, instead of being rejected, as required by :rfc:`5545#section-3.3.10` (``ordwk = 1*2DIGIT``). This mirrors the earlier :class:`~icalendar.prop.recur.month.vMonth` fix. Drafted with AI assistance (Claude Opus 4.8); reviewed, tested, and verified by the author. @Labib-Bin-Salam - Reject raw CR and LF in :class:`~icalendar.prop.uri.vUri`, :class:`~icalendar.prop.cal_address.vCalAddress`, :class:`~icalendar.prop.inline.vInline`, and :class:`~icalendar.prop.xml_reference.vXmlReference`. These value types are not escaped on serialization, so a lone ``\r`` in the value was written straight into the content line, slipping past the newline-only assertion in ``Contentline`` and reparsing as a separate property. This was reachable from untrusted jCal via ``URL``, ``ATTENDEE``, and ``ORGANIZER``. @alhudz - jCal property names, parameter names, and ``RRULE`` part names are now validated as lowercase iCalendar tokens when parsing jCal (RFC 7265 sections :rfc:`7265#section-3.4`, :rfc:`7265#section-3.5`, and :rfc:`7265#section-3.6.10`). They were kept verbatim and re-emitted into the content line on serialization, so a name containing ``:``, ``;``, or a lone carriage return could inject parameters or a new content line for consumers that treat a bare ``\r`` as a break. @alhudz Documentation ~~~~~~~~~~~~~ - Added return type annotations to the ``new()`` factory methods of the :mod:`icalendar.cal` components, such as :meth:`Event.new `. @ShirayukiRin (`Issue #938 `_) - Added a return type hint to the :meth:`TZP.use_default ` method. @Vishwa0223 (`Issue #938 `_) - Added type annotations to ``__init__`` methods across the package. @ShirayukiRin (`Issue #938 `_) - Added a return type annotation to :meth:`~icalendar.alarms.Alarms.add_component`. I used OpenAI GPT-5 Codex to identify the scoped method, draft the annotation and test, and review the validation results. @kudala-bharani (`Issue #938 `_) - Added a ``bool`` return type annotation to :meth:`~icalendar.parser.parameter.Parameters.is_utc`. I used OpenAI Codex (GPT-5) to identify the method. @Ace-0 (`Issue #938 `_) - Improved docstrings in :mod:`icalendar.error` to follow the project's Google-style docstring format, adding ``Parameters`` and ``Returns`` sections where appropriate. I used AI to assist me with this change. @Vrinda0211 (`Issue #1072 `_) - Added a docstring to the :func:`~icalendar.cli.main` function to follow the project's Google Style guide. @Areej-cs (`Issue #1072 `_) - Documented the construction and truth-value usage of :class:`~icalendar.prop.boolean.vBoolean`. I used OpenAI Codex with GPT-5 to assist with drafting and validation. @sahilmathur254 (`Issue #1118 `_) - Fixed unqualified ``InvalidCalendar`` and ``IncompleteComponent`` references in :class:`~icalendar.cal.todo.Todo` docstrings so they link in the API docs. I used AI to assist me with this change. @mitre88 (`Issue #1158 `_) - Fixed the broken link to :meth:`Component.to_ical ` in the documentation of how to handle parsing errors. AI disclosure: used Claude Sonnet 5 with the prompt "Fix broken links to Python objects in parse-errors.rst" to assist with this change. @SemTiOne (`Issue #1158 `_) - Replaced RFC copy-paste text in docstrings with Pythonic docstrings for calendar components. @ykd007 (`Issue #1244 `_) - Corrected the :meth:`Journal.new ` documentation so its parameter list matches the public method signature. I used AI to assist with this change. @w3lld1 (`Issue #1473 `_) - Corrected the ``FreeBusy.new()`` docstring, which described the method as creating an alarm instead of a FreeBusy component. Written with assistance from Claude Opus 4.5 (Anthropic). @andycheng2018 (`Issue #1473 `_) - Correct public docstrings whose prose looked like unsupported section headings. This contribution was drafted with AI assistance and reviewed by @kingrubic. (`Issue #1481 `_) - Clarified optional parameters and auto-generated ``UID`` and ``DTSTAMP`` values in the :meth:`~icalendar.cal.event.Event.new` docstring. AI disclosure: Grok assisted with reviewing RFC terminology and drafting the docstring update. @checkzhao8888 (`Issue #1504 `_) - Mention `icalendar-anonymizer `_ to remove private information from iCalendar files for bug reports and `Python Calendaring Ecosystem `_ as a resource. @stevepiercy (`Issue #1511 `_) - Consolidate and alphabetize the docstring parameters of the :class:`~icalendar.cal.alarm.Alarm` factory methods :meth:`~icalendar.cal.alarm.Alarm.new_display`, :meth:`~icalendar.cal.alarm.Alarm.new_audio`, and :meth:`~icalendar.cal.alarm.Alarm.new_email`. Prefixed required parameters with ``Required.`` @Solaris-star (`Issue #1591 `_) - Revised the security policy to use GitHub Security Advisory procedure instead of emailing the Plone security team, and clarified language. @stevepiercy (`Issue #1612 `_) - Revised how to document parameters in docstrings, specifically their order of appearance and usage of type hints. Revised guidance to write docstrings only for a class, and not their ``__init__`` or ``__new__`` methods. @stevepiercy (`Issue #1618 `_) - Updated the ``categories`` property documentation to include the ``Available`` and ``Availability`` components and its RFC 7953 conformance reference. (`Issue #1624 `_) - Changed ``date-time`` to ``datetime`` in the narrative parts of the ``RECURRENCE-ID`` and ``ACKNOWLEDGED`` documentation. AI-assisted. @atirna (`Issue #1655 `_) - Adapt pull request requirements to better handle AI slop. @stevepiercy - Add an RFC cross-reference and a ``Returns`` section to the :meth:`Contentline.value_separator_index ` docstring. @alhudz - Added a bullet point for reviewing pull requests to "Examples of how to contribute." @stevepiercy - Consolidate maintainers list with an include. @stevepiercy - Documented how to join the icalendar team. @stevepiercy - Restored the removed docstrings under the :class:`~icalendar.prop.integer.vInt` attribute declarations for :attr:`~icalendar.prop.integer.vInt.max` and :attr:`~icalendar.prop.integer.vInt.min`. @stevepiercy Internal changes ~~~~~~~~~~~~~~~~ - Renamed the internal ``timezone_datetime_property`` helper in ``icalendar.attr`` to ``_timezone_datetime_property``. It was never part of the public API (not listed in ``__all__``) and is only used at import time to build ``rfc_7953_dtstart_property`` and ``rfc_7953_dtend_property``, so no deprecation warning was added. I used AI to assist me with this change. @AhmedYoussef98 (`Issue #1011 `_) - Added automated tests for docstring structure and quality. @KartavyaDikshit (`Issue #1481 `_) - Added zizmor static analysis workflow for GitHub Actions security auditing. @srurora (`Issue #1487 `_) - Ensure that news fragment filenames are valid in CI. @stevepiercy (`Issue #1499 `_) - Add Commit-Check GitHub Action to detect commits by automated AI agents and bots. @stevepiercy (`Issue #1514 `_) - The Windows-to-Olson mapping test now verifies each mapped timezone resolves to the expected identifier. I used OpenAI Codex with GPT-5 to assist with this change. @floze-the-genius (`Issue #1562 `_) - Bumped maximum pull request description character count to 1500. Added instructions as comments in the pull request template. @stevepiercy (`Issue #1602 `_) - Added a ``make zizmor`` target that runs zizmor static analysis on the GitHub Actions workflows in the repository, matching the version and command used in the continuous integration workflow. Added zizmor usage documentation. AI disclosure: used Claude Sonnet 5 with the prompt "Create a make zizmor target for local static analysis of GitHub Actions workflows" to assist with this change. @SemTiOne (`Issue #1639 `_) - Removed two unreachable code branches from :meth:`vOrg.from_ical ` and :meth:`vOrg.from_jcal `. I used Claude Code (Anthropic's Claude Sonnet 5) to identify and remove the dead branches and to update the accompanying coverage tests; I reviewed the diff and ran the test suite locally. @vjymisal0 (`Issue #1641 `_) - Added ``security`` as a valid news fragment. @stevepiercy - Added anti-slop GitHub Action to detect commits by automated AI agents and bots. Removed useless Commit-Check GitHub Action. See related `discussion `_. @stevepiercy - Adjusted the anti-slop GitHub Action settings of ``max-failures`` to ``1`` to surface any number of failures, and the ``min-profile-completeness`` setting from its default of ``4`` to ``1`` to require at least one human-added profile attribute. @stevepiercy - Don't close pull requests automatically when the anti-slop GitHub Action fails. @stevepiercy - Made "Instructions" not a section heading in the pull request template. "Instructions" was interpreted as a section heading, even though it is in a comment. @stevepiercy - Rename anti-slop action. @stevepiercy Other tasks ~~~~~~~~~~~ - `Issue #698 `_, `Issue #698 `_, `Issue #698 `_, `Issue #743 `_, `Issue #1541 `_, `Issue #1549 `_, `Issue #1573 `_, `Issue #1592 `_ 7.2.2 (2026-07-20) ------------------ Documentation ~~~~~~~~~~~~~ - Fixed duplicate ``vInt.min`` and ``vInt.max`` descriptions in the API reference that caused the Read the Docs build to fail. 7.2.1 (2026-07-20) ------------------ Security fixes ~~~~~~~~~~~~~~ - Fixed `GHSA-qjcq-q7h7-r74v `_: a crafted ``REPEAT`` on a ``VALARM`` could exhaust memory or CPU via :attr:`Alarm.triggers ` and :class:`~icalendar.alarms.Alarms`. Expansion is now capped at ``icalendar.config.MAX_ALARM_REPEAT``. Its default is ``10000``. Set to ``-1`` to disable the cap. :class:`~icalendar.prop.integer.vInt` now also enforces the :rfc:`5545` signed 32-bit integer range on parse. @SashankBhamidi 7.2.0 (2026-06-23) ------------------ Removals and deprecations ~~~~~~~~~~~~~~~~~~~~~~~~~ - Deprecated :func:`icalendar.parser.string.foldline` as a private function for icalendar version 8. @IoannaGiag (`Issue #1011 `_) - Deprecated :func:`icalendar.parser.string.escape_char`, :func:`icalendar.parser.string.unescape_char`, :func:`icalendar.parser.string.escape_string`, and :func:`icalendar.parser.string.unescape_string` using the standard ``deprecate_for_version_8`` wrapper as per issue #1405. (`Issue #1405 `_) New features ~~~~~~~~~~~~ - Created an :attr:`~icalendar.prop.recur.frequency.vFrequency.ical_value` property for the :class:`~icalendar.prop.recur.frequency.vFrequency` component, mirroring the existing pattern on :class:`~icalendar.prop.recur.weekday.vWeekday`. @mvanhorn (`Issue #876 `_) - Created an :attr:`~icalendar.prop.geo.vGeo.ical_value` property for the :class:`~icalendar.prop.geo.vGeo` component. @IoannaGiag (`Issue #876 `_) - Added type hints to component methods. @Priyanshu-pulak (`Issue #938 `_) - Added test coverage for :func:`icalendar.compatibility.deprecate_for_version_8`. AI disclosure: I used GPT-5 Codex to help draft and refine the test and pull request text. I reviewed the output and validated the change locally. @iccccccccccccc (`Issue #1407 `_) - The :attr:`Alarm.uid ` accessor now falls back to vendor-specific UID properties (the existing ``X-ALARMUID`` and the newly added ``X-EVOLUTION-ALARM-UID``) when no canonical ``UID`` is present, so alarms exported by Evolution/GNOME Calendar expose a usable identifier. ``single_string_property`` now accepts an ordered list of fallback keys so further vendor aliases can be added later. AI disclosure: I used GPT-5 Codex (via the Codex CLI) to draft and refine this change and its tests; I reviewed and validated the output locally. @mvanhorn (`Issue #1421 `_) - The ``rdates`` and ``exdates`` properties are now writable: assigning a list replaces the ``RDATE``/``EXDATE`` values, ``del`` (or assigning an empty list or ``None``) clears them, and assigning the value the getter returns round-trips. Prepared with the assistance of an AI coding agent (Anthropic's Claude). @gaoflow (`Issue #1442 `_) Bug fixes ~~~~~~~~~ - Strictly validate :class:`~icalendar.prop.dt.datetime.vDatetime` values in :meth:`~icalendar.prop.dt.datetime.vDatetime.from_ical` and reject malformed input. This also improves handling of values with a ``TZID`` prefix, per :rfc:`5545#section-3.3.5`, Form #3. @uwezkhan (`Issue #1361 `_) - Preserve an explicit ``VALUE`` parameter (for example ``RDATE;VALUE=PERIOD`` or ``TRIGGER;VALUE=DATE-TIME``) when converting from jCal. Previously :meth:`Component.from_jcal ` dropped the value type, which is encoded in the jCal type field rather than as a parameter. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4). @gaoflow @lcampanella98 (`Issue #1426 `_) - For a newline-free string that the operating system can't use as a file path—for example, one containing an embedded null byte or one that is too long—treat it as calendar data, instead of propagating an :exc:`OSError` from :meth:`Component.from_ical `. Such input now raises a consistent :exc:`ValueError` across platforms. The string-versus-path boundary is now covered by tests. @uwezkhan (`Issue #1436 `_) - Accept the ``(dt, None)`` form that ``rdates``/``exdates`` return for a single date when adding ``RDATE`` or ``EXDATE``, so ``event.add("RDATE", (dt, None))`` no longer raises :exc:`TypeError` and the value round-trips. Prepared with the assistance of an AI coding agent (Anthropic's Claude Opus 4). @gaoflow @texttheater (`Issue #1439 `_) - Anchored the value validation regular expressions with ``\Z`` instead of ``$`` in :class:`~icalendar.prop.recur.weekday.vWeekday`, :meth:`vDuration.from_ical `, :class:`~icalendar.prop.dt.time.vTime` and :class:`~icalendar.prop.dt.utc_offset.vUTCOffset`. ``$`` matches just before a final ``\n``, so a value with a trailing line break was accepted; for ``vWeekday`` the newline survived in the ``str`` value and was re-emitted into RECUR output. @alhudz - For iCalendar files that have thousands of bare line breaks, reduce the content line parse time by a quadratic order of magnitude when reading a component with :meth:`Component.from_ical `. @alhudz - Parse a ``CATEGORIES`` value that contains an unescaped colon correctly. ``TEXT`` values do not escape ``:``, so a category such as ``CATEGORIES:CONFIDENTIAL,http://example.com/tag`` was truncated to a single ``//example.com/tag`` because the value boundary was found with the last colon on the line instead of the first one outside the parameters. @alhudz - Parse and serialize jCal iteratively so that deeply nested components no longer raise an uncaught :exc:`RecursionError`. :meth:`Component.from_jcal ` and :meth:`Component.to_jcal ` now handle arbitrary nesting depth, matching the iterative iCal parser and serializer. @arshsmith - Preserve literal percent escapes in parameter values. The parameter parser used ``%2C``/``%3A``/``%3B``/``%5C`` as internal markers for backslash-escaped delimiters, so a value that already contained those sequences (e.g. ``ALTREP="http://x/a%2Cb"``) was silently decoded to ``http://x/a,b``. The internal transport encoding now escapes ``%`` itself, leaving real percent-encoded values untouched. @alhudz - Raise :class:`~icalendar.error.InvalidCalendar` instead of leaking :exc:`OverflowError` when a ``DURATION`` value is too large for :class:`datetime.timedelta`, for example, ``P999999999999999999W``. Parsing a whole calendar now records such a value as an error, like any other invalid property, rather than aborting the parse. @arshsmith - Reject non-ASCII digits in :class:`~icalendar.prop.recur.month.vMonth` (``BYMONTH``). ``str.isdigit`` is ``True`` for digits like the Arabic-Indic ``١٢``, which was silently accepted and normalized to month ``12``, while characters such as ``²`` cleared the same guard and then leaked a raw ``int()`` error. :meth:`vMonth.from_ical ` now rejects both. @alhudz - Reject non-finite FLOAT values in :meth:`vFloat.from_ical() ` and :meth:`vGeo.from_ical() `. Inputs such as ``nan``, ``inf`` or an overflowing magnitude like ``1e999`` were silently accepted, producing ``NaN``/``Infinity`` tokens in :meth:`Component.to_json ` output that are not valid JSON. @alhudz - Strictly validate :class:`~icalendar.prop.dt.date.vDate` and :class:`~icalendar.prop.dt.time.vTime` values in their ``from_ical`` methods and reject malformed input, matching the strictness added to :class:`~icalendar.prop.dt.datetime.vDatetime`. Trailing data and ``int()`` quirks (underscores, whitespace, signs) are no longer silently accepted, per :rfc:`5545#section-3.3.4` and :rfc:`5545#section-3.3.12`. This change was prepared with AI assistance (Anthropic Claude, Opus model, via Claude Code) used to research the relevant RFC grammar and draft the validation and tests, all reviewed and verified locally. @alhudz Documentation ~~~~~~~~~~~~~ - Switch :file:`docs/how-to/usage.rst` from the deprecated ``.. code:: pycon`` directive to ``.. code-block:: pycon`` for consistency with the rest of the documentation. @mvanhorn (`Issue #626 `_) - Added type hint for ``encoding`` parameter in :func:`~icalendar.parser_tools.data_encode`. @cybs-joe (`Issue #938 `_) - Converted docstring of :meth:`~icalendar.cal.component.Component.add` to Google style. @mvanhorn (`Issue #1072 `_) - Fixed broken links and removed `:py` prefix in :class:`icalendar.cal.calendar.Calendar` documentation. @lcampanella98 (`Issue #1158 `_) - Fixed broken link and removed `:py` prefix in the :func:`~icalendar.parser.unescape_backslash` docstring. @vincere-mori (`Issue #1158 `_) - Fixed broken documentation links in :mod:`icalendar.alarms` and :mod:`icalendar.cal.alarm`. @lcampanella98 (`Issue #1158 `_) - Fix unqualified cross-references in :attr:`Event.start ` and :attr:`Event.start ` docstrings. @Esneider1107 (`Issue #1158 `_) - Fixed broken links in :mod:`icalendar.cal.free_busy` documentation by using fully qualified :class:`icalendar.cal.component.Component` attribute targets. @tsai135 (`Issue #1158 `_) - Qualify ambiguous cross-references and remove remaining ``:py`` prefixed roles. @wahajahmed010 (`Issue #1158 `_) - Fixed broken documentation links in :mod:`icalendar.cal.availability`. @lcampanella98 (`Issue #1158 `_) - Replaced the RFC quotation in the docstring for :class:`vBoolean ` with a Pythonic description, including parsing behavior, a conformance reference, and a working example. @tsai135 (`Issue #1244 `_) - Improved the documentation for ``attr.url_property`` by converting raw RFC text into a comprehensive Python docstring with usage examples and explicit RFC cross-references. Assisting tool: Gemini 1.5. @samrodri911 (`Issue #1244 `_) - Improved the :attr:`~icalendar.cal.component.Component.LAST_MODIFIED` docstring to a clearer Pythonic docstring. Assisting tool: Claude Haiku 4.5 @valemm13 (`Issue #1244 `_) - Tidied the auto-generated property docstrings by removing the redundant accepted values sentence and formatting return and error details as Sphinx sections. @mvanhorn (`Issue #1259 `_) - Fix Read the Docs administrative instructions in maintenance documentation by removing the obsolete step to check the default branch and adding a step to manually trigger the "latest" version. @stevepiercy (`Issue #1397 `_) - Clarified that :meth:`Calendar.new ` automatically sets not only required, but recommended properties too. @scop (`Issue #1459 `_) - Add how to report security issues. @stevepiercy (`Issue #1463 `_) - Added NixOS installation instructions to the documentation. @MichaelFehdrau0205 - Added instructions to :ref:`change-log` in Contribute documentation for how to avoid a filename conflict when adding a change log entry. @stevepiercy - Collect only documentation files for doctest. @stevepiercy - Documented :meth:`Calendar.from_ical ` as a direct :class:`~icalendar.cal.calendar.Calendar` API member so Sphinx can link to it from the custom components guide. Prepared with AI assistance. @xjn2005 Internal changes ~~~~~~~~~~~~~~~~ - Added tests for :attr:`FreeBusy.duration ` coverage. @kyriakikrimitza (`Issue #698 `_) - Refactored :file:`test_error_tolerant_parsing.py` to use ``.ics`` fixture files. @lcampanella98 (`Issue #1081 `_) - Added :file:`.pre-commit-config.yaml` configuration and documentation to automatically record AI prompts in commit messages with :program:`ai-prompt-auto-commit`. @AniruthKarthik (`Issue #1320 `_) - Support keeping a linked issue open in the pull request template when the pull request fixes only a part of the original issue. @stevepiercy (`Issue #1394 `_) - Trigger the creation of a comment with links to Read the Docs of the changed files to review in a pull request preview build. @stevepiercy (`Issue #1396 `_) - Add gh-profiler workflow to help triage AI-generated contributions. @SashankBhamidi (`Issue #1484 `_) - Added validation test for :attr:`FreeBusy.duration `. @kyriakikrimitza - Remove redundant and deprecated pull request preview feature on Read the Docs. We now use the RTD GitHub app, making this file redundant to pull request preview builds. Additionally the call to the old GitHub Action was deprecated on 2025-07-01, per its `README.md `_. @stevepiercy Other tasks ~~~~~~~~~~~ - Added type hints to all methods in the module :mod:`~icalendar.error`. @Priyanshu-pulak (`Issue #938 `_) 7.1.3 (2026-06-15) ------------------ Bug fixes ~~~~~~~~~ - Comparing components with ``Component.__eq__`` is no longer exponential in the subcomponent nesting depth, removing a denial-of-service vector where a deeply nested component could take minutes to compare. See `GHSA-cv84-9p8j-fj68 `_. @tidusec 7.1.2 (2026-05-22) ------------------ Bug fixes ~~~~~~~~~ - Replaced the recursive :meth:`Component.__repr__ ` implementation with an iterative stack-based walk so that deeply nested calendars no longer raise :exc:`RecursionError` when formatted via ``repr()``, ``str()``, or f-strings. The output format is unchanged for normally-shaped calendars. @gistrec (`Issue #1370 `_) Documentation ~~~~~~~~~~~~~ - Update maintenance documentation. Fix the version switcher on "stable" on Read the Docs. @stevepiercy (`Issue #1352 `_) 7.1.1 (2026-05-18) ------------------ New features ~~~~~~~~~~~~ - Created an :attr:`~icalendar.prop.dt.period.vPeriod.ical_value` property for the :class:`~icalendar.prop.dt.period.vPeriod` component. @ZairKSM (`Issue #876 `_) - Created a :meth:`~icalendar.prop.recur.weekday.vWeekday.ical_value` property for the :class:`~icalendar.prop.recur.weekday.vWeekday` component, mirroring the existing pattern on :class:`~icalendar.prop.boolean.vBoolean`. @mvanhorn (`Issue #1360 `_) Bug fixes ~~~~~~~~~ - Strictly validate BINARY property values in :attr:`vBinary.from_ical() ` and reject malformed Base64 input instead of silently accepting invalid characters. @uwezkhan (`Issue #1349 `_) Documentation ~~~~~~~~~~~~~ - Replace the RFC quotations in the docstrings for :attr:`Alarm.REPEAT ` and :attr:`Alarm.DURATION ` with Pythonic descriptions, including parameter notes, conformance references, and worked examples. @tmchow (`Issue #1244 `_) - Edited contributor documentation for how to add a change log entry, and maintenance documentation for how to process news fragments. @stevepiercy (`Issue #1256 `_) - Updated release process documentation. @niccokunzmann @stevepiercy @SashankBhamidi (`Issue #1293 `_) Dependency changes ~~~~~~~~~~~~~~~~~~ - Added `towncrier `_ to development dependencies. @stevepiercy (`Issue #1256 `_) Internal changes ~~~~~~~~~~~~~~~~ - Switched from manual change log management to `towncrier `_ to automate the process. @stevepiercy (`Issue #1256 `_) - Bump PyPy from 3.10 to 3.11 for testing. @stevepiercy (`Issue #1383 `_) 7.1.0 (2026-04-30) ------------------ Minor changes ~~~~~~~~~~~~~ - Deprecate ``icalendar.parser.escape_string`` and ``icalendar.parser.unescape_string`` for icalendar version 8. Use ``_escape_string`` and ``_unescape_string`` internally. :issue:`1011` - Added behavioral tests for :class:`~icalendar.cal.lazy.LazyCalendar` covering serialization round-trips, ``.todos``, ``.journals``, forward timezone references, and ``with_uid()`` substring false-positives. :issue:`1050` - Added edge case tests for :class:`~icalendar.prop.conference.Conference` parameter normalization covering string passthrough, empty list filtering, and ``None`` omission. :issue:`925` - Make icalendar an explicit editable install for clarity. :pr:`1268` - Do not run some tests until a pull request is approved. :pr:`1246` - Mark skipped CI tasks as skipped instead of running them. :issue:`1286` - Created an :meth:`~icalendar.prop.boolean.vBoolean.ical_value` property for the :class:`~icalendar.prop.boolean.vBoolean` component. :issue:`876` - Created an :meth:`~icalendar.prop.float.vFloat.ical_value` property for the :class:`~icalendar.prop.float.vFloat` component. :issue:`876` - Created an :meth:`~icalendar.prop.integer.vInt.ical_value` property for the :class:`~icalendar.prop.integer.vInt` component. :issue:`876` - Created an :meth:`~icalendar.prop.binary.vBinary.ical_value` property for the :class:`~icalendar.prop.binary.vBinary` component. :issue:`876` - Put the link check as the last documentation CI task, allowing the documentation build and Vale to run first and fail faster. :pr:`1295` - Extended :func:`~icalendar.timezone.tzp.TZP.localize` to support localizing both :class:`datetime.datetime` and :class:`datetime.time` objects, returning timezone-aware :class:`datetime.time` objects for the latter. :issue:`1142` - Add type hints to tests directory functions. :issue:`938` - Update to Contributor Covenant 3.0 Code of Conduct, hosted at https://pycal.org/code-of-conduct/. New features ~~~~~~~~~~~~ - Added :class:`~icalendar.cal.lazy.LazyCalendar` for lazy parsing of subcomponents. :issue:`158`, :issue:`1050` - Updated :func:`icalendar.prop.dt.time.vTime.from_ical` to support parsing time values with TZID parameters, returning timezone-aware :class:`datetime.time` objects. :issue:`1142` - Added ``subcomponents`` parameter to :meth:`Component.new `, :meth:`Event.new `, :meth:`Todo.new `, and :meth:`Availability.new `. :issue:`1065` - Switch to uv for development. :issue:`1102` Bug fixes ~~~~~~~~~ - Allow lenient parsing of content lines with optional whitespace around property and parameter delimiters (for example, ``REFRESH - INTERVAL; VALUE = DURATION:PT48H``) when parsing calendars with ``strict=False``. :issue:`351` - X-properties with a ``VALUE`` parameter are now parsed using the correct type instead of falling back to :class:`~icalendar.prop.unkown.vUnknown`. :issue:`1238` - Test that the ``DURATION`` property catches :class:`datetime.timedelta` objects without vProperty wrappers. :issue:`884` - Fixed :func:`~icalendar.attr.get_end_property` to avoid allowing the creating of VEVENT components with negative durations. Only VTODO components are allowed to have negative durations. :issue:`999` - GitHub Actions: conditional tests now show as "skipped" instead of "pending". :issue:`1264` - Fixed :meth:`Component.__eq__ ` method not being commutative when comparing subcomponents. :issue:`1224` - Verified that the ``VALUE`` parameter of jCal components is used for the type of the component property. :issue:`1237` - Fix :func:`~icalendar.parser.string.escape_char` handling of ``bytes`` input by converting with :func:`icalendar.parser_tools.to_unicode` before escaping. :issue:`1226` - Fixed ``RecursionError`` in ``walk()``, ``property_items()``, and ``to_ical()`` by using iterative implementations for component traversal and property extraction. :pr:`1348` Documentation ~~~~~~~~~~~~~ - Convert docstring of :func:`~icalendar.param.string_parameter` to Google style. :issue:`1072`, :pr:`1316` - Run ``sphinx-build`` with ``-W`` to turn warnings into errors. :issue:`1306` - Added `sphinx-llms-txt `_ extension to generate :file:`llms.txt` and :file:`llms-full.txt` files for AI/LLM documentation consumption. :issue:`1302` - Fixed CI Vale check reporting and resolved Vale errors. :issue:`1277` - Include :file:`Makefile` in documentation workflow path filters so documentation CI runs when Makefile logic changes, and keep Vale failures visible in CI output. :issue:`1277` - Document how to install icalendar on Alpine Linux. :pr:`1290` - Add documentation for usage of the Sphinx extension `sphinx-icalendar `_. :pr:`1268` - Add Repology badge and distribution installation instructions to install documentation. :issue:`1119` - Updated references to :class:`~icalendar.prop.uri.vUri` and :class:`~icalendar.enums.RELTYPE` classes in :file:`attr.py`. :issue:`1158` - Convert docstrings in :file:`attr.py` and :file:`cal/calendar.py` to Google Style format. :issue:`1072` - Explained import shortcuts in :doc:`../explanation/api-design` documentation. :issue:`1161` - Added tutorial for creating a calendar with events with attendees. :pr:`1262` - Added recognition of NLnet Foundation for its funding and Open Collective for donations to the documentation footer. :issue:`1214` - Documented ``vText`` properties according to :rfc:`5545#section-3.3.11`. :issue:`742` - Convert docstrings in :mod:`icalendar.caselessdict` to Google Style format with ``Parameters``, ``Returns``, ``Raises``, and ``Example`` sections as appropriate. :issue:`1072` - Fix broken docstring for :meth:`~icalendar.cal.component.Component.add_component` link in :attr:`~icalendar.cal.availability.Availability.available`. :issue:`1158` - Revise AI policy, per NLnet advice. See discussion under `pycal.org issue #24 `_. - Add documentation for how to use uv for development. :issue:`1102` - Reorganize Design documentation. :issue:`1292` - Improve docstring for ``categories_property``, which renders to HTML in :attr:`Calendar.categories `, :attr:`Event.categories `, :attr:`Journal.categories `, and :attr:`Todo.categories `. :issue:`1244` - Fixed Python object cross-references in ``icalendar.cal.component.Component._infer_value_type``. :issue:`1158`, :pr:`1344` 7.0.3 (2026-03-03) ------------------ Minor changes ~~~~~~~~~~~~~ - Show colorful required code changes in the CI output to help contributors solve the formatting issues. :pr:`1216` - Use ruff 0.15.0 for code formatting in :file:`tox.ini`. :pr:`1215` New features ~~~~~~~~~~~~ - Added :attr:`Event.RECURRENCE_ID `, :attr:`Todo.RECURRENCE_ID `, and :attr:`Journal.RECURRENCE_ID ` properties, including support in their ``new()`` constructors. :issue:`1231` Bug fixes ~~~~~~~~~ - Fixed :func:`~icalendar.timezone.tzid.tzids_from_tzinfo` not recognizing ``dateutil.tz.win.tzwin`` objects on Windows. UTC datetimes using ``dateutil.tz.gettz("UTC")`` now correctly serialize with the ``Z`` suffix instead of ``TZID=Coordinated Universal Time``. :issue:`1056` - Fixed :meth:`Calendar.get_missing_tzids ` raising ``KeyError`` when a VTIMEZONE exists for a timezone not referenced by any event TZID, for example, when added by the ``x-wr-timezone`` conversion. :issue:`1124` - Fixed :meth:`Calendar.get_missing_tzids ` and :meth:`Calendar.add_missing_timezones ` generating a spurious ``VTIMEZONE`` for UTC. :rfc:`5545#section-3.2.19` requires UTC datetimes to use the ``Z`` suffix; no ``VTIMEZONE`` component is needed or permitted. :issue:`1124` - Fixed :meth:`Parameters.update_tzid_from ` incorrectly setting ``TZID=UTC`` on UTC datetimes. :rfc:`5545#section-3.2.19` requires UTC datetimes to use the ``Z`` suffix without a ``TZID`` parameter. :issue:`1124` - Renamed the public functions ``escape_char`` and ``unescape_char`` to implicit private methods ``_escape_char`` and ``_unescape_char`` Fixed regression from :issue:`1008` by restoring :func:`~icalendar.parser.string.escape_char` and :func:`~icalendar.parser.string.unescape_char` as public functions. The public functions :func:`~icalendar.parser.string.escape_char` and :func:`~icalendar.parser.string.unescape_char` are now deprecated with warnings for external users. :pr:`1241` Documentation ~~~~~~~~~~~~~ - Add ``icalendar`` as a local workspace dependency to the ``docs`` dependency group in :file:`pyproject.toml` to fix dependency resolution issues with ``uv lock`` during documentation builds. :issue:`1253` :pr:`1254` - Use ``sphinx-icalendar`` to add syntax highlighting to iCalendar text 7.0.2 (2026-02-24) ------------------ Minor changes ~~~~~~~~~~~~~ - Show required code changes in the CI output to help contributors solve the formatting issues. :pr:`1216` - Use ruff 0.15.0 for code formatting in :file:`tox.ini`. :pr:`1215` New features ~~~~~~~~~~~~ - Enabled :meth:`Calendar.from_ical ` to read calendars from files. :issue:`756` - Added :attr:`Calendar.journals ` property to retrieve all journal components. :issue:`1230` Documentation ~~~~~~~~~~~~~ - Removed methods of ``str``, ``int``, and other classes and methods in the Python standard library from the documentation. 7.0.1 (2026-02-17) ------------------ Minor changes ~~~~~~~~~~~~~ - Setting :attr:`~cal.calendar.Calendar.calendar_name` now also writes ``X-WR-CALNAME``, and setting :attr:`~cal.calendar.Calendar.description` now also writes ``X-WR-CALDESC``, for improved client compatibility. :issue:`918` Bug fixes ~~~~~~~~~ - Make ``typing_extensions`` a dependency. :issue:`1202` Documentation ~~~~~~~~~~~~~ - Fixed the version switcher on Read the Docs, and documented the process for a major release. :issue:`1194` - Added usage examples for reading calendars from files and URL. :issue:`756` - Add type hints and convert docstrings to Google Style in :file:`cli.py`. :issue:`938` 7.0.0 (2026-02-11) ------------------ Minor changes ~~~~~~~~~~~~~ - Created an :meth:`~cal.todo.Todo.example` method for the :class:`~cal.todo.Todo` component. :issue:`743` - Created an :meth:`~cal.alarm.Alarm.example` method for the :class:`~cal.alarm.Alarm` component. :issue:`743` - Move property classes from :mod:`icalendar.prop` into their own files with sub-packages :mod:`icalendar.prop.recur` and :mod:`icalendar.prop.dt`. :issue:`987` - Move classes and functions in :mod:`icalendar.parser` into their own files. :issue:`987` - Clarified custom component (X-* and IANA-registered) parsing behavior through enhanced documentation and comprehensive how-to guide. Custom components are automatically handled by the library with no special configuration required. :issue:`432` - Reorganized custom component tests into a dedicated :file:`test_custom_components.py` file with expanded test coverage for :meth:`Component.from_ical `, :meth:`Calendar.from_ical `, and :class:`~icalendar.cal.component_factory.ComponentFactory` usage. :issue:`433` - The ``typing-extensions`` dependency on Python < 3.13 is now optional, part of the ``test`` extra. - The :func:`icalendar.tools.is_pytz_dt` return value is now hinted as ``TypeGuard[datetime]``, not ``TypeIs[datetime]``, since returning ``False`` should not allow narrowing it as non-datetime. - Regroup dependencies in, and remove obsolete ones, from :file:`pyproject.toml`. :issue:`906` - Add type hints to internal helper functions. :issue:`938` - Add type hints to prop value classes (vBoolean, vFloat, vUri, vBinary, vInline). :issue:`938` - Add type hints to remaining prop value classes (vText, vCalAddress, vCategory, vGeo, vN, vOrg, vAdr, vBroken, vUid, Conference, Image). :issue:`938` - Added type hints and overloads to :meth:`Calendar.from_ical ` and :meth:`Component.from_ical ` to support ``multiple=True/False`` return types. :issue:`1129` - CI: Print a link to Vale documentation when the spell checker fails. - Remove :file:`bootstrap.py` and :file:`buildout.cfg` files as they are old build configurations. :pr:`1171` - Enforce ruff formatting and linting across the entire codebase, with CI check to prevent regressions. :issue:`672`, :pr:`1171`, :pr:`1172`, :pr:`1173`, :pr:`1174`, :pr:`1175`, :pr:`1176`, :pr:`1177`, :pr:`1178`, :pr:`1179`, :pr:`1180`, and :pr:`1181` - Fix type annotations, typos, and validation logic in prop module: corrected return type hints in ``parse_jcal_value`` methods, fixed ``to_ical()`` return type in vDDDTypes, updated ClassVar type hint in TypesFactory, removed dead code, fixed "abbrevation" typo in vWeekday, and corrected validation logic in vMonth. :issue:`1185` - Rename :class:`~icalendar.prop.vBrokenProperty` to :class:`~icalendar.prop.vBroken` to match naming convention. :class:`~icalendar.prop.vBroken` now stores the actual exception object in ``parse_error`` instead of a string, and raises :class:`~icalendar.error.BrokenCalendarProperty` when accessing attributes like ``.dt`` that the expected type would have. :issue:`1087` Breaking changes ~~~~~~~~~~~~~~~~ - Drop support for Python 3.8 and 3.9. :issue:`977` - ``DURATION_REGEX`` moved from :mod:`icalendar.prop` to :mod:`icalendar.prop.dt.duration`. :issue:`987` - ``WEEKDAY_RULE`` moved from :mod:`icalendar.prop` to :mod:`icalendar.prop.recur.weekday`. :issue:`987` - Removed ``tzid_from_dt`` and ``tzid_from_tzinfo`` from :mod:`icalendar.prop` as they exist in :mod:`icalendar.timezone`. :issue:`987` - :meth:`Component.decoded` now returns a string instead of bytes for text properties. - Remove constants ``FOLD``, ``NAME``, ``NEWLINE``, ``QUNSAFE_CHAR``, ``QUOTABLE``, ``UFOLD``, and ``UNSAFE_CHAR`` from :mod:`icalendar.parser`'s export. :issue:`987` New features ~~~~~~~~~~~~ - Added ``recursive`` parameter to :meth:`Component.copy` to control copying of subcomponents and properties. :issue:`899` - Event components now have error-tolerant property parsing. Properties with parsing errors fall back to :class:`~icalendar.prop.vBroken`, preserving the raw value and allowing access to other valid properties. Errors are recorded in ``component.errors``. Partially addresses :issue:`158` - Added :class:`~icalendar.prop.AdrFields` and :class:`~icalendar.prop.NFields` named tuples for structured access to vCard ADR and N property fields. The ``fields`` attribute and ``from_ical()`` return value of :class:`~icalendar.prop.vAdr` and :class:`~icalendar.prop.vN` now return these typed named tuples, enabling access like ``adr.fields.street`` and ``n.fields.family``. Since named tuples are tuple subclasses, existing code using tuple indexing or unpacking remains compatible. Added ``name`` and ``units`` properties to :class:`~icalendar.prop.vOrg` for convenient access to the organization name and organizational units. Added ``ical_value`` property to all three classes. :issue:`1060` - Added ``with_uid`` method to ``Component`` to filter subcomponents by UID. :issue:`950` - Enforce linting of code with Ruff in CI. :issue:`672` Bug fixes ~~~~~~~~~ - Fixed :meth:`Calendar.new ` to automatically generate a UID when not provided, matching the documented behavior. Previously, the documentation stated that ``uid`` would be set to a new :func:`uuid.uuid4` if ``None``, but the implementation did not generate it. :issue:`1066` - Fixed import failure in Pyodide/WebAssembly environments by using lazy initialization for timezone data in the zoneinfo provider. The library can now be imported in environments without timezone data (e.g., Cloudflare Workers, PyScript, JupyterLite). :issue:`1073` - Fixed :meth:`icalendar.caselessdict.CaselessDict.__eq__` to return ``NotImplemented`` when comparing with non-dict types instead of raising ``AttributeError``. :issue:`1016` - Fixed decoding of categories. :issue:`279` - Link ``timedelta`` to :py:class:`datetime.timedelta` in the Python standard library documentation. :issue:`951` - Fix round-trip parsing of :class:`~icalendar.prop.vCategory` (CATEGORIES property) when category values contain commas. Categories like ``'Meeting, John'`` now correctly survive round trips between :meth:`Component.to_ical ` and :meth:`Component.from_ical ` instead of being split into multiple categories. Added :func:`~icalendar.parser.split_on_unescaped_comma` helper function. :issue:`127` - Fixed semicolon escaping in vCard structured properties (ADR, N, ORG). Semicolons are now correctly treated as field separators per :rfc:`6350`, not escaped as in iCalendar TEXT values. Added :func:`~icalendar.parser.split_on_unescaped_semicolon` helper function and :class:`~icalendar.prop.vAdr`, :class:`~icalendar.prop.vN`, :class:`~icalendar.prop.vOrg` property types. :issue:`137` - Fix :meth:`Image.from_property_value ` to raise ``TypeError`` instead of ``AttributeError`` when ``value.params`` isn't valid (most notably, isn't dict-like). :issue:`909` Documentation ~~~~~~~~~~~~~ - Improved docstrings and formatting for the :class:`~icalendar.alarms.AlarmTime` class. :issue:`1072` - Added how-to guide for handling parsing errors with :class:`~icalendar.prop.vBroken` and the ``component.errors`` attribute. :issue:`1085` - Updated 11 function docstrings in :mod:`icalendar.parser` to follow the Google Style guide, improving API documentation clarity and consistency. :issue:`1072` - Applied Google-style docstrings to :mod:`icalendar.tools` utility functions with Args, Returns, and Example sections. :issue:`1072` - Simplify contributors and add supporters in credits. :issue:`1035` - Add a section in the change log for Documentation. :issue:`1043` - Fixed multiple ``more than one target found for cross-reference`` warnings, and stopped using ``sphinx.ext.autosectionlabel``, in documentation. :issue:`952` - Add ``funding.json`` manifest for funding information. :issue:`1047` - Resolved ``Cannot resolve forward reference in type annotations`` warning in documentation. Added ``SPHINX_APIDOC_OPTIONS`` to ``make apidoc`` command, excluding ``__all__`` items from being duplicated in the documentation, and rebuilt the API documentation source files. :issue:`952` - Document how to create and read attendee information in events. :issue:`130` - Add usage examples. :issue:`443` - Improve documentation contribution guide by adding chapters for small edits, builds and checks, and a style guide. Added details for Vale usage, Diátaxis framework, narrative and API documentation, and fixing all spelling errors. :issue:`991` - Document dictionary and property accessors. :issue:`124` - Moved content from the README into documentation to reduce maintenance and point to the authoritative source of information. :issue:`1006` - Added "Code conventions" section to the Development contributing guide. :issue:`1004` - Use Google style docstrings in :mod:`~icalendar.parser_tools` :issue:`1017` - Document compatibility of icalendar with RFCs. :issue:`1147` - Added Upgrade guide. :issue:`997` - Fixed links in jCal usage documentation. - Enable sphinx-issues extension. :issue:`1091` - Replaced "Arguments" and "Args" with "Parameters". :issue:`1076` - Added documentation of how to set up a development environment with git. :issue:`906` - Documented how to install and use pre-commit. :issue:`996` - Added a new rule to our docstring style guide to escape docstrings, and added a new section for type hints in the code conventions section. :issue:`1080` - Documented ``__init__`` methods. :issue:`1079` - Moved "Edit this page" link to above the page table of contents. :issue:`1106` - Added Good First Issue link and fixed Up For Grabs link in Contribute guide. :issue:`1095` - Updated ``CONTRIBUTING.rst`` to explicitly state not to use Args sections in docstrings. :issue:`1076` - Document ``vInt.__new__`` method parameters in class docstring with :rfc:`5545` examples. :issue:`1118` - Improved ``make vale`` command to provide guidance on failure or success. :issue:`1137` - Add ``sphinx_copybutton`` configuration to exclude line numbers, prompts, and console output when copying code blocks. - Change Sphinx configuration to convert ``--`` to en dash ``-``, ``---`` to em dash ``—``, and ``...`` to ellipsis ``…``, but doesn't transform quote marks as they should be preserved. This restores cleaner and consistent rendering of docstrings in the :doc:`API Reference <../reference/api/icalendar>`. See `smartquotes_action `_. - Fix incorrect return type annotation in :meth:`Component.from_ical `. :issue:`1141` - Fixed broken links in ``docs/how-to/usage.rst`` documentation. Part of :issue:`1158` - Enhanced the first-time contributors section, in hopes that fewer contributors ask permission before starting work. :issue:`1165` - Changed Sphinx configuration to inherit from ``CaselessDict``, displaying all of its class members in the in-page navigation. Fixed broken cross-component links in ``Availability.new()``. :issue:`994` - Configured Sphinx to append the docstring from the ``__init__`` method into its class docstring. :issue:`1156` 7.0.0a3 (2025-12-19) -------------------- Minor changes ~~~~~~~~~~~~~ - Add static ``[project.urls]`` to ``pyproject.toml`` for GitHub dependency graph compatibility. :issue:`1035` - Created an :meth:`~cal.todo.Todo.example` method for the :class:`~cal.todo.Todo` component. :issue:`743` - Add type hints to :mod:`icalendar.caselessdict` file. :issue:`938` - For releases, added a step to update the version switcher for documentation. New features ~~~~~~~~~~~~ - Added contributor list format and sorting instructions. Bug fixes ~~~~~~~~~ - Fix double-unescaping in :meth:`vText.from_ical` and :meth:`vCategory.from_ical` by removing ``unescape_char()``. :issue:`1008` 7.0.0a2 (2025-11-29) -------------------- Minor changes ~~~~~~~~~~~~~ - Convert changelog section labels to reStructuredText subheadings for improved Read the Docs navigation. :issue:`982` - Move sections in Table of Content of Reference guide. - Improve :py:class:`icalendar.prop.vDatetime` documentation. :issue:`946` - CLI now writes output files using UTF-8 explicitly instead of relying on the system default. :issue:`1005` New features ~~~~~~~~~~~~ - Add compatibility to :rfc:`9253`: - Add new property types :class:`vUid` and :class:`vXmlReference` - Add properties to all components: :attr:`Component.concepts`, :attr:`Component.links`, :attr:`Component.refids`, :attr:`Component.related_to` - Add new values to :class:`RELTYPE` - Add ``LABEL``, ``GAP``, and ``VALUE`` parameters to property values affected by :rfc:`9253` - Allow setting default values in :class:`Parameters` - Allow ``None`` as an argument to :class:`Parameters` - Add ``ical_value`` to several property values. :issue:`876` - Add PyData Theme version switcher to documentation. :issue:`825` - Test compatibility with Python 3.14. :issue:`973` - Added documentation of Vale usage for icalender documentation style guide. :issue:`815` - Added funding link to :file:`pyproject.toml`. :issue:`800` - Added documentation of docstring format. :issue:`747` - Add ``TypeAlias`` ``icalendar.VPROPERTY`` as a type for all properties. - Allow access to :class:`icalendar.TypesFactory` via :func:`icalendar.TypesFactory.instance` - Classes in :mod:`icalendar.prop` can all be initialized with parameters now. - Allow passing :class:`icalendar.vDDDTypes` as arguments to :class:`icalendar.vDDDLists` - Add support for :class:`datetime.timezone` identification. - Add :func:`icalendar.is_utc` to check if a timezone is UTC. - Add support for JCal parsing and serialization. :issue:`978` - Add :class:`icalendar.JCalParsingError` exception for jCal parsing. - Add :attr:`icalendar.Parameters.tzid` and :func:`icalendar.Parameters.is_utc` for timezone handling. - Add :class:`icalendar.vUnknown` which is the default now for unknown properties and the same as :class:`icalendar.vText` - Add :func:`icalendar.ComponentFactory.get_component_class` that also creates a new component class when it does not exist. - Add :func:`icalendar.ComponentFactory.add_component_class` to have a unified interface to add component classes. - Add :func:`icalendar.Component.to_json`, :func:`icalendar.Component.to_jcal`, and :func:`icalendar.Component.from_jcal` methods so all components can be serialized and parsed. - Classes in :mod:`icalendar.prop` receive the following updates: - Add ``to_jcal`` and ``from_jcal`` for serialization. - Add ``default_value`` class attribute for the default ``VALUE`` parameter. - Add ``VALUE`` parameter property. - Add ``examples()`` classmethod that returns examples for testing. - Added documentation of how to fuzz icalendar. :issue:`905` - Added first-time contributors section to Contributing. :issue:`934` - Revised check of which files trigger a documentation build on Read the Docs. :issue:`848` - Created a separate path to contribute to documentation. :issue:`929` - Added a code of conduct. :issue:`907` - Added redirects of moved files and 404 not found page to catch any other obsolete URLs. :issue:`959` - Added artificial intelligence (AI) policy. :issue:`933` Bug fixes ~~~~~~~~~ - Correct ``name`` of ``AVAILABLE`` component. - Fix ical serialization of :class:`icalendar.vTime` - Make sure a component's ``conferences`` property adds ``VALUE=URI`` if the conference is created as a URI. - Fix CLI deprecated :py:class:`argparse.FileType` usage and remove ``PendingDeprecationWarning``. :issue:`1005`. 6.3.2 (2025-11-03) ------------------ Minor changes ~~~~~~~~~~~~~ - Add PyData Theme version switcher to documentation. See :issue:`825`. 5.0.14 (2025-11-03) ------------------- Minor changes ~~~~~~~~~~~~~ - Add PyData Theme version switcher to documentation. See :issue:`825`. 4.1.2 (2025-11-03) ------------------ Minor changes ~~~~~~~~~~~~~ - Add PyData Theme version switcher to documentation. See :issue:`825`. 7.0.0a1 (28-10-2025) -------------------- Minor changes ~~~~~~~~~~~~~ - Split up ``cal.py`` into different files as sub-package. - Format more source code with ruff. - Exclude type checking block from test coverage. - Add private ``icalendar.compatibility`` module to merge functionality for older Python versions into one place. - Add type annotation to ``from_ical()``. - Fix enum documentation. - ``DTSTAMP``, ``LAST_MODIFIED``, and ``CREATED`` can now be set to ``None`` to delete the value. - Enhanced ``Calendar.new()`` to support organization and language parameters for automatic ``PRODID`` generation. - Added ``duration`` setter to ``Event`` class for more intuitive event creation. - Added ``validate()`` method to ``Calendar`` class for explicit validation of required properties and components. - Add improved setters for ``start``, ``duration``, and ``end`` properties with explicit locking mechanisms to provide more flexible property manipulation while maintaining RFC 5545 compliance. The implementation includes comprehensive test coverage to ensure proper behavior and backward compatibility. - Add ``new()`` method to ``vCalAddress`` class for consistent API usage. The method supports all RFC 5545 parameters including ``CN``, ``CUTYPE``, ``DELEGATED-FROM``, ``DELEGATED-TO``, ``DIR``, ``LANGUAGE``, ``PARTSTAT``, ``ROLE``, ``RSVP``, and ``SENT-BY``, with automatic ``mailto:`` prefix handling. See :issue:`870`. - Refactor ``set_duration`` methods in ``Event`` and ``Todo`` classes to eliminate code duplication by extracting common logic into shared ``set_duration_with_locking()`` function in ``icalendar.attr``. See :issue:`886`. - Consolidate duplicate logic patterns between ``Event`` and ``Todo`` classes by extracting shared functions in ``icalendar.attr`` for property setters, validation logic, and property access. This eliminates approximately 150 lines of duplicate code while maintaining 100% backward compatibility and RFC 5545 compliance. See :issue:`891`. - Accept and ignore non-standard empty ``RDATE`` fields when parsing ICS files. - Improve contributing documentation by adding a change log requirement, adding a pull request template, adding clear CI enforcement warnings, and updating ``README.rst``. See :issue:`890`. - Make coverage submission optional for CI. - Bump ``actions/setup-python`` version from 5 to 6 for CI. - Add comment explaining tzdata dependency purpose to prevent confusion. See :issue:`900`. - Fix duplicate blank issue template by adding config.yml to disable GitHub's default blank option. See :issue:`777`. - Add PEP 561 py.typed marker to enable type checking support. The package now distributes inline type annotations for mypy and other type checkers. See :issue:`395`. - Bump ``github/codeql-action`` from 3 to 4 in CI fuzzing workflow. - Add docstring for ``vDatetime``. See :issue:`742`. - Update LICENSE file heading formatting. - Document ``icalendar.version`` module. - Use ``CONTRIBUTING.md`` in favour of ``.rst`` file. - Add type hints to ``icalendar.tools`` module. - Fix more examples link failed in ``README.md`` by renewing its link. See :issue:`958`. - Fix Contributing, Changelog, License links failed in ``README.md`` by renewing its link. See :issue:`959`. - Fix more examples link in ``README.md``. See :issue:`958`. - Add ``CODEOWNERS`` file for automatic pull request review assignment. See :issue:`945`. Breaking changes ~~~~~~~~~~~~~~~~ - Correctly throw a ``TypeError`` for wrong types during property creation where a ``ValueError`` was thrown before. - Move ``types_factory`` into ``Component.types_factory`` - Move ``components_factory`` into ``Component.get_component_class`` - Move ``icalendar.cal.IncompleteComponent`` error into ``icalendar.error``. - Remove ``icalendar.UIDGenerator``. Use Python's built-in `uuid library `_ instead. New features ~~~~~~~~~~~~ - Add ``new()`` to ``icalendar.Calendar`` to set required attributes. See :pr:`569`. - Add ``new()`` to ``Alarm``, ``Event``, ``Todo``, ``FreeBusy``, ``Component``, and ``Journal`` components. See :issue:`843`. - Add ``value`` to ``Parameters`` to access the ``VALUE`` parameter. - Add ``Availability`` and ``Available`` components from :rfc:`7953`. See :issue:`654` and :issue:`864`. - Add ``stamp``, ``last_modified``, ``created``, ``CREATED``, ``busy_type``, ``class``, ``comments``, ``contacts``, ``location``, ``organizer``, ``priority``, and ``url`` properties to components that use them. - Add ``availabilities`` attribtue to ``Calendar``. - Add ``status``, ``transparency``, and ``attendees`` properties. See :issue:`841`. - Add ``uid`` property that is ``''`` by default and set automatically with ``new()``. See :issue:`315`. - Make icalendar compatible with `RFC 7986 `_. - Add ``url``, ``source``, ``refresh_interval``, ``conferences``, and ``images`` properties to components. - Add ``td`` property to ``vDDDTypes`` to make it compatible with ``timedelta`` value types. - Add documentation for how to install icalendar on Fedora. See :issue:`704`. - Add documentation linkchecker to make and CI configuration. See :issue:`854`. - Add support for Vale as spell and style checker. - For ``make livehtml`` watch all files in ``src/icalendar``, but ignore ``.typed`` and contents of the ``tests`` and ``fuzzing`` directories. See :issue:`931`. - Restructure documentation, following Diátaxis framework. See :issue:`820` which included the following tasks. - Start building API Reference documentation. - Add Vale with Microsoft Writing Guide style checker. - Add :file:`Makefile` to automate all documentation builds with a single command for each. - Write documentation of how to build documentation. - Move "History" into "Credits". - Add a Glossary of terms. - Add version module to API reference. - Exclude private Python methods and test modules from the API reference. - Fix display of credits to use reStructuredText. - Fix the documentation version display. - Show older versions of documentation on Read the Docs. - Fix the PyPI icon. - Add a Security Policy. - Consolidate Contributing files into the how-to Contributing guide. - Move the change log into the Reference guide. - Remove all rights reserved from the License file and remove from documentation, leaving it in the repository root. - Move Maintenance into the Contribute guide. - Move icalendar contributors into the Contribute guide. - Merge about the project into Credits. - Split :file:`api.rst` into separate modules. - Move "icalendar utility" into a how-to guide for command line interface. - Move "icalendar package" into a how-to usage guide and API reference guide. - Split installation into how-to guides for usage and contributing. - Redesigned home page to reflect the new structure and point readers to the proper location. - Fix various Sphinx build warnings and errors. - Use text for lexer of ical files. - Update :file:`README.rst` to align with new documentation structure. - Add documentation builds to continuous integration. - Review pull requests that involve source docstrings, continuous integration, or documentation, and provide suggestions and corrections. - Add spell checking to documentation. See :issue:`853`. - Add link check to docs test. See :issue:`854`. - Document installation of icalendar on Fedora. See :issue:`704`. - Miscellaneous documentation follow-up fixes. See :issue:`919`. Bug fixes ~~~~~~~~~ - Fix ``Todo.end`` to return ``start`` for date-only ``DTSTART`` (not ``start + 1 day``). Events keep existing behavior. See :issue:`898`. - Fix ``AssertionError`` when converting ``VTIMEZONE`` components with only ``DAYLIGHT`` subcomponent (no ``STANDARD``) to pytz timezones. The code now handles cases where a timezone definition contains only DST transitions by calculating the DST offset from ``TZOFFSETFROM`` and ``TZOFFSETTO``. Test case contributed by @niccokunzmann in :pr:`379`. See :issue:`321`. - Normalize Conference parameter lists (FEATURE/LABEL/LANGUAGE) to comma-separated values. See :issue:`908`. - Fix read from stdin issue - See :issue:`821`. - Fix invalid calendar: Parsing a date with TZID results in a datetime to not loose the timezone. See :issue:`187`. - Fix timezone placement in ``add_missing_timezones()``: ``VTIMEZONE`` components now appear before ``VEVENT`` and other components that reference them. See :issue:`844`. - Fixed ``Todo.duration`` and ``Event.duration`` to return ``DURATION`` property when set, even without ``DTSTART``. See :issue:`867`. - Fix ``VALUE`` parameter handling: ``datetime.date`` objects now correctly set ``VALUE=DATE`` parameter when added to properties like ``EXDATE``, ``RDATE``, and ``DTSTART``. The ``VALUE`` parameter is also properly used when parsing iCalendar data. See :issue:`349`. - Fix URL-encoded characters being incorrectly unescaped during content line parsing. The parser now properly handles backslash escaping and double-quoted sections without corrupting URL-encoded values like ``%3A`` (colon) in DESCRIPTION fields. Added ``unescape_backslash()`` function to separate :rfc:`5545` backslash escaping from URL encoding. Optimized implementation using regex for single-pass processing. Added type hints to ``Contentline.parts()`` method and comprehensive unit tests. See :issue:`355`. - `make livehtml` now reloads with code changes. See :issue:`931`. - Added tests for tools, vFloat, vBinary, vGeo, and vTodo to improve unit test coverage. :issue:`698` 6.3.1 (2025-05-20) ------------------ Bug fixes ~~~~~~~~~ - Remove forced quoting from parameters with space and single quote. See :issue:`836`. 6.3.0 (2025-05-15) ------------------ Minor changes ~~~~~~~~~~~~~ - Deprecate ``icalendar.UIDGenerator``. See :issue:`816`. New features ~~~~~~~~~~~~ - Add the ``uid`` property to ``Alarm``, ``Event``, ``Calendar``, ``Todo``, and ``Journal`` components. See :issue:`740`. Bug fixes ~~~~~~~~~ - Fix component equality where timezones differ for the datetimes but the times are actually equal. See :issue:`828`. - Test that we can add an RRULE as a string. See :issue:`301`. - Test that we support dateutil timezones as outlined in :issue:`336`. - Build documentation on Read the Docs with the version identifier. See :issue:`826`. 6.2.0 (2025-05-07) ------------------ Minor changes ~~~~~~~~~~~~~ - Use ``ruff`` to format the source code. - Update project metadata to use License-Expression. - Use ``tzp.localize(dt, None)`` to remove the timezone from a datetime. - Remove the HTML documentation when building with ``tox`` to force rebuild. - Switch to PyData Sphinx Theme for documentation. See :pr:`803`. New features ~~~~~~~~~~~~ - Add getters ``rrules``, ``rdates``, and ``exdates`` for unified and simple access to these properties. See `Discussion 662`_. - Add attributes to the calendar for properties ``NAME``, ``DESCRIPTION``, and ``COLOR``. See :issue:`655`. - Add a ``color`` attribute to ``Event``, ``Todo``, and ``Journal`` components. See :issue:`655`. - Add ``sequence`` attribute to ``Event``, ``Todo``, and ``Journal`` components. See :issue:`802`. - Add ``categories`` attribute to ``Calendar``, ``Event``, ``Todo``, and ``Journal`` components. See :issue:`655`. - Add compatibility to :rfc:`6868`. See :issue:`652`. - Add ``freebusy`` property to the ``Calendar`` to get this type of subcomponents easier. - Add parameters from :rfc:`5545` to properties ``ALTREP``, ``CN``, ``CUTYPE``, ``DELEGATED_FROM``, ``DELEGATED_TO``, ``DIR``, ``FBTYPE``, ``LANGUAGE``, ``MEMBER``, ``PARTSTAT``, ``RANGE``, ``RELATED``, ``ROLE``, ``RSVP``, ``SENT_BY``, ``TZID``, and ``RELTYPE``. See :issue:`798`. - New properties from :rfc:`7986` can occur multiple times in ``VCALENDAR``. See :pr:`808`. Bug fixes ~~~~~~~~~ - Fix ``STANDARD`` and ``DAYLIGHT`` components that have a date as ``DTSTART``. See :issue:`218` - Move import at the end of ``icalendar.parser`` into a function to mitigate import errors, see :issue:`781`. - ``ALTREP``, ``DELEGATED-FROM``, ``DELEGATED-TO``, ``DIR``, ``MEMBER``, and ``SENT-BY`` require double quotes. These are now always added. - Classify ``CATEGORIES`` as multiple in ``VEVENT``. See :pr:`808`. 6.1.3 (2025-03-19) ------------------ Bug fixes ~~~~~~~~~ - Fix to permit TZID forward references to ``VTIMEZONE``\ s - Stabelize timezone id lookup, see :issue:`780`. 6.1.2 (2025-03-19) ------------------ Minor changes ~~~~~~~~~~~~~ - Add funding link to Tidelift. - Link to related package. - Shorten first example in documentation. - Add ``name`` and ``email`` properties to ``vCalAddress``. - Add type hint for property ``params`` in ``icalendar.prop``. - Set default value for ``params`` as ``params={}`` in mulitple constructors in ``icalendar.prop`` to improve usability. - Improve object initialization performance in ``icalendar.prop``. - Add type hint for ``params`` in multiple constructors in ``icalendar.prop``. Bug fixes ~~~~~~~~~ - Restrict timezones tested, see :issue:`763` 6.1.1 (2025-01-18) ------------------ Minor changes ~~~~~~~~~~~~~ - Add a ``weekday`` attribute to :class:`icalendar.prop.vWeekday` components. See :issue:`749`. - Document :class:`icalendar.prop.vRecur` property. See :issue:`758`. - Print failure of doctest to aid debugging. - Improve documentation of :class:`icalendar.prop.vGeo` - Fix tests, improve code readability, fix typing. See :pr:`766` and :pr:`765`. Breaking changes ~~~~~~~~~~~~~~~~ - The ``relative`` attribute of ``vWeekday`` components has the correct sign now. See :issue:`749`. New features ~~~~~~~~~~~~ - Add `Security Policy `_ - Python types in documentation now link to their documentation pages using ``intersphinx``. 6.1.0 (2024-11-22) ------------------ Minor changes ~~~~~~~~~~~~~ - Add ``end``, ``start``, ``duration``, ``DTSTART``, ``DUE``, and ``DURATION`` attributes to ``Todo`` components. See `Discussion 662`_. - Add ``DTSTART``, ``TZOFFSETTO`` and ``TZOFFSETFROM`` properties to ``TimezoneStandard`` and ``TimezoneDaylight``. See `Discussion 662`_. - Format test code with Ruff. See :issue:`672`. - Document the Debian package. See :issue:`701`. - Document ``vDatetime.from_ical`` - Allow passing a ``datetime.date`` to ``TZP.localize_utc`` and ``TZP.localize`` methods. - Document component classes with description from :rfc:`5545`. - Merge "File Structure" and "Overview" sections in the docs. See :issue:`626`. - Update code blocks in ``usage.rst`` with the correct lexer. - Tidy up the docstring for ``icalendar.prop.vPeriod``. - Improve typing and fix typing issues New features ~~~~~~~~~~~~ - Add ``VALARM`` properties for :rfc:`9074`. See :issue:`657` - Test compatibility with Python 3.13 - Add ``Timezone.from_tzinfo()`` and ``Timezone.from_tzid()`` to create a ``Timezone`` component from a ``datetime.tzinfo`` timezone. See :issue:`722`. - Add ``icalendar.prop.tzid_from_tzinfo``. - Add ``icalendar.alarms`` module to calculate alarm times. See :issue:`716`. - Add ``Event.alarms`` and ``Todo.alarms`` to access alarm calculation. - Add ``Component.DTSTAMP`` and ``Component.LAST_MODIFIED`` properties for datetime in UTC. - Add ``Component.is_thunderbird()`` to check if the component uses custom properties by Thunderbird. - Add ``X_MOZ_SNOOZE_TIME`` and ``X_MOZ_LASTACK`` properties to ``Event`` and ``Todo``. - Add ``Alarm.ACKNOWLEDGED``, ``Alarm.TRIGGER``, ``Alarm.REPEAT``, and ``Alarm.DURATION`` properties as well as ``Alarm.triggers`` to calculate alarm triggers. - Add ``__doc__`` string documentation for ``vDate``, ``vBoolean``, ``vCalAddress``, ``vDuration``, ``vFloat``, ``vGeo``, ``vInt``, ``vPeriod``, ``vTime``, ``vUTCOffset`` and ``vUri``. See :issue:`742`. - Add ``DTSTART``, ``TZOFFSETTO``, and ``TZOFFSETFROM`` to ``TimezoneStandard`` and ``TimezoneDaylight`` - Use ``example`` methods of components without arguments. - Add ``events``, ``timezones``, and ``todos`` property to ``Calendar`` for nicer access. - To calculate which timezones are in use and add them to the ``Calendar`` when needed these methods are added: ``get_used_tzids``, ``get_missing_tzids``, and ``add_missing_timezones()``. - Identify the TZID of more timezones from dateutil. - Identify totally unknown timezones using a UTC offset lookup tree generated in ``icalendar.timezone.equivalent_timezone_ids`` and stored in ``icalendar.timezone.equivalent_timezone_ids``. - Add ``icalendar.timezone.tzid`` to identify a timezone's TZID. Bug fixes ~~~~~~~~~ - Add ``icalendar.timezone`` to the documentation. 6.0.1 (2024-10-13) ------------------ New features ~~~~~~~~~~~~ - Added ``end``, ``start``, ``duration``, ``DTSTART``, ``DUE``, and ``DURATION`` attributes to ``Event`` components. See `Discussion 662`_. - Added ``end``, ``start``, ``duration``, and ``DTSTART`` attributes to ``Journal`` components. See `Discussion 662`_. Bug fixes ~~~~~~~~~ - Fix a few ``__all__`` variables. - Added missing ``docs`` folder to distribution packages. See :issue:`712`. .. _`Discussion 662`: https://github.com/collective/icalendar/discussions/662 6.0.0 (2024-09-28) ------------------ Minor changes ~~~~~~~~~~~~~ - Add ``__all__`` variable to each modules in ``icalendar`` package - Improve test coverage. - Adapt ``test_with_doctest.py`` to correctly run on Windows. - Measure branch coverage when running tests. - Export ``Component`` base class for typing New features ~~~~~~~~~~~~ - Use ``pyproject.toml`` file instead of ``setup.py`` Bug fixes ~~~~~~~~~ - Fix link to stable release of tox in documentation. - Fix a bad ``bytes`` replace in ``unescape_char``. - Handle ``ValueError`` in ``vBinary.from_ical``. - Ignore the BOM character in incorrectly encoded ics files. 6.0.0a0 (2024-07-03) -------------------- Minor changes ~~~~~~~~~~~~~ - Test that all code works with both ``pytz`` and ``zoneinfo``. - Add message to GitHub release, pointing to the changelog - Make coverage report submission optional for pull requests - Parallelize coverage - Rename ``master`` branch to ``main``, see `Issue `_ - Update ``docs/usage.rst`` to use zoneinfo instead of pytz. - Added missing public classes and functions to API documentation. - Improved namespace management in the ``icalendar`` directory. - Add Python version badge and badge for test coverage - Remove 4.x badge - Update list of ``tox`` environments - Use Coveralls' GitHub Action - Check distribution in CI Breaking changes ~~~~~~~~~~~~~~~~ - Use ``zoneinfo`` for ``icalendar`` objects created from strings, see :issue:`609`. This is an tested extension of the functionality, not a restriction: If you create ``icalendar`` objects with ``pytz`` timezones in your code, ``icalendar`` will continue to work in the same way. Your code is not affected. ``zoneinfo`` will be used for those **objects that** ``icalendar`` **creates itself**. This happens for example when parsing an ``.ics`` file, strings or bytes with ``from_ical()``. If you rely on ``icalendar`` providing timezones from ``pytz``, you can add one line to your code to get the behavior of versions below 6: .. code:: Python import icalendar icalendar.use_pytz() - Replaced ``pkg_resources.get_distribution`` with ``importlib.metadata`` in ``docs/conf.py`` to allow building docs on Python 3.12. - Remove ``is_broken`` property. Use ``errors`` instead to check if a component had suppressed parsing errors. See :issue:`424`. - Remove untested and broken ``LocalTimezone`` and ``FixedOffset`` tzinfo sub-classes, see :issue:`67` - Remove Python 3.7 as compatible. icalendar is compatible with Python versions 3.8 - 3.12, and PyPy3. - Remove ``pytz`` as a dependency of ``icalendar``. If you require ``pytz``, add it to your dependency list or install it additionally with:: pip install icalendar==6.* pytz New features ~~~~~~~~~~~~ - Check code quality with `Ruff `_, optional report - Test compatibility with Python 3.12 - Add function ``icalendar.use_pytz()``. - Allows selecting components with ``walk(select=func)`` where ``func`` takes a component and returns ``True`` or ``False``. - Add compatibility to :rfc:`7529`, adding ``vMonth`` and ``vSkip`` - Add ``sphinx-autobuild`` for ``livehtml`` Makefile target. - Add pull request preview on Read the Docs, building only on changes to documentation-related files. - Add link to pull request preview builds in the pull request description only when there are changes to documentation-related files. - Add documentation of live HTML preview of documentation and clean up of ``install.rst``. - Add ``sphinx-copybutton`` to allow copying code blocks with a single click of a button. Bug fixes ~~~~~~~~~ - Change documentation to represent compatibility with Python 3.8 - 3.12, and PyPy3. - Rename RFC 2445 to RFC 5545, see `Issue 278 `_ 5.0.13 (2024-06-20) ------------------- Minor changes ~~~~~~~~~~~~~ - Guide to delete the build folder before running tests - Add funding information - Make documentation build with Python 3.12 - Update windows to olson conversion for Greenland Standard Time - Extend examples in Usage with alarm and recurrence - Document how to serve the built documentation to view with the browser - Improve test coverage New features ~~~~~~~~~~~~ - Create GitHub releases for each tag. Bug fixes ~~~~~~~~~ - Parse calendars with X-COMMENT properties at the end the file by ignoring these properites 5.0.12 (2024-03-19) ------------------- Minor changes ~~~~~~~~~~~~~ - Analyse code coverage of test files - Added corpus to fuzzing directory - Added exclusion of fuzzing corpus in MANIFEST.in - Augmented fuzzer to optionally convert multiple calendars from a source string - Add script to convert OSS FUZZ test cases to Python/pytest test cases - Added additional exception handling of defined errors to fuzzer, to allow fuzzer to explore deeper - Added more instrumentation to fuzz-harness - Rename "contributor" to "collaborator" in documentation - Correct the outdated "icalendar view myfile.ics" command in documentation. #588 - Update GitHub Actions steps versions - Keep GitHub Actions up to date with GitHub's Dependabot Bug fixes ~~~~~~~~~ - Fixed index error in cal.py when attempting to pop from an empty stack - Fixed type error in prop.py when attempting to join strings into a byte-string - Caught Wrong Date Format in ical_fuzzer to resolve fuzzing coverage blocker 5.0.11 (2023-11-03) ------------------- Minor changes ~~~~~~~~~~~~~ - The cli utility now displays start and end datetimes in the user's local timezone. Ref: #561 [vimpostor] New features ~~~~~~~~~~~~ - Added fuzzing harnesses, for integration to OSSFuzz. - icalendar releases are deployed to Github releases Fixes: #563 [jacadzaca] Bug fixes ~~~~~~~~~ - CATEGORIES field now accepts a string as argument Ref: #322 [jacadzaca] - Multivalue FREEBUSY property is now parsed properly Ref: #27 [jacadzaca] - Compare equality and inequality of calendars more completely Ref: #570 - Use non legacy timezone name. Ref: #567 - Add some compare functions. Ref: #568 - Change OSS Fuzz build script to point to harnesses in fuzzing directory Ref: #574 5.0.10 (2023-09-26) ------------------- Bug fixes ~~~~~~~~~ - Component._encode stops ignoring parameters argument on native values, now merges them Fixes: #557 [zocker1999net] 5.0.9 (2023-09-24) ------------------ Bug fixes ~~~~~~~~~ - PERIOD values now set the timezone of their start and end. #556 5.0.8 (2023-09-18) ------------------ Minor changes ~~~~~~~~~~~~~ - Update build configuration to build readthedocs. #538 - No longer run the ``plone.app.event`` tests. - Add documentation on how to parse ``.ics`` files. #152 - Move pip caching into Python setup action. - Check that issue #165 can be closed. - Updated about.rst for issue #527 - Avoid ``vText.__repr__`` BytesWarning. Bug fixes ~~~~~~~~~ - Calendar components are now properly compared Ref: #550 Fixes: #526 [jacadzaca] 5.0.7 (2023-05-29) ------------------ Bug fixes ~~~~~~~~~ - to_ical() now accepts RRULE BYDAY values>=10 #518 5.0.6 (2023-05-26) ------------------ Minor changes ~~~~~~~~~~~~~ - Adjusted duration regex 5.0.5 (2023-04-13) ------------------ Minor changes ~~~~~~~~~~~~~ - Added support for BYWEEKDAY in vRecur ref: #268 Bug fixes ~~~~~~~~~ - Fix problem with ORGANIZER in FREE/BUSY #348 5.0.4 (2022-12-29) ------------------ Minor changes ~~~~~~~~~~~~~ - Improved documentation Ref: #503, #504 Bug fixes ~~~~~~~~~ - vBoolean can now be used as an parameter Ref: #501 Fixes: #500 [jacadzaca] 5.0.3 (2022-11-23) ------------------ New features ~~~~~~~~~~~~ - vDDDTypes is hashable #487 #492 [niccokunzmann] Bug fixes ~~~~~~~~~ - vDDDTypes' equality also checks the dt attribute #497 #492 [niccokunzmann] 5.0.2 (2022-11-03) ------------------ Minor changes ~~~~~~~~~~~~~ - Refactored cal.py, tools.py and completed remaining minimal refactoring in parser.py. Ref: #481 [pronoym99] - Calendar.from_ical no longer throws long errors Ref: #473 Fixes: #472 [jacadzaca] - Make datetime value shorter by removing the value parameter where possible. Fixes: #318 [jacadzaca], [niccokunzmann] New features ~~~~~~~~~~~~ - source code in documentation is tested using doctest #445 [niccokunzmann] Bug fixes ~~~~~~~~~ - broken properties are not added to the parent component Ref: #471 Fixes: #464 [jacadzaca] 5.0.1 (2022-10-22) ------------------ Minor changes ~~~~~~~~~~~~~ - fixed setuptools deprecation warnings [mgorny] Bug fixes ~~~~~~~~~ - a well-known timezone timezone prefixed with a `/` is treated as if the slash wasn't present Ref: #467 Fixes: #466 [jacadzaca] 5.0.0 (2022-10-17) ------------------ Minor changes ~~~~~~~~~~~~~ - removed deprecated test checks [tuergeist] - Fix: cli does not support DURATION #354 [mamico] - Add changelog and contributing to readthedocs documentation #428 [peleccom] - fixed small typos #323 [rohnsha0] - unittest to parametrized pytest refactoring [jacadzaca] Breaking changes ~~~~~~~~~~~~~~~~ - Require Python 3.7 as minimum Python version. [maurits] [niccokunzmann] - icalendar now takes a ics file directly as an input - icalendar's CLI utility program's output is different - Drop Support for Python 3.6. Versions 3.7 - 3.11 are supported and tested. New features ~~~~~~~~~~~~ - icalendar utility outputs a 'Duration' row - icalendar can take multiple ics files as an input Bug fixes ~~~~~~~~~ - Changed tools.UIDGenerator instance methods to static methods Ref: #345 [spralja] - proper handling of datetime objects with `tzinfo` generated through zoneinfo.ZoneInfo. Ref: #334 Fixes: #333 [tobixen] - Timestamps in UTC does not need tzid Ref: #338 Fixes: #335 [tobixen] - add ``__eq__`` to ``icalendar.prop.vDDDTypes`` #391 [jacadzaca] - Refactor deprecated unittest aliases for Python 3.11 compatibility #330 [tirkarthi] 5.0.0a1 (2022-07-11) -------------------- Breaking changes ~~~~~~~~~~~~~~~~ - Drop support for Python 3.4, 3.5 and PyPy2. [maurits] New features ~~~~~~~~~~~~ - Document development setup Ref: #358 [niccokunzmann] Bug fixes ~~~~~~~~~ - Test with GitHub Actions. [maurits] 4.1.0 (2022-07-11) ------------------ New features ~~~~~~~~~~~~ - No longer test on Python 3.4, 3.5 and PyPy2, because we cannot get it to work. Technically it should still work, it is just no longer tested. Do not expect much development on branch 4.x anymore. The main branch will be for the remaining Python versions that we support. [maurits] Bug fixes ~~~~~~~~~ - Test with GitHub Actions. [maurits] 4.0.9 (2021-10-16) ------------------ Bug fixes ~~~~~~~~~ - Fix vCategories for correct en/de coding. [thet] - vDuration property value: Fix changing duration sign after multiple ``to_ical`` calls. Ref: #320 Fixes: #319 [barlik] 4.0.8 (2021-10-07) ------------------ Bug fixes ~~~~~~~~~ - Support added for Python 3.9 and 3.10 (no code changes needed). - Replace bare 'except:' with 'except Exception:' (#281) 4.0.7 (2020-09-07) ------------------ Bug fixes ~~~~~~~~~ - fixed rrule handling, re-enabled test_create_america_new_york() 4.0.6 (2020-05-06) ------------------ Bug fixes ~~~~~~~~~ - Use ``vText`` as default type, when convert recurrence definition to ical string. [kam193] 4.0.5 (2020-03-21) ------------------ Bug fixes ~~~~~~~~~ - Fixed a docs issue related to building on Read the Docs [davidfischer] 4.0.4 (2019-11-25) ------------------ Bug fixes ~~~~~~~~~ - Reduce Hypothesis iterations to speed up testing, allowing PRs to pass [UniversalSuperBox] 4.0.3 (2018-10-10) ------------------ Bug fixes ~~~~~~~~~ - Categories are comma separated not 1 per line #265. [cleder] - mark test with mixed timezoneaware and naive datetimes as an expected failure. [cleder] 4.0.2 (2018-06-20) ------------------ Bug fixes ~~~~~~~~~ - Update all pypi.python.org URLs to pypi.org [jon.dufresne] 4.0.1 (2018-02-11) ------------------ - Added rudimentary command line interface. [jfjlaros] - Readme, setup and travis updates. [jdufresne, PabloCastellano] 4.0.0 (2017-11-08) ------------------ Breaking changes ~~~~~~~~~~~~~~~~ - Drop support for Python 2.6 and 3.3. 3.12 (2017-11-07) ----------------- New features ~~~~~~~~~~~~ - Accept Windows timezone identifiers as valid. #242 [geier] Bug fixes ~~~~~~~~~ - Fix ResourceWarnings in setup.py when Python warnings are enabled. #244 [jdufresne] - Fix invalid escape sequences in string and bytes literals. #245 [jdufresne] - Include license file in the generated wheel package. #243 [jdufresne] - Fix non-ASCII TZID and TZNAME parameter handling. #238 [clivest] - Docs: update install instructions. #240 [Ekran] 3.11.7 (2017-08-27) ------------------- New features ~~~~~~~~~~~~ - added vUTCOffset.ignore_exceptions to allow surpressing of failed TZOFFSET parsing (for now this ignores the check for offsets > 24h) [geier] 3.11.6 (2017-08-04) ------------------- Bug fixes ~~~~~~~~~ - Fix ``VTIMEZONE``\ s including RDATEs #234. [geier] 3.11.5 (2017-07-03) ------------------- Bug fixes ~~~~~~~~~ - added an assertion that ``VTIMEZONE`` sub-components' DTSTART must be of type DATETIME [geier] - Fix handling of ``VTIMEZONE``\ s with subcomponents with the same DTSTARTs and OFFSETs but which are of different types [geier] 3.11.4 (2017-05-10) ------------------- Bug fixes ~~~~~~~~~ - Don't break on parameter values which contain equal signs, e.g. base64 encoded binary data [geier] - Fix handling of ``VTIMEZONE``\ s with subcomponents with the same DTSTARTs. [geier] 3.11.3 (2017-02-15) ------------------- Bug fixes ~~~~~~~~~ - Removed ``setuptools`` as a dependency as it was only required by setup.py and not by the package. - Don't split content lines on the unicode ``LINE SEPARATOR`` character ``\u2028`` but only on ``CRLF`` or ``LF``. 3.11.2 (2017-01-12) ------------------- Bug fixes ~~~~~~~~~ - Run tests with python 3.5 and 3.6. [geier] - Allow tests failing with pypy3 on travis.ci. [geier] 3.11.1 (2016-12-19) ------------------- Bug fixes ~~~~~~~~~ - Encode error message before adding it to the stack of collected error messages. 3.11 (2016-11-18) ----------------- Fixes: - Successfully test with pypy and pypy3. [gforcada] - Minor documentation update. [tpltnt] 3.10 (2016-05-26) ----------------- New: - Updated components description to better comply with RFC 5545. Refs #183. [stlaz] - Added PERIOD value type to date types. Also fixes incompatibilities described in #184. Refs #189. [stlaz] Fixes: - Fix testsuite for use with ``dateutil>=2.5``. Refs #195. [untitaker] - Reintroduce cal.Component.is_broken that was removed with 3.9.2. Refs #185. [geier] 3.9.2 (2016-02-05) ------------------ New: - Defined ``test_suite`` in setup.py. Now tests can be run via ``python setup.py test``. [geier] Fixes: - Fixed cal.Component.from_ical() representing an unknown component as one of the known. [stlaz] - Fixed possible IndexError exception during parsing of an ical string. [stlaz] - When doing a boolean test on ``icalendar.cal.Component``, always return ``True``. Before it was returning ``False`` due to CaselessDict, if it didn't contain any items. [stlaz] - Fixed date-time being recognized as date or time during parsing. Added better error handling to parsing from ical strings. [stlaz] - Added __version__ attribute to init.py. [TomTry] - Documentation fixes. [TomTry] - Pep 8, UTF 8 headers, dict/list calls to literals. [thet] 3.9.1 (2015-09-08) ------------------ - Fix ``vPeriod.__repr__``. [spacekpe] - Improve foldline() performance. This improves the foldline performance, especially for large strings like base64-encoded inline attachements. In some cases (1MB string) from 7 Minutes to less than 20ms for ASCII data and 500ms for non-ASCII data. Ref: #163. [emfree] 3.9.0 (2015-03-24) ------------------ - Creating timezone objects from ``VTIMEZONE`` components. [geier] - Make ``python-dateutil`` a dependency. [geier] - Made RRULE tolerant of trailing semicolons. [sleeper] - Documentation fixes. [t-8ch, thet] 3.8.4 (2014-11-01) ------------------ - Add missing BYWEEKNO to recurrence rules. [russkel] 3.8.3 (2014-08-26) ------------------ - PERCENT property in VTODO renamed to PERCENT-COMPLETE, according to RFC5545. [thomascube] 3.8.2 (2014-07-22) ------------------ - Exclude editor backup files from egg distributions. Fixes #144. [thet] 3.8.1 (2014-07-17) ------------------ - The representation of CaselessDicts in 3.8 changed the name attribute of Components and therefore broke the external API. This has been fixed. [untitaker] 3.8 (2014-07-17) ---------------- - Allow dots in property names (Needed for vCard compatibility). Refs #143. [untitaker] - Change class representation for CaselessDict objects to always include the class name or the class' name attribute, if available. Also show subcomponents for Component objects. [thet] - Don't use data_encode for CaselessDict class representation but use dict's __repr__ method. [t-8ch] - Handle parameters with multiple values, which is needed for vCard 3.0. Refs #142. [t-8ch] 3.7 (2014-06-02) ---------------- - For components with ``ignore_exceptions`` set to ``True``, mark unparseable lines as broken instead rising a ``ValueError``. ``VEVENT`` components have ``ignore_exceptions`` set to ``True`` by default. Ref #131. Fixes #104. [jkiang13] - Make ``python-dateutil`` a soft-dependency. [boltnev] - Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to false allows the user to preserve the original property and parameter order. Ref #136. Fixes #133. [untitaker] - Fix tests for latest ``pytz``. Don't set ``tzinfo`` directly on datetime objects, but use pytz's ``localize`` function. Ref #138. [untitaker, thet] - Remove incorrect use of __all__. We don't encourage using ``from package import *`` imports. Fixes #129. [eric-wieser] 3.6.2 (2014-04-05) ------------------ - Pep8 and cleanup. [lasudry] 3.6.1 (2014-01-13) ------------------ - Open text files referenced by setup.py as utf-8, no matter what the locale settings are set to. Fixes #122. [sochotnicky] - Add tox.ini to source tarball, which simplifies testing for in distributions. [sochotnicky] 3.6 (2014-01-06) ---------------- - Python3 (3.3+) + Python 2 (2.6+) support [geier] - Made sure to_ical() always returns bytes [geier] - Support adding lists to a component property, which value already was a list and remove the Component.set method, which was only used by the add method. [thet] - Remove ability to add property parameters via a value's params attribute when adding via cal.add (that was only possible for custom value objects and makes up a strange API), but support a parameter attribute on cal.add's method signature to pass a dictionary with property parameter key/value pairs. Fixes #116. [thet] - Backport some of Regebro's changes from his regebro-refactor branch. [thet] - Raise explicit error on another malformed content line case. [hajdbo] - Correctly parse datetime component property values with timezone information when parsed from ical strings. [untitaker] 3.5 (2013-07-03) ---------------- - Let to_unicode be more graceful for non-unicode strings, as like CMFPlone's safe_unicode does it. [thet] 3.4 (2013-04-24) ---------------- - Switch to unicode internally. This should fix all en/decoding errors. [thet] - Support for non-ascii parameter values. Fixes #88. [warvariuc] - Added functions to transform chars in string with '\\' + any of r'\,;:' chars into '%{:02X}' form to avoid splitting on chars escaped with '\\'. [warvariuc] - Allow seconds in vUTCOffset properties. Fixes #55. [thet] - Let ``Component.decode`` better handle vRecur and vDDDLists properties. Fixes #70. [thet] - Don't let ``Component.add`` re-encode already encoded values. This simplifies the API, since there is no need explicitly pass ``encode=False``. Fixes #82. [thet] - Rename tzinfo_from_dt to tzid_from_dt, which is what it does. [thet] - More support for dateutil parsed tzinfo objects. Fixes #89. [leo-naeka] - Remove python-dateutil version fix at all. Current python-dateutil has Py3 and Py2 compatibility. [thet] - Declare the required python-dateutil dependency in setup.py. Fixes #90. [kleink] - Raise test coverage. [thet] - Remove interfaces module, as it is unused. [thet] - Remove ``test_doctests.py``, test suite already created properly in ``test_icalendar.py``. [rnix] - Transformed doctests into unittests, Test fixes and cleanup. [warvariuc] 3.3 (2013-02-08) ---------------- - Drop support for Python < 2.6. [thet] - Allow vGeo to be instantiated with list and not only tuples of geo coordinates. Fixes #83. [thet] - Don't force to pass a list to vDDDLists and allow setting individual RDATE and EXDATE values without having to wrap them in a list. [thet] - Fix encoding function to allow setting RDATE and EXDATE values and not to have bypass encoding with an icalendar property. [thet] - Allow setting of timezone for vDDDLists and support timezone properties for RDATE and EXDATE component properties. [thet] - Move setting of TZID properties to vDDDTypes, where it belongs to. [thet] - Use @staticmethod decorator instead of wrapper function. [warvariuc, thet] - Extend quoting of parameter values to all of those characters: ",;: ’'". This fixes an outlook incompatibility with some characters. Fixes: #79, Fixes: #81. [warvariuc] - Define VTIMETZONE subcomponents STANDARD and DAYLIGHT for RFC5545 compliance. [thet] 3.2 (2012-11-27) ---------------- - Documentation file layout restructuring. [thet] - Fix time support. vTime events can be instantiated with a datetime.time object, and do not inherit from datetime.time itself. [rdunklau] - Correctly handle tzinfo objects parsed with dateutil. Fixes #77. [warvariuc, thet] - Text values are escaped correclty. Fixes #74. [warvariuc] - Returned old folding algorithm, as the current implementation fails in some cases. Fixes #72, Fixes #73. [warvariuc] - Supports to_ical() on date/time properties for dates prior to 1900. [cdevienne] 3.1 (2012-09-05) ---------------- - Make sure parameters to certain properties propagate to the ical output. [kanarip] - Re-include doctests. [rnix] - Ensure correct datatype at instance creation time in ``prop.vCalAddress`` and ``prop.vText``. [rnix] - Apply TZID parameter to datetimes parsed from RECURRENCE-ID [dbstovall] - Localize datetimes for timezones to avoid DST transition errors. [dbstovall] - Allow UTC-OFFSET property value data types in seconds, which follows RFC5545 specification. [nikolaeff] - Remove utctz and normalized_timezone methods to simplify the codebase. The methods were too tiny to be useful and just used at one place. [thet] - When using Component.add() to add icalendar properties, force a value conversion to UTC for CREATED, DTSTART and LAST-MODIFIED. The RFC expects UTC for those properties. [thet] - Removed last occurrences of old API (from_string). [Rembane] - Add 'recursive' argument to property_items() to switch recursive listing. For example when parsing a text/calendar text including multiple components (e.g. a ``VCALENDAR`` with 5 ``VEVENT``s), the previous situation required us to look over all properties in ``VEVENT``s even if we just want the properties under the ``VCALENDAR`` component (VERSION, PRODID, CALSCALE, METHOD). [dmikurube] - All unit tests fixed. [mikaelfrykholm] 3.0.1b2 (2012-03-01) -------------------- - For all TZID parameters in DATE-TIME properties, use timezone identifiers (e.g. Europe/Vienna) instead of timezone names (e.g. CET), as required by RFC5545. Timezone names are used together with timezone identifiers in the Timezone components. [thet] - Timezone parsing, issues and test fixes. [mikaelfrykholm, garbas, tgecho] - Since we use pytz for timezones, also use UTC tzinfo object from the pytz library instead of own implementation. [thet] 3.0.1b1 (2012-02-24) -------------------- - Update Release information. [thet] 3.0 --- - Add API for proper Timezone support. Allow creating ical DATE-TIME strings with timezone information from Python datetimes with pytz based timezone information and vice versa. [thet] - Unify API to only use to_ical and from_ical and remove string casting as a requirement for Python 3 compatibility: New: to_ical. Old: ical, string, as_string and string casting via __str__ and str. New: from_ical. Old: from_string. [thet] 2.2 (2011-08-24) ---------------- - migration to https://github.com/collective/icalendar using svn2git preserving tags, branches and authors. [garbas] - using tox for testing on python 2.4, 2.5, 2.6, 2.6. [garbas] - fixed tests so they pass also under python 2.7. [garbas] - running tests on ``https://jenkins.plone.org/job/icalendar`` (only 2.6 for now) with some other metrics (pylint, clonedigger, coverage). [garbas] - review and merge changes from ``https://github.com/cozi/icalendar`` fork. [garbas] - created sphinx documentation and started documenting development and goals. [garbas] - hook out github repository to https://about.readthedocs.com service so sphinx documentation is generated on each commit (for main). Documentation can be visible on: https://icalendar.readthedocs.io/en/latest/ [garbas] 2.1 (2009-12-14) ---------------- - Fix deprecation warnings about ``object.__init__`` taking no parameters. - Set the VALUE parameter correctly for date values. - Long binary data would be base64 encoded with newlines, which made the iCalendar files incorrect. (This still needs testing). - Correctly handle content lines which include newlines. 2.0.1 (2008-07-11) ------------------ - Made the tests run under Python 2.5+ - Renamed the UTC class to Utc, so it would not clash with the UTC object, since that rendered the UTC object unpicklable. 2.0 (2008-07-11) ---------------- - EXDATE and RDATE now returns a vDDDLists object, which contains a list of vDDDTypes objects. This is do that EXDATE and RDATE can contain lists of dates, as per RFC. ***Note!***: This change is incompatible with earlier behavior, so if you handle EXDATE and RDATE you will need to update your code. - When createing a vDuration of -5 hours (which in itself is nonsensical), the ical output of that was -P1DT19H, which is correct, but ugly. Now it's '-PT5H', which is prettier. 1.2 (2006-11-25) ---------------- - Fixed a string index out of range error in the new folding code. 1.1 (2006-11-23) ---------------- - Fixed a bug in caselessdicts popitem. (thanks to Michael Smith ) - The RFC 2445 was a bit unclear on how to handle line folding when it happened to be in the middle of a UTF-8 character. This has been clarified in the following discussion: `[Ietf-calsify] draft-ietf-calsify-rfc2445bis-01.txt / UTF-8 `_. And this is now implemented in iCalendar. It will not fold in the middle of a UTF-8 character, but may fold in the middle of a UTF-8 composing character sequence. 1.0 (2006-08-03) ---------------- - make get_inline and set_inline support non ascii codes. - Added support for creating a python egg distribution. 0.11 (2005-11-08) ----------------- - Changed component .from_string to use types_factory instead of hardcoding entries to 'inline' - Changed UTC tzinfo to a singleton so the same one is used everywhere - Made the parser more strict by using regular expressions for key name, param name and quoted/unquoted safe char as per the RFC - Added some tests from the schooltool icalendar parser for better coverage - Be more forgiving on the regex for folding lines - Allow for multiple top-level components on .from_string - Fix vWeekdays, wasn't accepting relative param (eg: -3SA vs -SA) - vDDDTypes didn't accept negative period (eg: -P30M) - 'N' is also acceptable as newline on content lines, per RFC 0.10 (2005-04-28) ----------------- - moved code to codespeak.net subversion. - reorganized package structure so that source code is under 'src' directory. Non-package files remain in distribution root. - redid doc/.py files as doc/.txt, using more modern doctest. Before they were .py files with big docstrings. - added test.py testrunner, and tests/test_icalendar.py that picks up all doctests in source code and doc directory, and runs them, when typing:: python2.3 test.py - renamed iCalendar to lower case package name, lowercased, de-pluralized and shorted module names, which are mostly implementation detail. - changed tests so they generate .ics files in a temp directory, not in the structure itself. collective-icalendar-138c845/CODE_OF_CONDUCT.md000066400000000000000000000003571524134033600206510ustar00rootroot00000000000000# Contributor Covenant 3.0 Code of Conduct The icalendar project follows the Contributor Covenant 3.0 Code of Conduct, which is hosted on the Python Calendaring Ecosystem website at the following link. https://pycal.org/code-of-conduct/ collective-icalendar-138c845/CONTRIBUTING.md000066400000000000000000000002531524134033600202760ustar00rootroot00000000000000# Contributing See our [contributing guide](https://icalendar.readthedocs.io/en/latest/contribute/index.html) for complete information on how to contribute to icalendar. collective-icalendar-138c845/LICENSE.rst000066400000000000000000000024411524134033600176620ustar00rootroot00000000000000======= License ======= Copyright (c) 2012-2013, Plone Foundation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. collective-icalendar-138c845/MANIFEST.in000066400000000000000000000002711524134033600176030ustar00rootroot00000000000000include *.rst tox.ini graft docs recursive-include src/icalendar * recursive-exclude src/icalendar *.pyc *~ recursive-exclude src/icalendar/fuzzing *.py *.sh *.ics include funding.json collective-icalendar-138c845/Makefile000066400000000000000000000136611524134033600175140ustar00rootroot00000000000000# Makefile for Sphinx documentation .DEFAULT_GOAL = help SHELL = bash # You can set these variables from the command line. SPHINXOPTS ?= PAPER ?= VERSION ?= # Internal variables. RUFFPATH = "$(realpath .venv/bin/ruff)" SPHINXAUTOBUILD = "$(realpath .venv/bin/sphinx-autobuild)" SPHINXBUILD = "$(realpath .venv/bin/sphinx-build)" TOWNCRIERPATH = "$(realpath .venv/bin/towncrier)" DOCS_DIR = ./docs/ BUILDDIR = ../_build PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -W -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . VALEFILES := $(shell find $(DOCS_DIR) -type f -name "*.rst" -print) # Also add `src` for docstrings. VALEOPTS ?= ZIZMOROPTS ?= PYTHONVERSION = >=3.10,<3.15 # Add the following 'help' target to your Makefile # And add help text after each target name starting with '\#\#' .PHONY: help help: # This help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' # environment management .venv: ## Install required Python, create Python virtual environment, and install package requirements @uv python install "$(PYTHONVERSION)" @uv venv --python "$(PYTHONVERSION)" @uv sync --group dev @uv run pre-commit install .PHONY: sync sync: ## Sync package requirements @uv sync .PHONY: init init: clean clean-python .venv ## Clean docs build directory, Python virtual environment, and initialize Python virtual environment .PHONY: clean clean: ## Clean docs build directory cd $(DOCS_DIR) && rm -rf $(BUILDDIR)/ .PHONY: clean-python clean-python: clean rm -rf .venv/ # /environment management # documentation builders .PHONY: html html: .venv ## Build html cd $(DOCS_DIR) && $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: livehtml livehtml: .venv ## Rebuild Sphinx documentation on changes, with live-reload in the browser cd "$(DOCS_DIR)" && ${SPHINXAUTOBUILD} \ --watch "../src/icalendar/" \ --watch "../CHANGES.rst" \ --re-ignore ".*\\.swp|.*\\.typed" \ --ignore "../src/icalendar/tests" \ --ignore "../src/icalendar/fuzzing" \ --port 8050 \ -b html . "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) .PHONY: dirhtml dirhtml: .venv cd $(DOCS_DIR) && $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: .venv cd $(DOCS_DIR) && $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: text text: .venv cd $(DOCS_DIR) && $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: sphinx-changes sphinx-changes: .venv cd $(DOCS_DIR) && $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." # /documentation builders # test .PHONY: linkcheck linkcheck: .venv ## Run linkcheck cd $(DOCS_DIR) && $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/ ." .PHONY: linkcheckbroken linkcheckbroken: .venv ## Run linkcheck and show only broken links cd $(DOCS_DIR) && $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | GREP_COLORS='0;31' grep -wi "[^.]broken\|redirect" --color=always && if test $$? = 0; then exit 1; fi || test $$? = 1 @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/ ." .PHONY: vale vale: .venv ## Run Vale style, grammar, and spell checks @uv run vale sync @uv run vale --no-wrap $(VALEOPTS) $(VALEFILES); \ if [ $$? = 0 ]; then \ echo; \ echo "Vale passed!"; \ else \ echo; \ echo "Vale spell, style, and grammar check failed."; \ echo "Read the error messages above to see what didn't pass."; \ echo "For guidance of how to correct the errors, see:"; \ echo "https://icalendar.readthedocs.io/en/latest/contribute/documentation/build-check.html#spelling-grammar-and-style"; \ exit 1; \ fi .PHONY: zizmor zizmor: .venv ## Run zizmor static analysis on GitHub Actions workflows @uv run zizmor $(ZIZMOROPTS) .github/workflows/ .PHONY: doctest doctest: .venv ## Test snippets and docstrings in the documentation @echo; @pytest src/icalendar/tests/test_with_doctest.py .PHONY: docs-all docs-all: .venv clean vale doctest html linkcheckbroken ## Clean docs build, then run vale, doctest, html, and linkcheckbroken .PHONY: test test: .venv ## Run code tests and coverage @uv run tox # /test # development .PHONY: dev dev: .venv ## Install required Python, create Python virtual environment, install package and development requirements .PHONY: format format: .venv ## Format the code base with ruff $(RUFFPATH) format $(RUFFPATH) check --fix # /development # deployment .PHONY: rtd-prepare rtd-prepare: ## Prepare environment on Read the Docs asdf plugin add uv asdf install uv latest asdf global uv latest .PHONY: rtd-pr-preview rtd-pr-preview: rtd-prepare .venv ## Build pull request preview on Read the Docs cd $(DOCS_DIR) && $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${READTHEDOCS_OUTPUT}/html/ # /deployment # release .PHONY: wo wo: .venv @uv run generate_windows_to_olson_mapping.py $(RUFFPATH) format .PHONY: changes-check changes-check: dev $(TOWNCRIERPATH) check .PHONY: changes-draft changes-draft: dev @test -n "$(VERSION)" || (echo "VERSION is not set. Run 'export VERSION=x.y.z' first." && exit 1) $(TOWNCRIERPATH) build --draft --version ${VERSION} --yes .PHONY: changes changes: dev @test -n "$(VERSION)" || (echo "VERSION is not set. Run 'export VERSION=x.y.z' first." && exit 1) $(TOWNCRIERPATH) build --version ${VERSION} --yes # /release collective-icalendar-138c845/README.rst000066400000000000000000000123471524134033600175430ustar00rootroot00000000000000========================================================== Internet Calendaring and Scheduling (iCalendar) for Python ========================================================== The `icalendar `_ package is an :rfc:`5545` compatible parser and generator of iCalendar files. icalendar can create, inspect, and modify calendaring information with Python. icalendar supports multiple timezone implementations, including `zoneinfo `_, `dateutil.tz `_, and `pytz `_. ---- :Homepage: https://icalendar.readthedocs.io/en/stable/ :Community Discussions: https://github.com/collective/icalendar/discussions :Issue Tracker: https://github.com/collective/icalendar/issues :Code: https://github.com/collective/icalendar :Dependencies: `python-dateutil `_ and `tzdata `_. :License: `2-Clause BSD License `_ :Contribute: `Contribute to icalendar `_ :Funding: `Open Collective `_ ---- .. image:: https://img.shields.io/pypi/v/icalendar :target: https://pypi.org/project/icalendar/ :alt: Python package version on PyPI .. image:: https://img.shields.io/github/v/release/collective/icalendar :target: https://pypi.org/project/icalendar/#history :alt: Latest release .. image:: https://img.shields.io/pypi/pyversions/icalendar :target: https://pypi.org/project/icalendar/ :alt: Supported Python versions .. image:: https://static.pepy.tech/personalized-badge/icalendar?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads :target: https://pypistats.org/packages/icalendar :alt: Downloads from PyPI .. image:: https://img.shields.io/github/actions/workflow/status/collective/icalendar/tests.yml?branch=main&label=main&logo=github :target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3Amain :alt: GitHub Actions build status for main .. image:: https://readthedocs.org/projects/icalendar/badge/?version=latest :target: https://icalendar.readthedocs.io/en/latest/ :alt: Documentation status .. image:: https://coveralls.io/repos/github/collective/icalendar/badge.svg?branch=main :target: https://coveralls.io/github/collective/icalendar?branch=main :alt: Test coverage .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://img.shields.io/badge/%F0%9F%A4%91-Funding-brightgreen :target: https://opencollective.com/python-icalendar :alt: Funding Install icalendar ================= See how to `install icalendar `_. Usage ===== For how to use icalendar, including how to read, modify, and write iCalendar files, see the `Usage `_ guide. Related projects ================ `VObject `_ VObject is intended to be a full-featured Python package for parsing and generating Card and Calendar files. `iCalEvents `_ iCalEvents is built on top of icalendar, and allows you to query iCalendar files to get the events happening on specific dates. It manages recurrent events as well. `recurring-ical-events `_ Library to query an ``icalendar.Calendar`` object for events and other components happening at a certain date or within a certain time. `x-wr-timezone `_ Library and command line tool to make ``icalendar.Calendar`` objects and files from Google Calendar, using the non-standard ``X-WR-TIMEZONE`` property, compliant with the :rfc:`5545` standard. `ics-query `_ Command line tool to query iCalendar files for occurrences of events and other components. `icalendar-compatibility `_ Access event data compatible with :rfc:`5545` and different implementations. `caldav `_ CalDAV is a :rfc:`4791` client library for Python based on icalendar. `icalendar-anonymizer `_ Anonymize iCalendar files by stripping personal data while preserving technical properties for bug reproduction. `ical2jcal `_ Convert between iCalendar and jCalendar. `sphinx-icalendar `_ Provides calendar code blocks and syntax highlighting in a tabbed interface for documentation as an extension to Sphinx. `Python Calendaring Ecosystem `_ The standards compliant, integrated Python calendar packages. Change log ========== See the `change log `_ for the latest updates to icalendar. collective-icalendar-138c845/SECURITY.md000066400000000000000000000063371524134033600176470ustar00rootroot00000000000000# Security policy This security policy describes how to privately report _potential_ security issues with icalendar, and how the icalendar security team processes reports, all in a responsible manner. If you're unsure whether your issue qualifies as a security vulnerability, it's better to err on the side of caution and report it as one, allowing the icalendar security team to evaluate its merits and determine whether it's one. - [Report a _potential_ security issue](https://github.com/collective/icalendar/security/advisories/new). - [Security advisories](https://github.com/collective/icalendar/security/advisories) ## Supported versions Security vulnerabilities are fixed only for the latest version of icalendar. It is highly recommended to upgrade to the latest release. ## Procedure The icalendar security team will coordinate with the [Plone security team](https://plone.org/security/report). If it's determined that your report may be a security vulnerability with the project, the team may contact you for further information. As volunteers, the team asks that you delay public disclosure of your report for at least ninety (90) days from the date you report it to the team. This will allow sufficient time for the team to process your report and coordinate disclosure with you. Once verified and fixed, the following steps will be taken: - The team will use GitHub's Security Advisory tool to report the vulnerability. - GitHub will review the Security Advisory report for compliance with Common Vulnerabilities and Exposures (CVE) rules. If it is compliant, they will submit it to the MITRE Corporation to generate a [CVE](https://www.cve.org/). This in turn submits the CVE to the [National Vulnerability Database (NVD)](https://nvd.nist.gov/vuln/search). GitHub notifies the team of their decision. - Assuming it is compliant, the team then publishes the [Security Advisory](https://github.com/collective/icalendar/security/advisories) on GitHub, which triggers the next steps. - GitHub will publish the CVE to the CVE List. - GitHub will broadcast the Security Advisory via the [GitHub Advisory Database](https://github.com/advisories). - GitHub will send [security alerts](https://docs.github.com/en/code-security/supply-chain-security/managing-vulnerabilities-in-your-projects-dependencies/about-alerts-for-vulnerable-dependencies) to all repositories that use icalendar and have opted into security alerts. This includes Dependabot alerts. - The team will make a bug-fix release. - The team will send an announcement through the usual channels: - the [change log](https://github.com/collective/icalendar/CHANGES.rst) - the [GitHub releases of icalendar](https://github.com/collective/icalendar/releases) - [icalendar Discussions Announcements](https://github.com/collective/icalendar/discussions/categories/announcements) - [icalendar Open Collective conversations](https://opencollective.com/python-icalendar/conversations) - [Plone's Security Announcements](https://plone.org/security/announcements) - [Plone Community Forum Announcements](https://community.plone.org/c/announcements/27) - The icalendar security team will provide credit to the reporter or researcher in the vulnerability notice. collective-icalendar-138c845/docs/000077500000000000000000000000001524134033600167755ustar00rootroot00000000000000collective-icalendar-138c845/docs/_include/000077500000000000000000000000001524134033600205575ustar00rootroot00000000000000collective-icalendar-138c845/docs/_include/configure-git-card-dev.inc000066400000000000000000000003651524134033600255030ustar00rootroot00000000000000.. card:: .. image:: ../_static/contributing/development-icalendar-git-workflow.svg :alt: icalendar git workflow :target: ../_static/contributing/development-icalendar-git-workflow.svg +++ *icalendar git workflow* collective-icalendar-138c845/docs/_include/configure-git-card-doc.inc000066400000000000000000000003731524134033600254710ustar00rootroot00000000000000.. card:: .. image:: ../../_static/contributing/development-icalendar-git-workflow.svg :alt: icalendar git workflow :target: ../../_static/contributing/development-icalendar-git-workflow.svg +++ *icalendar git workflow* collective-icalendar-138c845/docs/_include/configure-git-steps.inc000066400000000000000000000013601524134033600251500ustar00rootroot00000000000000#. Start by `forking icalendar's repository `_ to your account through the GitHub interface. .. seealso:: `Fork a repository `_ #. `Clone your forked repository `_. #. `Configure Git to sync your fork with the upstream repository `_. .. seealso:: :ref:`branch-policy` collective-icalendar-138c845/docs/_include/configure-git.inc000066400000000000000000000007571524134033600240250ustar00rootroot00000000000000Configure Git ------------- Unless you're a maintainer or administrator, you don't have write access to push commits to GitHub repositories under the Collective organization or the icalendar repository. You can, however, push commits to your fork. Thus, a typical workflow will be circular in nature. You'll pull code from the upstream icalendar repository, push your work from your local clone to your remote fork, then make a pull request from your fork to the upstream icalendar repository. collective-icalendar-138c845/docs/_include/maintainers.inc000066400000000000000000000003061524134033600235630ustar00rootroot00000000000000- `@geier `_ - `@jacadzaca `_ - `@niccokunzmann `_ - `@SashankBhamidi `_ collective-icalendar-138c845/docs/_include/prerequisites.inc000066400000000000000000000036371524134033600241670ustar00rootroot00000000000000Prerequisites ------------- You'll need to first install prerequisites on, and clone the icalendar repository to, your local computer. Tests, code formatting, and documentation builds and checks require that you install :program:`GNU Make`, :program:`uv`, and :program:`Git`. .. note:: :program:`uv` handles the installation of Python and all dependencies for icalendar. :program:`Make` ''''''''''''''' :program:`Make` is used to provide an interface to developers to perform repetitive tasks with a single command. :program:`Make` comes installed on most Linux distributions. On macOS, you must first `install Xcode `_, then install its command line tools. On Windows, it is strongly recommended to `Install Linux on Windows with WSL `_, which will include :program:`Make`. Finally, it is a good idea to update your system's version of :program:`Make`, because some distributions, especially macOS, have an outdated version. Use your favorite search engine or trusted online resource for how to update :program:`Make`. uv '' `uv `_ is used for installing Python, creating a Python virtual environment, and managing dependencies for documentation. Install :program:`uv`. Read the console output for further instructions, and follow them, if needed. .. tab-set:: .. tab-item:: macOS, Linux, and Windows with WSL .. code-block:: shell curl -LsSf https://astral.sh/uv/install.sh | sh .. tab-item:: Windows .. code-block:: shell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" .. tab-item:: Other `Other uv installation methods `_ Git ''' Install `Git `_ for your operating system. collective-icalendar-138c845/docs/_static/000077500000000000000000000000001524134033600204235ustar00rootroot00000000000000collective-icalendar-138c845/docs/_static/contributing/000077500000000000000000000000001524134033600231325ustar00rootroot00000000000000collective-icalendar-138c845/docs/_static/contributing/development-disable-pre-commit.png000066400000000000000000001601521524134033600316420ustar00rootroot00000000000000‰PNG  IHDRľÓ1cŢ1YiCCPICC Profile(‘c``bI,(Čaa``ČÍ+) rwR��ŚR`ĹŔČ É Ä Ć ™�\\ŕŕTÂŁQÁ·k@Ő@pYdÖ¬wâ«T¸ż;ě¨űţĽěâzLő(€+%µ8H˙b­ä‚˘F ; Ľ¤Ä®˛EЀޞ{@ět{�ťao« r˛OŮɉ)@ö [' I<‰ť›Sš uČő<©yˇÁ@š�eśL,‚Ü�$Đ8Ôš@Őć30T21d2¤3d0”0(08E rR�lO†<†d= Ű�Á�Í@aŚv±_«9@«ĽbÁŠ ŰŠubĘ@ç~b`8°­ ±(˘ŚßXŠÓŚŤ l Ö˙˙–e``ßĹŔđ·č˙˙ßs˙˙˙»„��ů&Po!~ßbvľb­ĘbeXIfMM*‡i&’†P ľ ÓASCIIScreenshotýÁ,>iTXtXML:com.adobe.xmp 834 Screenshot 1069 1 E  @IDATxěťxUוď—z蘒‚„BŃ«Á`Ü ŕw;¶ÓědbÇyIfňfňć›÷&y™Ç™$/Ĺ©ŽDZănÇ6ŘôŢ{•HH Ô{×[˙}u.WâęŢ«‚ŇńÝ{Î={ź]~GśsÖ^kŻí%"áúńŐO»~Ľ»>cu›ĄźŮúÉÖĎýë'B?úńŃňc‹ßś‹}l˝ô�tގüNý8¦YĺX[äÇyř Ż%Öol!ŽůëG:Ňë°ňł~ňçß_÷˙Ö˙#kË˙Ľ˙ŕjÝoqď„Xż±…8ţ˝đţkă.ŕĂçO÷{ ¸€ źż|ţňůŰý˙†uµ¶|ţňů‹g�őĽĹ˝býĆâř÷2Ňžż¸‡ôÖ˙M«×Oˇ~Žčg‡~Žëç¤~,NĐ !m€ e[$ŽÓĎúY6jÔ¨Ŕ´´±MqqńˇˇááŃľ>ľľ~~ašfÇ+biŞ^ZT§ŁŢŠë’u횼ťúíĺ�ŕ�Ď”aŇmU™zŇ­°~ŰÓÔâ.äĎż?ţ˙3wŰm˘Ç}Ăţ…÷Ţůü±Ą$Ö3„Ď_ű{ ß?ôĎŁÇ}8Ö˝ÔÚÚŽńůËç/źżřż`¤Ç˙ű˙ľ ęűGk[[m[kKs]]mEié…ş‚üĽ€˛˛2 &˝ŻźôWÇĽq˘°Łźôł4..nŃüů‹"këę“kjkĄ±±QZ[[Ą˝˝]o~ťŇŃ…›B$@$@$@$@$@$@ź=ooocCőńń??? ’¨Č ;»yó†ĘŇóç7k«6ęçý@ůí„•7T?7éçşéÓgĚI“>˝¬Ľ"Ľ´´Tęęęş)˝P|)$@$@$@$@$@$@$pĄ@/ĹĆYi¤¦¦VšššĂÇ¦Ź‹‹Ś ÷.))ÁtxOë§Šď4ýÜ Ą7($bVee•TWWÓ˛«P($@$@$@$@$@$@Cź<“á­ÜŇŇ"Á!áŁăbcĄ¤¤8@[^ ź*(ľ_ŇůĽ·Ą¤Ž›VĄJ/¬Ľ      ¸Ú@ń…58fԨĆúÚŔúúz¸-ď„é÷îi3fGÖÖÖI-•Ţ«íş˛˝$@$@$@$@$@$@`Ě­®®‘ąó"žŐ ýdx«µ×·˝­# îÍ      ¸Ú @ż­««OŇŕÍ0öŽ÷=:±±¶®VÚÚÚ®öľ±ý$@$@$pU 6Q(ŻŠĆ˛‘$@$@$p€~ =79eL‹6¶oTô¨đňňJ·]ńÓ5ďMŠ–[ăÂe\h ÉźW×$+Kkä­łŇÚÇ�Ď^Z^XX��‡żżź)ݵĄUę5"W­.ŁÔ×Ň^ľ^ł$T"çKP’­ĽĆł­RµŁAĘ7ÔIg#R»˝ČĚ@$@#śžMŃŃŃ)çĎ—šçŃ`#‰ŹŹ“'źx\tΑüâ—/2�ä`fy$@$@$ĐE ©©IF'Ä…éĎ)ľ���-Ú•ÄřĘO˛SeB—ÂkĺÍ|V$DĘs‡NKiłgVc_‰Ť‹“«(ł |BBBä‚.§Ô¦á©=żHűl¬Ť±)ĽÖ9!éţ‚OôÂÉ˙Ůi­ň¬<ë|nI€H€F�¤¤Dą÷ž»Ě ¬Őëâ’yűíw͡ˇ!ćřÉÜ“ň÷ż$oľů•^;)î� � �ŔŕčP=ú®–áëăëâĘ­řޤ¨K,˝Îš”©Ęď=‰QňZQ…łdű±ĐĐĐK,˝öD‡(żČ ·gWł8äKŻłüA)~‚Ľeką\“3>}šqÓnh¨—Ó§ eőšµĆÂlowH€H€H`„čĐé¸ĐwµŰťľ:™Öĺü˘[ă`öL�×­â«nĚžJ¨ću§řFÎ÷Ľ<äĄâë)}ć# �‘CŔq ű;W,—ăÇOHqq±ĽôŇź¤ąąŮ€€byűm·�ý’’s;J•Ýůâŕ/ĺeĺĆ}‰íŞĐ¶·µ«bŰÝ›Ę[˝˘ĽŐĹîÍ1:ŹŘ’�đ0óÇZ[[Ĺ×W§ď¤ĄJ¸ńĹߪbĽP,�g˛Ă=5{ŠuŞŔ˘ś:fŚÜtÓ ćXž*Ő 2eĘ$U�˝äťwß·çĺ � � Ś4đŢ‚ľ ńUb—ýĎ r™î�8!Ô}^<¤=Oň÷�×ëŞěľäuUÓH€H€†�ŇŇ jaÝ!óçĎ5�­,EJěćÍ[dó–­2M­ş�ŠŠJ٤Dz˛&%ŠčŹ^ř‰±ĚBĹň żýÝLŢ"µăŮ0űŻ˙íMăB=vlšůÝ󫲲ʜ7~|†şWŻ0Ę0‚lMČĚ4YsrNĘ›o˝-cǦÉCŢo?=Ró@đp?s¦PŽ9*P�këęíy¸C$@$@#ś€—±ř„6ÜČÉť�]^÷Á÷Áę6Ë! Ö®[/;vî’Śqé’śś$&L�  @Y˛äZ9zô�ZRăM/áĘ|˙}÷Ú{ŚAZe¨ś;wÎX| ‹ěEéĽ$‰Ś´y^ť>}Ú¬xPP`ŰZq0rsrĄT]ťăâbeéŇ%ć\(ľk×®7Vk{aÜ!  �‘K@]ťÝČńÚFąŮM6“ŚĺŤÜ Ć�Üě‰`y#wŇp¦ŐDnv—éXŢ�B$@$@= Lś�ećÉ666É;ďĽ+’cęîlYVC40¬Â‰‰ŁŤEówˇđBAĆ2| ú±$@]źť‰;/¦Ţâm\¸Pfęť2e˛äČřŚŚnÁ˝tđ‡­4 2–Ä|ä�=˝B�oQH€H€H€ /ow Ţ.®�,ąąőM‚ĽîÁŞšŐęëN°¶°»ŔV(Ł|Cť4ş·ä6µšĽîęe: � �ŔČ#€ĺ‹Ţ}ď�Ĺ7,,ĚĚŁ…˛ ř/yM•Ęv3xëÖí&Off†Qzˇ˙íŤ·Ś 2˘5#¸ćÚÂ=� ză¦-&@VPP�±#š3ÄÜĐ-ćCZőąçL0g‘¦÷îÝ/ÇŽ—•+?1ĺ"/¬Ä{öě“�‡›S1Ö^ßúH­Ŕ   C ÝkŮň»;ľ8Bě L\€ŻËµ|ˇăĐi)mnsvú%Ç|}|\®ĺ ĹřBi©´ő��yIA]ü"5ćł±˝.kĹ8˙§¤µŠ“|{cČă$@$@6ááá&ňrm­¬66:ĹĺSw¬�ĎŽ™¬ą·=]—}ôŮşŻrĎÝwę|ăLł|Ňö»$-uŚ.4Éó‹_ľ(UU6K3ʇrŤváC!  ÉÖ©B˙ŕť:źĚ ˙ ó–\I˝ŽFż_R%Uş#�Q¨1ٔߟvYçsK$@$@Ă‘@||Ľäś8ć™Ĺw8`źH€H€Hŕj!$)Éɬó‡ˇÜ•�Ä   ׺,ľ n�[ą.†©$@$@$@—›@CCŁśČÉ˝ÜŐ°|  ~ĽĽĐ' neŰ~dŹH€H€H€H€H€H€F6ť ¤Ň®“aÍÎȆÁŢ“ � � � � � [n—3¶=gÇH€H€H€H€H€H€†7›‡ł/ßá}™Ů;     ąlÎśă;r˙Řs     Ţl_U|)$@$@$@$@$@$@$0 Ř,ľŢ n5/.űD$@$@$@$@$@$  ™ĺŚ„®Îüc ¸jDDĆ\5meCI€H€H€H€†Z|‡Îµ`KH€H€H€H€H€H€ź€-ľ�•%’ � � � � � ť¶9ľť n5.›@$@$@$@$@$@$0řĽls|ŐâkÓ€ż–H$@$@$@$@$@$@W’@—ľK‹ď•Ľ¬›H€H€H€H€H€Hŕrŕ:ľ—›0Ë'     ¸Bl®ÎşŽo׺FW¨¬–H€H€H€H€H€H€.»«3çř^Ŕ,•H€H€H€H€H€H`Hđő¤^Ţľ?î6‰{«„DeŠŹ_°'§1 � ôJ ˝µAę+s¤4ĄśĎűX:;ÚzÍË    蛇s§Ż;Wg˙ (™tÝó3±_őđ$ pFháqÓÍ'!ăN9şţ;ŇŇXé,+Ź‘ � � � �@˙Ř<ś˝|]-gKŻĄô6×—Č©=?•šŇCŇÚTŐżJy � tđ ŚTĄ7[Ňg}Ă ¬á^s`ŐÓ´üň/„H€H€H€H`đtY|].g7îVóBÚ\Nö}ř¸”źŮDĄwđ.K"�Mh¸§ŕŢ‚{ ĽJpϡ� � � � �Ŕ °·rŐ9~ěí¦ľS{~"m-ő�V7 " ‹î-¸Ç@¬{Ž•Ć- � � � � /—ß�¨ SGMéáA¨‹E� �€sÖ=Ćşç8ĎĹŁ$@$@$@$@$ĐGvWgËůř…�R[›®lŔ™ź˙ě…>öŽŮI€®&Ö=Ćşç\Mmg[I€H€H€H€†0O\ť‡póŮ4      đ„€ë¨Îž”Ŕ<$@$pµ�ŚŠ–¬‰“Mó ňOÉą’ł.»â 3g͕֖VŮł{»ËĽC9Ń“~„……KÖ¤)-eeĄ’w2GjŞ7˘xD¤<đĐăR|¶P>üŕíˇŚŚm#  ¸ş č:ľ�,S˛łeٲĺ2aÂSň‰'äď˙@:4Č5u/.8$TîXv·ěß·KrsŽwOĽ ő§?ó,’üSyRzľä˛ôřr—YÍBIŔ�9sŻ‘%×ÝhrŘżGŢxýĎ.r‹ɲĺ÷JCcýU­řşëÇ”©ÓĺλŔ`;ŹNuZ»fĄ¬[łĘ~ĚŐÎu×ß,‰I)˛mëFÉĎË5Y{‹ŠŽ‘ÔÔ±%›ŠŻ+šL#   o—Á­úZřÝ÷Ü+OĺiYłzµ<űě×Íű8†´Ë)j9Čž:CŇĆÚrÍš=߼¸]Î:/gŮ}폗^Ęĺ+>§ýOż,ÍşÜĺ_–FłPpC`’Z4-ÉĚš(~ţţÖĎ»Ĺ˙őwŮ”Ţ'ŽÉďż);¶o6Ą¦Ť3ů®żńV™89[š/9öé'ʲ÷HyEąüĺĺߥÚ=űłsűąá†[¤QŰřŰfęMKĎ�iÓfę`ŕ8ńóő“'ŽĘĆ kěLL&~‘ � � xD 2"LjjëÍű�G' 0“·懅HUuíK˛źŢ>hŠ/Ü›_~ůŹÝ”^«(ÂHCžţşýX`ĄřňSĎČÚµ«$M_¸˛§Í�Ŕ€ yö›ß•={vČ–Ťë¬ćŘ·>>>r˝ľ,NPKO€�ś={FV­ü@*ő% ‚ônşM2'LÔ—Í9[Th\ýÎź5ő=óěwd×®í2Q­FQQ1R Ö‹•żg^”ń2\U])Ö~bćơ}}Íß×ţ Í!ˇˇňŘ_‘}y->[$›7­•Ó:wâŞ?ž¤›BşľPÖ˝÷=,Pn^˙ëźĚËd’ş5Ξ»@Ňő…ł®ľN<ĘÖMëOăţ0$Ь ÍŚYłäŕÁúǦüFEGËÔ©Ódßž=C¶ÇÓ¦Í2mËU«f•*�P|áâ‹é–<üč×�ŔŔ@I˝{¬•d¶ĄŞÔĂ]:[VKńť¬űqŞP>´ßüź»÷ţGdŚ*˝ĺ:O¶łŁÝXA“’Säůţ›řűů›Ľ¸`� ±©Á¸Ăťş©±ÉŢ–‡yR23' ÜŤkjŞe|ĆISáߨ’ŰJomm1J<Ľ_ĆŤĎ4ÖTÜŻÜőñSĄçĎKssłč=÷ľ3 ř©SąňéŞłą¬ł­­UŰÚnň·µµ ~;;főß«k=yë·;Ź<ö%ILL6ĺăş|NďE~ʲˇkŕ3"2Jýü—ő ……§Ĺ+ČKćÍ_$Qj}ţÓK/vë� � � ¸&�+–âë:çঢ^<çĎťż0(š«ó}ÉÂ|ŢŢiČÓ_�îË9:÷Äń#Ć‚’5qŠ„…†Ű‹Ś‹-ÁŞ žÔ ,Ĺú˛×ÜŇ,»vn“"}ńq&wÝó ,şv©ZGňĺС} Đ_řŞ=ëÝ÷>$ ŮŇwďÚf^Śň)S2ˇľnş]•ľ*UnOČôłĺ™çľ+Q11F1ŹŚ�R÷ă‹.ŢýÉß—ţ M‹ß`Fbîß«óë’ĺž{Ća#îúă.Ý*Jü?ˇSÔę´ĘnAyř±/ĘXUz÷ěŮi¬|·©ĄlÚt›raťËíđ#PS[#űöí5Š.Bđ1JŻCÚP XA0@sđŕ>ł?n\¦`~=d‚˝‚Ň eňwżýą<˙ź˙[Ž>`ҬŻ�ökwŠZ„ĂÂ#tcŐÂkéţ}»M`¨Â3˛eózůďţŻüäÇ?4�Bá‘2FÇ,�ŇűĘź/˙ńý˙%‡şÚ’Ńuż„•J/¬ężţŐOäů˙ř7Ů«�yPô¦ŞEV^¤C!~áů—˙čűf^mpP�,Xp­Gý°Ú�-Ôżż˙–©oĽzÍ<öř—ĺźţůßĺs÷?Şj67gwuľóÖkr¦Ë˝qýjůë+/‰łcŽő:î»â�€[PzŻËQ˝†Ž‚{”Ţj|üýo~.?˙éó† 71hG!  đŚ€Ąôćć|fÖ^´ ďR¨ 7Ú0 ± °ű šĹ·/ŤYqçťňČĂŹŘOůË«‘÷ß{ĎţŰŮÎDUrש5Ö]H±Z9ŐQgrüč!cuĄ.†Ű¶lp–E]ăŤR†Ŕ+ř®És2÷„Q¨ńrס–(m›6®•OÔ 9rř <űŤR đ$9 /Ľ�\uź{덿�ýQ±ńúb8ZţüÇ_KsSłqýĽëžL]pcěO~śăILáú•ŻÖÝżüéwć'\LďůÜCF†ŐĹUŠ‹ ]¦[ýEÁ<řyÉP‹ÓkŻľ$'sm�ÄđŇŹyÉ«?ýHđ˘ 9©�%g]GÉ5ůuŐ¨©®VEoźţĚ0}ŮżźF®˛ý‚G„Lµ¨÷H}}˝±nV¨§G´[šŞŢ"p3Ž×�-H‘zzś:iöa vĚ©Ż«“< Ü”•5I&©;ocCŁş*űK~AžT”—™s¶ę=hƬ9ň¨*�Ń:(ÚĄX‡„„·Ŕ;˙Ď!'Ž5ń "4/$Q­Ă�ÂÂű ŢŰoţUŢ{çoҡŕk—\oŇ˝}Ľĺ~Ť�  2[¸&{Ň“ŮákßŢťÚŻ™6c–ŢK3Ť2Ź�=(áżűÍ˙“¤”1&wou:ŐŻ]W<ââL™…Egě×eß^\—ŮöşÎśÎ7Vk\ăo÷_•Ýŕ̸D··Ű,ŃöĚÜ!  pJŔQémoďpšçrDťP~ÇŹK3ŐôŰňk[Ç·cĐßą9&’3ćô:DyF�ĄäBůőDéµĘ�Ą×’“9'Ě‹Ťő»ŻŰÄÄ�ďřq‡2Oé‹>ĽDÁýóÂ,)=W"eÎKşş[[Š`�ľLY‚Ą>đŇ ĄRZzÎlC5b©Ąřö5ż) _ą9ÇěąóňlřŘXŰ‹˘«ţŔ…ŇUşŐßE×^o”ĽÉQQKjŐÚTpú”™‡�ŞŻ|uŤ<|pżýĺßĘÇ- p†ŕ˙ëWžz¶[“&é<_(ľľ~¶Ű#,‹–457Y»öíui†â;>s’ţßo4Ç-ËđuG~ň‹_3nĚP¬KJŠÄ_]vŕ â(­Ş€[ŇÖŢjívŰbЇŁX ś—x™Ăř?l«U…şVž*tŔÍÓ~Xe#Ň2–jĐé ›ÖŻ1Ě©E?FŤŠ“ÉŮÓD:mą{«Ó*«ż[W<|»ĆkŰŰ.*°­:°ç(P€‚ľTçý­|bÖdó�{ű/~öĽcVî“ � � 8!ĄwtB¬”–•Kě¨'9.Âs¸ĽÜWäâQĎöbb˘4‡k•´VŤh ¤_ĘŻÍâëĺ«ÚŽg­r“ K=ýÔWM +ÇŕV8-X�±<ţřňâŻe/ĘﱣÇLŔ+űA7;Ž# ťťjířČ�¸ĄgŐÖśłúşÚnIM:÷ ÖMKđâ×ékţľ”ŤĽíí_;:.¶Í]ÜĄ[í€ buM•qˇ<¤Š-^Ž-yů/‹9,DK—Ţ,‹—Ü oĽöŠqM·ňp;< „GDkď�L§OźaÜź‡˘Ő×WeŞ"�ełQç(C06_]�ÓÔîÚzه$«uSpŻ� Ţ=ĺđáýrű˛»ŚU´UÝ„á’|Hc@ŕNŤ˙3‡î5sáQ÷·ţéő,ÂĺďÜÇĺć›—™yÂpĄnŃ{ĐB ž5]ç `cđď̵~ů÷¶{,ĽVáýüůs·j�»~XŤ3f¬Ü÷ŔŁę¦^-?űń‚Za:¬ă� Đ×pWuZeak)ŢîŽ9¦»ÚG`/‚…aúČŚD+G�«3ŇáţŞş�cËżôu Ü•dŽĂýśB$@$@$08˘Nä\Ź[oÓ×4¸U·Ţ*@ĐŞ5k×Čýçóňňź^–ă]ÖŔ,u)|üóŹkĐĄ»%Ô*Qž=ĽLfMšląŕÝT×Ôt%pĂëMrŐEÖ‘,ťÇwľ¤ŘdĂÜ\DUEPšµś"nÍe]/Z�ű‡'Ě÷˝âŞ?îÚă®?îŇ­ňׯűÄĽL?ţÄSęFý°ĽňňoL^čŔ˘ők>1٧ľöśĚš3źŠŻo�nĂŐŁaĆŚ™&¸•Ů�®p Á­†Ú<_DQE¶NµŢÖą¨ť:‡Ä(K�× ßm[6JmÝrGŕßúgăÁ‘–zq^®u¬“9ę/„‚"jwaśś’&7Ü|»*ÇÝâXe¸Ú"x"AĂeů›ßú9s&ßĚéĹß ë?Ż‚w jý�Ü�›/â`ęÁ{ďţM°>±'ý°ÚpěŘ!łNqxX„<÷í–szŚŐi(÷ÄăęŠí®N¬Űký- NB�–ő骿_rĚыǪßÝö¨*űPvѦŻ?űm˝Žu&’µăy¨Ë»a rďîíš"ľ:š ë}ąZÁ)$@$@$@® X–U̱ÍÍ+Đw€‹ďK®Ďě[jY™kK±Źęspu.9wˇÖ^‡ćô®:dňt÷ť·ß’ó˘.«qŁüěg?7ěăŘľ˙}ůÖ·ľ-ăűŕ*'ç¨nşQ-+w ‚0Ý˙ŕă„ĺ˘ułgńR KÇük®5sl{¦ĂR‰hźóć-R×˝é&@Ě˝ź{DćÎY(uµµĆ’yZ_ ± ŇSŐzr·Î—…ěşŠŻ»ţôě_Ďß诫ţ¸K·Ę�Ĺó˝îť7ęË<sţl™ó‚µÁv°ě‰ŁEŘ*�ŰáE @ç“ÂŇkEtFď°ŹcHj2yňTÓ$¸ę;*˝8h)bPaY…ÇúE9AO(šÎäŕ�}öĂpń·ä�ĎŐŘQjA†"âľÁ˙%üëúaŰ:|;z‡üţ·żPv·*�áfÉ%xs¬Ń匬şţđ»_ë5"S#z1BlŢ´NvíŘÚ§~ zôőá~�Y�ĎĄ·LŁRżőĆ«rF—J‚¸ŞéXâ *ú>sö\şä�˝˙&Ôîy@ůţő/bâ/Ôę}ľ\çRcľłŁŕ:®]łŇ8�_łp‰Lž2]Nĺź”75Co^>ŽçsźH€H€HŔćVŚĺ„ xBý¬ĹRzŃKďWlfe7Őý(–ßŢ–,úŃŹž—oű;ň ¶5}űR<˘‚.ľîF™=g�FE­•Ő«?’9şß›ŕ%q’ľŕbY�ÝŮůÝw^ż$ë; ćŢű–‡~ÂĽ„iÄĎ?|[¬ůe}ĺrŰw™tś\[W#ożůޱv ˛ńĄ˘ŻmN,čx u.žç÷¤?¨Ă±~¸�;Š«ţ ź«ôžýݱmł‰¬şxÉŤ&řó@)XpÍłĽ ÖS>~ü°ľśŻtl÷‡!�ŢÖéµ,~C­ËŻ˝úÇ^›´V˙^ń±sţ_xţ˙�Á3«‚Ň…5x{ ­ďýĎçz6JÖËř•™�hÂÎ�ô<Ęě÷v/ HXř]R( Đ(”ŽJ;ë ”®Öŕ�[^ŢÝŞéi?¬Ŕ ć·/ţT°>0ćőÖčôËŠmĺqW'Ęř©F±ĆŇBÖúę=ŹAÉvěˇF‚vüŤşzň0‘ł5ÎBUU…Y y¬ŔVMkČă:®[ű‰± #ţ\¶)$@$@$@}#`)śP~sN~v‘ť1 uXéEwmúY§×˛÷t:tŘ)�EŹn1Ç7ż˛ĐizfdŚ—'žxRľ÷˝öřôź˙ěů÷ľ ëZb®™ÍŽ`+ĎýŹ‘Ű7ËĘŹŢëµ,ĚÇĹK#^X{“ŤţÜ®Qśť˝”âśč�QfţźĺňÜ[9źĹqOúă®îúă.ÝUů� %ç5�+ć®Ę`ÚČ$ŕÉý&"2Fď¶ą·#“{ «÷?|ăMtě3:W·Vç"c5X§·oŰd–b"%   �Á%`­ăŰ»Aopë�âbß�–śť=EßŢnpBk µ89˙äÉÜ>)˝V?ňłÄĐ VŇXß SućlYQ�­|=·�6ěNzZGzć·–%éyüJüö¤?îÚĺ®?îŇ]•7Bşş"Ä4 ��¨Őµˇ_UoLłHLLҸ ‰fą˘S§rdó†u)šç’ � � ôB–×ĎR `Zť6‹Ż÷ ·şÜ ^˙ëőEçY¤�R|5¨Kˇxy÷í×»-©sąŰŔňI€H€®<Ě3ţĂo~~ĺÂ� � � }]ˇŁm9ŁËÝăâłEň§—~}ą«aů$@$@$@$@$@$@ĂŚ€w×d_§Ýjo­7ÇýŁś¦Vżţě·>«ŞX �Ŕ `Ýc¬{Îh«$   ľĽĽĹĹŞÁő•'M×Ă㦠_ě �Ŕ'`Ýc¬{Îo@$@$@$@$0,t­zÓélM{ĎçŰ–íHźőśřú‡ŘŹs‡H€‹î-¸Ç@¬{Î`•ÍrH€H€H€H€F6Ż.CŻKWçŇĽ•RW~LBdĆ/KĚ�kĹ/0rd“cďI€…î%¸§ŕŢ‚{ î5¸çPH€H€H€H€H`Đآ:‹ËĺŚ:;ÚäčúďȤ랗Đ�‰2qń Zý,�H€,PzqŻÁ=‡B$@$@$@$@�L ĂĄâ‹ĘZ+ĺŔާ%~Üm7öV ‰ĘżŕAn‹#iÚ[¤ľ2GJóWĘůĽŹ©ôŽ´?ö—H€H€H€> 6Wg]Ç×Ep+«°ÂśËýŔ|¬cÜ’ � � � � � �Ŕ�&ĐĺęěrŽď�îG$@$@$@$@$@$@pŐŮ�ó™…H€H€H€H€H€H€†&›‡s§G®ÎCłl �ŔH$ĐÖÖ:»Í>“ � � � ô�@×»Ł]ťűʧ� � � � � � }^â…FvŇŐyč_+¶�H€H€H€H€H€H _:ÍYŢžDuîWů<‰H€H€H€H€H€H€® ›Ú+ęęL!      �aHࢫs׺FðŹě � � � � � �Ŕ�&@Wç}ůŮy     !Őy„\hv“H€H€H€H€H€F*Fu©Wžý&     �BŔ›Á­FČ•f7I€H€H€H€H€H`„`TçzáŮm     )Ú}GJOŮO čNŔK|}|ĹÇ×W—3ç’ćÝŮđ � �Ŕp%€]:;;¤˝­MÚÚŰ´›]«ś×ł_$ĐE`Č(ľ�2cć\§¦¦¦FŽ>ŕ4Í? @fΚ+­-­˛g÷v Ź�”z\ŠĎʇĽíô$ŮĽĽĽÄß?Đ<ř[ŰZ¤ł˝Cű|đŹěż öžH€F¬ięĺăm|ĄĄĄIź‡|ŽŚ«?R{é…Ž«ąc�Hpp¨,[~ŻÓÖääőŞř™óëŤâ#©©c%,,Ü®ř^wýÍ’�”"۶n”üĽ\§uđ �ŔH!`SzŰu”»­­u¤tšý$  C˝ťííҢ__?3ÜÜܤiT~ů'2\ �żíŽ!ŁřZ�[[[ĺÓO>´~šmMMu·ß®~śÎĎ“?üî—RYYnĎ–2&M&L�$9ÇŹRńµSá ŚLpo†‹•Ţ‘yýŮk  �‹đ,ôÖX·x6¶µs0ř"î /Ćâë…ÉmîWdT¤TUV9-ÇUšłÚ;:dëćőΒ̱�ČHąő¶;%9%U*TąÝżgW·Ľ ‰I˛lĹ=R^Q.yů·rߏIjÚ8“çúo•‰“łĺĎüŤřůűË 7Ţ&ă22%$$TJJÎ qEy™É‹rî×sĎ•K}}ťLŇóÖŻýTvíÜÚ­>ţ ¸ş`N/Ü›)$@$@$@bćůúůúSńĺĂ0&`,ľÜj|úźü—eçŽí˛eó¦nŔ.şVćΛ/˙ýÂóÝŽ»űÜ- ¬Ŕ–ućáGż(Ię¶ ”Ô»ÇvËëďç/qq ŞĎŰzś×ŮŮnň´a—kăCŹ|A23ł¤±±AęjkŚE–á×_ýŁäťĚ«ś��Xńńń‘¦ćFu ÷éV� \}Č sz)$@$@$@ęŕ¬ĎD/?®pĘż…áO`P\ťű›ĺ«_űşˇe)żPzŻY¸H~őËź÷‰b «ú—ýżÝÎůčĂwŤxÂÄÉFémmm‘—Őj‹VwŢużL›>«[~ÇďĽőš„ę|_¸:o\żZvďÚ&ă'Ą·ąąY~ů‹¤˛Ľ\îX~Ź,¸f±Lź1Ű(ľVPzßď Ůą}‹u�[ �«�ĆÄČę*ľ€l: � �Ŕ Ŕ3q@2K�ő¨ĺyf,‚qŞą©I? |6ęUeaÝ �¨úü ‚ÔŐÖ×R~Q¤Ąô"­/ҡ®Îy§ş Ş®®4EÄÇŹ6ۢ˘B)8uŇěďß·ËĄâë¬îÄd›Ĺ¸¨č´Qz‘çČ‘�FńMKËčv ę¦ŇŰ � � � � �@7�Şôú¨ËtC}­tt×ŇnéÎ~x{«˛"^�"ŤMőβđ ó÷¨Á­c�G›ÓSů…Ą·ŻJ/zŐ˘nÍ/˙ţWN;čëgÓÓańµ¤ÉDˇł~őm ‹Ż% ¶˙lˇaˇÖ!łmiąXW·ţ F'&ʔɓ%6.Îô÷Bi©>rDJŠ‹Ą˙*‚8~:٨÷ł¦ĆĆ+ľ¬˘â7j;¬©!�ŇŃ…kźŰuę‰ă}¸G–!˙óJ÷nűqúwY]]-MMŤCžH$0Ľ ř«Ą·AăâxŞô‚ň677î§ž*ľ�2zt’Ny ’ęšJąpţ|×zÄ6ľiۧ\cöÔÖz÷jş2‰Ic”Y“”—•jDî™^çűBđ…B$@ 0mÚtź‘!;wí–M›l1 C`á‚k$÷äI} ě¨Ŕ€ ˝Ąč}ČWýZ$VGĘ;ĄCŠ Íým@…ŕ䤤d9{¶H_._„ϸŘX©×{8®Vąś} Ä»¸pá"źžÇ|u°$22ʬcOĹ÷jý+b»I`ř€{3<7ű*8ÇS×čX}Ď�9GüüütŔŻIŇ2Ą˝ŁMîß«ŇEZŽ·LΞ&‡í¶Š/¦lVi€_(ľ�úś­yłŠ/řdfN4�óXá¦gz_Ż ň;Öןó]ťÓíz]FĹ·kŠ›÷ ·rěĐ@^ýCţâSĎ8)ĄçJä�÷Ţ”ŁGJmÝr —o|럥T/pZjz·ĽÎ~TUV�Ă -Őő}#äÓU—óçK®Ó_zúY):'S˛g�<ŚÚěŚ Ź‘ŔČ#Kďřńăĺý÷ßGĎŹ3g äÜąbY±âN)UĄd –ß„Ń jUm“üS§Ěh5b ¤čŔ^ÂčŃr*Ď6ťcä‘gŹAŤĐ°°nŠoĎcđ~ĘÉ9a–ç"5  &NĘÖÁľŤýłAęęjĚáÜy uő•©Fń űXŁS2?]őˇXް±±qŇÜŇ,{÷l·gsL·a;�4Wç�ňëěŠ. ·ż±]ËYeúéâÚ�uM~ăµWäî{”¨čhIPĹuĂúO庥7[YťNŚß©KĄŹ/±±ń2sö\Łřţţ·ż�eĐję´™’’śŞîŤ˛főDzcŰfS�ßŘ‘r‡F$¸7ďÜą«›Ňk�€"Ś4ä�â Áü***ě.Zđ>9{ö¬>Č�L�ë>#aáá&˛|Ťşµ–k@>¬E Á=:sŽî××Ő©˛tÁžž–6V×5Ż”p=.Qyyą¦ě¨¨hS¦·Ź·46čH±*ń¨ßX1ŠŚr‘^ˇKÄőć–ě® �˛3JBCĂŚ5 Ş+n�U¬–ááRxć´ýî§ú“’’t�ňĽÉvWú .aŞ,j›ęëÍoëe=w}vĹŮ]¬ľ8nчH]‚–Z pTëh6®ź%˝Ő7jT¬`)@›Fż*ôúaP¸ç±šę*IMMŐAŕRă!ŕ–�౦=°°ŕ:«µ×µ˛k€Ř]›­¶sK$@W‚@HH�që…Ň �ĄîĽQúśôÖÁăήąĹţ:x8gîB ‹—šŞj`α{uây’©ٱqńfyÓ]’5'÷¸Ţź+nÂŁFĹɶ-LůxnÍś=_—=-’ÜÇĚ1ś‹U_¶mµĺ1ő ÖËk—\ože0®›gçq5ÜeMš*ŃŞ»`ŃÉÜRváĽ9ÍU[�ë+gMš"1ú\hoÓw„âBsžő…:,\,ą9Ç%>a´yđU˝ińu7Éy]’5'ç�=ݲ�OȲőďçu ˙ÄńŁvĹŮ]}V˝Ž[W}°�ś)Č—¸ř}ď��J}źČËË‘ę*[ '«¬ŚńY’��h¸Zď#‘ú®2uÚ,É9~ĬĽýݺŷż �Uö{˙ó9·§źRP/<˙Ě+ÖÜ�Őź|d?ŻđtÁ%ĺŕÂ˙ôÇ?”}ąÂć üíµ?ÉŞ•ď›—Á Ą¶?@« gĺXiÜ’ P&7m˛ „9ëm©Ţ3-Zč,ÉăcőŞPFFEw-xË@Ń…˘f)k((.>^˘"ŁĄF•¦ÖúVŁü@ą+ČĎ×§¬HrňăňUUĄž-ŞŘDGÇ�9–‹,ć@%$¨ËW­®on›ĘkĘŚ2/P€Đ†1A©’źĘŢvĚi®­©1&Ü;uî–cş• ˇ»6$čĂŠwYY™Q|cőĹ®ÎÜ—‘'X_h°n:Ä(ę:0W6Oę0ýTKymM­¶ąJ_‚˘Ě˝z�»>»âŚë⮦’_)cĆH‡.RQˇŠk€żŚNHR¸Ý<\ŐWŻJ»żö=ÄĽ4U�ą\°pô<†ęĐox XűđčŤ�ł>`]kKéE®Úl*á � \AU:�¨�˛µ:�XXX`ôč–w'-ȸqŞ\–I‰NŰ­Ów˛UyZżv•I›Ş«Á`Žpˇ>šőŢš¬AoçÎ[ Ć´5fŞ&\…10Yˇ®ÂqŞŔâ™ŰŃ1Ú®řâ^ÚĐŕ<®bddŞň|N˝Uę$9%ÍÄÁ t±¶%QŰ2yĘTٰîS·miUËíôsĚóëĚé|}7h•ôôńúló7®Î¦ýBť8vţÜ9 đ”ppEßjuąV�•Žý©Óg*żd3ÍÚ`9 ď;Ö–žÔ‡rĹĎ6m3ęÇĘ<ĺ:(ĺA†1(°eÓ:ÇbĚłJůhm_Qái“†¶éű‹mşk·úţĂkČX|űÚöžŠŞ'ç÷YŔ9xA˘� �Ŕ@dgO•™3gÚ‹Ř»wŻ:tĐţ»·ť’âłúđMTËf˛t¨ĘoĄŢ“»BŚĽFEĆ�i–rR§OXöBBUI¬�UłĚ̆bńóń3Á˛,ĹÇüa�­ĚhUBKíŁßP6ŁŐ"‹9Sx°BĐ–4„Ŕ"hSný»)ĺ&Q%şjFžĂĂ#»őĘljjš9_(Ç ě:*ľ°ŽÂ˛ k­«:¬‚đb7tH«ZXGëČ1”BcíŐÁ�Ţú �+ÎM Ä]¬6X[X°üąŠĺoŐFű�ÄÝuĹ5nŇ%ATůĹ€‡%ÎŽYiÖ¶7‘Ź��T ú9»˘Űó:¸jłU>·$@$p% ŘżÇ(o�Ç;aŇdł:Kˇ*IP¨Ą˘ĽB—0ÝjŐŐŐO âv¨Á,11E§ĺČńc‡Múą’ł˛xÉ :2Á”Ě•…â Ë.žKđJÂ@Ł··ŹNC‰�ÓjÁěM ŁĐUg !!a®Vç];¶š€‘đ Ęž6C=oÂ1Ví˛-đę‰S%;O-Ä'N1ĺáج9óÍ~ĎŻµĂŞŚAäü®r¬�äEťII)’«ĺĺž8jNÇĽŕ9sŻQ…<Ĺ t÷Ą>ĄÖĎłE6 u…zdíŢ˝ÍÔ‰gO–*Â�c�}K0gąNY'Ä'Ú_X±/ś/U÷ö‹‰­ü}ŮvyĐuúöĺ$ć% �‘BŠcśşAYĂžýFšĄ\ZJ.”_O•^”Ź,Ď÷R¸ĂFDDHŞÝĂöT]|!F¸î&řŰ–s�•Ą:ý× Ń`9…»p€Žř ¬]­¸Ž ˘%¶őU±ŐK xB w(_–4¨k#ĘŽÖhĂĂÄU ĽˇŽm@äj(úŽR­sX™ˇ ŁĚg=§Ł×wuXĺ8¶VdÔë§Ă||UůŐýŢúŚh˘®8c&Ž'}°Ú�-^®đ˛Ż/O&Ł^Śľăš»«O[ęXTźö{cŕëgł ăĄÂ\‡öŽ‹îí®ÚlťĂ- � \I°˘nßşŃxqBAŠ×Á㙳ćJ‘*­öí¶7íB—+1”u „Ű2÷s+2~#š0žPrai„’=j”xçz›©•płť0qŠQŚ˝TńĹ€¬5ČŠó{ ő>Féűş.+,žĹ�]ĎřŢÚ‚üh+bY‚~µµ·Y?ű´ŤPK0<¨"#˘”ŮĽnç†ęsÓNűZ_¸¨şâi)ľx¶¤B˝ĎĐŽPUš_¤ĂK7ulşy/BŮx~;rČ:µß[ €CĽµ—ý.„'’ �Ŕp%€%‹ćÎťc”°ž}„b†4ä±ĘďÇ䑥×:ÇÚB™„E·@G�áÎëŁÍRŮ•CďÓöşŹĽ-:ú ÝŘôtIJN҇„.‡¤ŠLM×CŐ*»·m»şŕş+î‚«D’.ń¤J’%�zŽ÷E(\ ř: ¬ĽXiĺ�ɰ6bZd+5G´ÎýĹüß*­Ţ;˝ žqž�»¶ř¨’ q\Z�§=źźžÔ…xKÁz;V=p Đ¶čývlz¦9ĺŚZŽ!MM ¦,¬pJWB€”ę2¨'M1ű�ĄŚąk ˘LŁŹięú‹Aqx‡ejđ§ţ ęĂ@lZú8ńňö2óŁSRÇęs7IvîŘf"M÷µ>w}čO[1ő Ŕ EEgúSÄ%çtą:«-ąËäI  ĐąŻĹćă Ĺşuk¤üž9sĆ·BtIëV\Żó—¬ą­¨ăňŁŃz!XJ©D8YnVpŤ†ë–k�‚�‡hWb•‰ůÄđt…5đ\I‰Ş–b‡]“©—ˇŘşkC1˘8jdF´‚˘ VVÇęP”áŞήGG×őčv]š|V]Żł&NŇůĐu&ŕę¸Ř*đZ¶üîÎC‡/ÎSxÁ,�H€.�ťűZ^v1ČCj 1Š^Îuv–=š;w®|řáßť%{t _ťŁ +˛ŁÎú|zŁ@IDATńdŰś_/§A-Ěůj=¶,ŁŽçąÚG™�ľş7;+Ó“6`„ó|ú??I9őŁźŽíu×gWśQN_ű`ă‚kŰŞ/*Oö®Fą«.Ňť:Űń\gÇűŘŰ>ÚŽż/ëzŁśńăÍ2S–BŽsݵą·ňyśHŕę%0ĎF<ٱ\h_ďńđBAĐżę*Ď—¬±… 3Aau쏠NÄ`°Úţ”1Xç¸k ˘'Łź  Öľ«Ő··ľ÷§>w}đ´Ý(gÉu7JŽFžÎ;isÉöôÜŢňegO‘•}ĐtqrOo9yśH€H W�ě<ĄC±\›{«ČRVśĄ›ó[/ş¬:Ëăě�«2ťĺwuĚ“68čpUVoižÔŃŰąÖqw}v—Ţ×>ŘÚl›çlµÁqë®>¸˘÷gÇzćéůn^)))ĆP©®n°@GhdOĸtڏŤóܵągŮüM$@ ТKÓ«rVŻĘďĄ}Î(AůÂ98·/b 鹢ě¬l(éCEܵŢ\�)�ŽíJúSź»>¸ŞĎJĂĽj�Ţr=·Ň˛íruÖĺŚ,˙ş�”ĆsI€H€H€†,XŚ‹ŠŠÔ%>Ö|ĽÔ‚ĺ–Ξ=cŚ6dφ‘ \07VăK©ő6LŐ ›G‘»†wvbzK“~tú eD@�±¤ä PÖ$'ŽđÚ˝Ž0»Ľ¦Ľ|uh×ń8÷I€H`XŔ-#Ž®ŁĂşĂě t€…¤¸ÇzÍ„C$@ć™8ęž«MMőćCŞ$ĐWpĺްîÓľžćQ~ËâëíQnf" �aBnś^>Ľő “ËÉn� � �ž‰x6RH`ř°ŤěxÓŐyř^böŚHŕRX{·ç˛—ćâ  đLěą.ýČč9{9Rt94h(Q � Ś mÔǵÖoőš]%  �K Ú;ž‰x6RH`¸°\ť9Çw¸^aö‹H ťş$@“řűj(ŰĂľçr1˝śČĂ$@$@$pŐ€÷fXzˇôâ™č|±Ů«ľ«ě t°Ů|‡UTçé3ćHŤ.*}*ďŇ5ź°&ÔËî–ýűvInÎńAý3HJJ‘”Ô´^Ë-}ĺ%É;y©ĺy(ăĆ‚ŃX8šB$@$@$@$@$@Ă�€÷�Q|aýüňSĎČÚµ«$&&V&Mž*żűÍ˙ÓĹ‘áâ!ş`ňą÷ľ‡ĺÓO>”cG ňßxÓí2!k˘‡Ę±Ł‡dő' HqvU&Ňžyö;˛k×v™8iŠDEĹH�ş(Żüř=ąő¶;%5-]ŞŞ+eĂÚOŚBmĺßľ}ł¤¦¦KšZť«««eËćuräЧŁaaárëí+$Q]ł}Ô2ť꤬úř}ih¨—e+î•ěi3$0 HžýćweĎž˛eă:·ŕć={îIOĎ�şú:9rä lÝ´ŢiýËŞ–[     ¸ŇÚGŚĹב4ć—]8/“¦L5Á®�6qR¶äś8j¬ź±qńŞĽÍ–uëVÉşŐ«Ě©HűüOÉÔ©3eĎîíŽĹy´ď®Ě˝{wšrrµž·Ţř‹Ů/ ŁGËź˙řkinj–ŞĘ ąëže••]0yĘĘJĺŐ?ýÎěĂBűť˙ůżeţ5‹ĺdî sĚújÓŰ·nŇóJíJmhh¨Ś;Îd9®mXmG©5|ۛ۶»6çäĚť^ýéG˛qýjSÎIUŘKΞµŞĺ–H€H€H€H€H€®8�©ř‚úáĂĺš…K–ĐŃIÉ#ôžą ‰‰Éf™“ääTyŕˇĎw]$/łŰő»oOË,(Čł\S]eeAé…”–ž3ŰPuY¶_¸C[ŇŢŢ.� Ż–ŕžŇÖÖ*PT33'Ęě9ó%ZÜ””Tiiµąz÷ĚŹßîÚŚ€‚Ó§äún•TU óOĺĘá�űĄ˘ĽĚYq ťŞäB`…őô´Z_á> A0©1©cuđE «IđđË]™(ż?2F[9 ]Uę’M=%##˸%żüŇ‹öĺśzôIÍÖ]Ńöv q×f__?3&M6­_#ë×|"áęöü”Î�žĄeĚ!¦� � � � � �Ŕ•%`,ľşŽŻ—Í…÷Ę6fđj‡ň…@TŽRx¦@jjj™ýC÷ĘŠ;ď3űű÷7htâ3…ĆÚ[ŇÓů2gÎť÷;S^~é×ýZ¦Č]™§4Âr$55M®]r˝®]|Ň•ŠŠŽ–őë>ऍJť —1Aššš$3k’deMŃő†›ěyëęj%"şÖ^ů†÷ŢtiĽ®C‹ŹŁĽńú+rP•Üžrp˙^ąĺÖ&ŕÓŮł…Ý’ß}ó5ąçľ‡ä–[–›ă,µrĺn•^W®Ę®Ętnńí4‘š»5LŔýÚ’}Ş°Ď™»Đô¬6m\{Iđ-´ ˝vëRI�׌hŐµµ5‚HÎIÉ)VQĆ•Ë<-[~ŻěÖČÎďľóş¸j3NܰţSYpÍmĂ5f©§ăÇËšŐ+íer‡H€H€H€H€H€®4ŻeËďî'iiéNËC¦Mźé4ŤI€H€H€H€H€H€\¸hńuťďŞKőóó“»îy@|}ýÔöxµÂfŚĎP<™H€H€H€H€H€HŕJ°Íňő ®Î‘Q‘RUY唆«4g'ž–¤¤ąé–;äăßu–Ĺ —[o_ˇîĎ)âăă+ů§NĘŞŹß—††zY¶â^Éž6C‚äŮo~WöěŮ![7o�gžýެ]»JÜoĘHIM“»ďyP^űëËrˇôĽIß·o—dMś"Ń1Łä?đŻâŞž^ça‚«˛aMF{wěآîß“%!!QNś’M×HńŮ"S�@€Ürër7nĽ4·´ČÁ{%Y­ĺeeĄ˛ćÓŹĹ*Ł·>—ž+qŰ?wu !¨çĆ›n×vN”ŕŕP9vô�¬ţä#ill0íÄőś=w�¤§gH]}ť9rP¶nZoŇřE$@$@$@$@$@˝č nĄ®ÎC@žü—eá˘k/i Ž!­/RzľD¶nŮ ®Y¬[ç®Í��AňČçż$é™râřQý‘ÉŮÓŤ˛ŚşNžĚ‘â⳪ 6Ë®ťŰ¤H•iH\ühUĚBĚ>ľüýü%..A·6ë2Ňo¸ńvijj–Ű6‹»zěőcÇ“˛Ńž›oY.mmmrčŕ>;.CV8¸oß÷Ŕc—îśśă†ÁÂkŻ3J{Xh¸˝E®úěI<©ăž{”E×.•ŠĘJ9ŞJ픩ÓĺéŻ='~ţţ¦?öE«Jďž=;ÍĂm·Ý©.ŕłěmä � � � � � 8#`ą:ű…9ľżýÍ‹ňŐŻ}Ý´sËćMf Ą÷š…‹äWżüąłö»<ká8UjWÜużüęç/Ř-‡ÖIP·oÝd,›–R*cÇŽ3YŽ«ĹÖĹQ1±˛M•h¬’žČńc‡ĺµW˙h˛ÂÝÚU=ž”×[w}°Î+P+ď«ţ˝ůYS[-·¨"%AÁÁ2Q-ÓëÔ‚ ë.¤¸¸H}ěKfß“/wmHHLr[Gl\Ľ*±łeÝşU˛nő*SmΉŁňů'ž’©SgŞR~L"""eő§ÉĆő«MúÉ“'¤äěYOšČ<$@$@$@$@$@#šŔru®«­5 ®ĄüâşXJ/Ňú*mm­ňîŰŻË—ľňŚÜľěny덿t+éP¨23'Ęě9óŐ-9VRRRĄĄµĄ[ľţü8u*×~Úĺ¬ÇÓ˛Oć··çt~žx«/aá6«.¬Ý–śĚ9!ÍÍÍÖO·[wm=:ɔ᪎ÄÄdÓ¦ääTyŕˇĎwŐée¶ŁbceĎîíRpú”\Ă­’ŞůĘ®ćĺenŰÇ $@$@$@$@$@$ Ľ†„Ĺ—˘§ň Ko”^벞-:c¬�7߼Ě(ąÖql#"#ĺ+O?'AAÁrZ•Şs%6·ć1cŇłąÝ‡éJ«guxZv{{›ýôÎŽ.w=k-¤˝˝ĂlńŐŮŮ!úq%Ž}v×Oęđęň¶oďĐşőcÉ�ý»ĄTçMC^ţĂ‹7ěôôLYşôfYĽäyăµWŚ{¶•ź[     č…@§o/ Wä°Ąü˘ň�(˝Vă7®[m"3ߡV_/‡őŠ32˛ŚűěË/˝(ą:żňĐŁOę÷EĹÇĽ}.*¶ťŞ”5h°Ąxť7kIÂčDk×éÖÓzśžěćŕ@ËÎ=qLZ[[%kŇd)QgćDéügKÜőŮ]<©–÷ööv Ľ•'[6®3UĂ­|LęX�…îâ�Ř´~Ť¬_󉄫ŰóS:˙w–Zę-ÉV›ą%     �Ľ‡”â‹Ć †ÂëŘÉ÷Őĺů©Ż}Óń�TVV�ßă2&h Ş&ÉĚš$YYS¤©ąÉžŻ®®V"Â#eţ5×Jž»BÔćUIJ§Í”ÚÚńÓŔVsć]cĎďlÇ“zśťçx Š‚=9JᙏúŕxNĎý¦¦Fµ„•Ĺ‹oTe7XeŠř˛¬´V~W}v×?OęhĐ(Íg Ôµ}‰qy>]�/sć,0ëżüŇŻĄM•â'ľđUŮ˝k›Ű*‰‰)&8ŹB$@$@$@$@$@PWça$°×vv7Új« ňÉʿ˝�fÜ•x*/G©íFŮB4a(˛�äś”śb§WŰI“§Ę˛ĺ÷ĘnŤěüî;ŻË–ÍëĚň=Xz§J•çŰ6ÉŇëoGb{şăI=ČßŮłŃ]… +ău-a|ĺŤ×_´Ď]pŽcŮ]őŔĄň×W^’Ĺ×ݨóśč2Aµ˛zőGFé4‰]_®úěI˙<©ăÝ7_“{î{ČŢBµ`»rĺfŔż7¬˙TŁt/‘9sŻ1�ĘŽ?,kVŻD…H€H€H€H€H€Üh÷Z¶üîÎC‡/8rwĆpJÇŇD°¨bŽooa¬‹pǵs[««śŻ;lĺqÜzRŹcţľě¤ěxuŐnjlÔľTš*ŃŻçţǿȎí›eĺGďuk†«>»jC_ę�ŠŽŃ5•}¤ěBi·şńuDFEÉy];ŘńZ\’‘†5��Č)/;7¬űČΑ � � � �©ŮSäăŹ>hVßľâih¨|\ImMő%É}Qzq˛'ő\R‰‡ú[6̇ů‚”j×őqwHc}�Lť1KçÔúKuĎę]őą·6ôµŽĘŠňžŐÚ÷V‡=wH€H€H€H€H€HŕfĹź­ř^Âd€ŐôőżţQnĽůY´h©řęśĺÂ3ůf¨ |5ňYÔ1íd$@C“Ŕť‹ăĄaďm´E�”ÂX � � �ŔUFŔĚ…í â{•]¶ÁlnńŮ"ů“�şśňYÔq9ŰϲI€H€H€H€H€®fĆâ«‹¨:,ós5w‡m'      �îŚĹ·ÝŰŠtÜ=‘żH€H`xHHH”Ĥ1N;‡`vic3$0ŕâzÖN3öń ż€Ě�9Wbăúxf߲‡…iűÓŇ%kâ]˙z¬®EîÓ·>ŁÜiiă¬ű"­mrľĽAň‹ŞĄĽŞ©ŰŁ Ńë÷ż ő ­})’yI€H€H€Fş:Ź ‹Í®’ � zxÚŘqRQ^¦ëx7tC’>nĽÄÇŹ–"]+{0%0(XF'&™ĺ¸.”^ž¨Ô™&ɸŚLuâńŇČçm¨ÎOĆŽË�#‡j¤ôÁ™ăš2&MŁ«Gˡ{űŤÇKŤ&gO“Çö‹łŕ�Î Î?[#[öžŚ×úűzKcs»ÄÇËus’ur_�R|8·LÂCü%=8BĘ*ĺ|E�Lă¬8#  QŚ«ł†đĄ� �Ŕ"p¶čŚŚM/‰ÉÉrJ×ď¶ÖŃ8µČ–ŞbÚ¦Šă`JMuĄ|şęCimmĚbíeA!ź™%E…gäŘŃCZO«Z—ădň”i2uÚ YżîSépX’Í~bwÂŐJ;jT\ĎXv(±vɬÉń2%#Ú(ö5ő-˛ngˇ|Ľą@îľ!Cüý|ä‘eY& µUT7Ë©3UT|†žg“ � �Ŕ0!`\ť5¸çř“ Ęn� xB F—(«ŞŞ¸<;*ľříďď/g‹ M1°LNČš¤ dĽĐ5¤‹ĺÄńŁFyEÚµK®×Ľg$N-Ä!!!˛ú“Ź$22ZRRÓ$&f”477Ë9='?/WołŢ˛`ábÉÍ9.%ĹEâíí-°Đ˘l?­ł¦˛JrrŹ d«ě3ůZv‚q ĆR_yy9ö5·{ö3cü©Đ<öď¶'•ž‡eŮKfĎY cRRĄ ŕԀꝜ=]Őj KňâënR%ű´äź:é”�«ţŮčáζýĹ’–.Ůă/ZoaŮ]:7EŢ[›'Ĺę$).TVm9- ¦%J‰ţ>|˛\š[ÚŚb<5s”‡51 � � �Ŕđ$`,ľŢĂłsě � ôNŕ\I±QRCCĂí™G'›5·-Wä©ÓgŞe8CŤ Wĺ…×^'Xźn”×¶Ö69­J*dćěy­J/,Żőuµ2qb¶$%Űć#?kČÔéłLŮPh Ďś–°�p™;o�*Á&y'Lś¬VÚŁ(GŹ%STńt&��A"ĹŞ„÷”Ňó%ňŃßß6J/ŇRoů…RUĚkÔŢ®m.P%ĽÂTçŚ�»zz¶ł·ßťR®ÖŰ)•^+/”ßÇ–O4J/Žť+kPe·]ăC%1.ÄX�§O�•�đ@ënI€H€H€F �N3YJĽ}»E� Ře �‘G–Ú TiJJ‘'Ž�ź®c+°˛B #-7÷„äž8jŽA!ž3÷ Ś•b[<îśěݳݤC ¬�}R­·� FY¬2űÖĹÄÄ9ĄÜăÇ›ĂçJÎĘâ%7čüâ»Ĺą˘Ľ\vďŢfŇ›šš4`Őd-?Ř̶ĘÂÖ UWWçxř’ý�Ö ëųX Vëvţ©\S>¬ÓGžÖcNtóŐŘds9 ňłçÄ|ކƋ®č�ţŢpqÖâQ‘ÁR®.ŇŁcCěçq‡H€H€H`dđR8]ťGćőgŻI`DhnnRĄô‚ÄŹmßÄÄdńŃqŔ˘.«iDd¤™/%3gÍëĆ*4$Ôţ»˘ĽÔľßÔÔhÜŤ1×6:z””—_�bukn¨Ż3îËVĆđ”­J±C�+yŞS qLL¬]ń-+»XvEE™iO¨*ÍŤŤÝrutÍGöq˛a°ëµú�­#Oëq<ż·}oŰ�Ş˝ÝĚÍ1ŮΖÖÉúťEöS˛Ő•yÖ¤ţÍ;Ć`…H€H€H`čŔ{Ö@…ß�äů$@W%(Ą3fΖ(ŤRś0:ÉĚűµ˘ #22ËätvvŘűW|¶PjU‘µä˘:f;˛sűfI×ČĘ1ѱ’1>SŁ,Ź—}{w«’{1Ş˛U6ć;J[[[·e”::Ú/&÷¬čbŠÔÖÖÇť�Č(�kłŁ`®í�1iRSScg¤ V˝Žő86ĎÓţ9žßŰ~@€Ź$¨¨i”Đ›Ő7-1\ž¸k’9ĺťŐ'%XŁ:÷Wă!Úßşy � � �ŔgJ@ýÔÜę3%ÎĘH€†�s%E:'´YƤ¦ëĽÜ)QwcKŞŁŁC-¸e޸î2źýűöČ™Óů˝.u„¨ĐQjéÍS÷číŰ6Ęúµ«u™ť6 ,•fk¶VŮŽkú"x"&×7ÔvËëÉŹíCyŮIŐ Z=×?~˘.4]Ô˛9XőzuYa{kŰ`Ő�ňˇDŹ)»Ź”J‡B8JemłT×µHl”s«­^> � � � č _óŃŮ˙ˇrB$ �«�ŰŇs%’¬Ši§ZW‹»˘9ŁKP&«*+%-}ś@Ń«Ôů¶)©cuŢo’ěܱÍ(š=»Ą׹óJaa�Q�#Â#%@Ú–Öî–]”]YQaÖ؅űEݮǦgšâÎh «ţHnÎ1™ŁÁ±®Y´DňóóLű±ě˘0WV– ćĂr=Đza-R%:ml†® \*őÖo«Ý}í߼ů×÷í�öXEtŰΚ'ďŻĎ“Öź’Ůş¤Q�Îá˝PŃ(»ť“¤řĺDń…Šńy xÉŕVÝ€ň � � Ś0çřöEaśv7~t˘¤ĄĄ L™ZRŽ> /§Ý_^ťžČ�—�:m–™»�@>˝IhX�Yo/¸źbÉ—�ňž·`‘Q ŘPH 7r…5pKuľ/ćý: 1D'ÎĘšl764ȱcGTá;Ż–ČKâ———k/”e¸c-]X?쪧ěŮ˝]&MžŞó‡çš$Ô}đŔ^űrFć ��Ó˛vÂőÚ™Ŕ2˝{׍2=QËÍ6Y ŘĂŠ}üČa»»ö@ëŻř„Ńú˙uމě|čŕ~gÍOë�+vHh�řřÚ"e;+,H•ŘKĆÉŽĂçdő¶BíK§(črPiŃ2#+ÖŮ)&Şó�„PYąĄ@fO‰wš‡I€H€H€F Űű“ײĺww:|d¤ôÚm?oĽů]ńâ`^tß{ďMł§Ű�ˇ�gżů])÷Ýw^ďvÜú‘1>Kxčó&Z-ÖWEDXpű­Wĺp×Kő¬ŮóŐ*7FŢ{çoÖi.·^ú2ýď?ř±Ľ˙޲sű—y™xu��ŚQk+Ö§ýě$88T׿ő28žÔŠ%‰‚��ó…ˇ|ş’` ”ĺ­îĽl5Xwg¬ Ś2ç';–?Đz –ÝÁč–‡‚2묬;wWZ±ĽQ»~ü|/xpěźµŹü¸vďmĽ8ÇÚJă–H€H€H`dČΞ"+?ú �®Î×K•\·ô&c­XőńĆ”©Ö�ű|LţűG?°%ҡ:î*�Ű—ÝĄë¤6Čo^ü© „=O<ů”Ü~ÇÝvĹ&(ȸ.łň¤ţVU«ő㉠âó`KSsŁŕăJZݧAˇ<©§]×öT Äâă©ô%ݧe2 � � �ŔŐE ËŐY×ď`p+ű•[ĽäF9­ÁkŢyë5ű±cGkÄ#Ź~Á¬áąuËyćŮďČľ}»t]Í)g”üçţŐX*o˝}…YăÓÇÇW׹<)«>~_ęuŽ ·9g×®í2qŇŤ"ű˙Ů»(É®ó0Đ·zzrÎ3Č� "�`3 F1“"E‘¦MËi-SµZ[«cźuXŻV:^[¶lËV°$fRŚ˘D0‚$H� @"�4“óLOîé˝˙«zÝ5=13÷}—lTuŐ«÷Ţ˙ÝW=ő×M‹ÓăyŢWoúRzýŢšÎÍÝŞ·ďŘ–ľ{ó×Ó#?4éíădŁĄtĽă?çÁ ;w&˛ŹŰnűAş$°bĹiÍ㏦[ľ÷­´öé§Ş=LË­ăŻ{ý›Ó\”' :�~>Ę�˝îăƲ/wŢńăÁYowlß–>űWź¨Lz{§V1]yőµŐ¤=ŃzüӟޖzłotýÓ?ţĂt0'Ę™gť“Ţů‹żśľńőżÍkŁŮ�!üo|Íóy_–[âć¤űď»;}óë_9jY�îórź @€�Ú]ť{rVWB4ÇC¬=ąpѢôł<�ëđň@N”ţŐď|8ýŕ–oWO-[ľ2˝úĆ7¦}űö§Ű~řýÝţŢ÷�P-cňŕ÷Ą¸·šIő5Żű…Á]UŻÉÉ׎íŰsrű`şćÚëÓ‡~ă·ÓÂ<›lŚí‹őBßü–w>Łí'|ü®sú˙3íĎ ôöm[ÓŰŢńžęx1ˇV”‰n3µNäřÝç\ ë?‘ĚNdŹçVŢO|ôĎŞWîܵ#˝.'ÂŃ=yĆĚ™é˛ÜţÝď|ŁjMŤ bbť÷˝˙WşŽrôÝđxű»Ţ›“ţwĺÖâ·¤Ç$w5ż-Ý{w{ŇśřŇáüó/LK/M?Ě­íu ëçä v~–[ŢŁ\öś+ó$B÷ ¶°×ŰEý]}ÍőéŰßţZúö7żV=Ű}ŕďýătŐU×UÝÚëmÝ pj ›{jŐ‡ł!@€§Ł@§«s^ÎHWçŞţä •˘Äň#uąâޫӫ^őúú×tONĆ>ý©ŹTż?účęÁÇŁeňˇĽśČĹ_–®Ţ s÷çĄéěłĎÍË�´»áÖFRW—ť;¶§ią…3’Ţ(7¶'왓»,׉ďD·ß´qĂ„Žß}ÎőyÔ·ŤááŐÔ/É­«Źäńv=iiNâçĚ™[­ąů`N*ëňđęó8GÔĎշ۶nI˙ëŹ˙kî"~V•Ľ^~ĹŐ齿ü÷ŞîĎźË]žG+÷ŢsWzŃ /«ZŰWć×.Z´8Ýô•/µůgśUťăYgť[M˘ŐŢ =FpÉŇ‘g„=j' @€ @ŕ´č¬ăŰęŐŐą]‘Ňkb\đË^ţętĺUצ•y­Ňů‹„űď˝ű¨MëegúóěşÝłĆţüg·ç/Ěôz� @€$0Ôâ[PPÇJĚ>úH^köů/|qş=Ź%ݵkçŕî®ĚăGcâ§őëÖ>Ö}ç /Móóáżüó˙Q­AĎ˝÷ýĚ˙J »·?Ţ÷ŹÇńŹu›·´»g_”[˝cíŇ(çĺ.ĘłfÎ5ÜX?ő­yŚđđ˝#şÇÄ`ŃrćŃßô•/U-‡ŃZ8Ľ<š[ŠoĎKEr3GkqĚF|ćYgß´ë÷ÜŰ|„µ»»ăvmśďŽľ}ڵťüńŹÜűDcč>çĂťó8\íěŻ>ý±ô®wż/Ohő÷«™źc2­§ź~2ÎüČ�u~ČÝŹ?úRMęőĆ7ľ­ňާžxâńô•żů|ŠçŁD·äXľ(’ÜhŤ˙â>]=~×ĎOŠ.Ëő’JŐť˙ÔçúĹĎ~*˝ăß[MÄOE+ňWżúeIo7–ű'D ¦PX^öŠÓe—_™ě]é§?ą úđ}Ĺ Ň �)pö˘Yé•—,M+ćÍżąď`Ú¶ç`Úť[µvç.…�¶ŔśÜbNî ±pÖÔ´döÔtíYó«źu;÷Ąď<´9=ąu* pĽĚę|Ľ$íçä Ěš=']wÝóŇö<“óçżö·Ďę— '/jG:•¦öö¤7\ľ"]ş|NušŰöLw>µ3=şyOÚąW˛{*םs;5ćÍěMç/™•ßyiĺĽé˝×ź•ذ+Ýtď†tđP{ŘË©q¦Î‚NS�ĽŽŻBŕ4�îÎđ÷4;k§[ŞŔĽ™SÓ;Ż=3-ť3-í=x8Ýúضt÷Ú]yů¬‘Ƕ—ę .Ç"_ýěÉťé®§wĄ+Ď�›nXµ0‘47-š=-}ţεů ¤‰-Ťw,çŕµ P´Ŕ35Żŕ pü"éýŔ Ď©’Ţő»¤Źüř©ôóÜŇ+é=~ĆöÔ,�xďÄ{(ŢKńžZ–'‡űŔ ÎIń^S @€Ŕ1´Žž˘řöćĄhŠŔ´Ü˝9ZzgMť’Vç.Íźąc]ęŰßžx­)â$p˘â˝ď©xoĹqď¸öŚď9…ýµŐiÓ¶˝E�<“xÂf_^_úD•ăą˙xŻĹ{.–:Š÷ B€&.Pµřću|Ío5q3[ Đx�‹—µ×ęŤuzGš˝9ş7><ůd"^3Ń®Ń7ľć iÎÜyéŔÁijooš:uZÚ·şĺ»ßLßýö7žµ:zîő/Lgť}NúŇ>ó¬ťC}ŕďßą.ýѧÝ9yIśm;ö¦KĎ_–~óצ…ó¦gŻţô×7?”ÎX2;-}îĚôđŰÓýy)Ş7ż|U˝‹Óęö™Äóý×ď§÷ˇ—¦KóŇA'˘ĎýÇ{-ŢsŻşhqş(żßÜw"NŮ>  @ H�ŞĹ7On50±®xEŠ“�ń»uâűhžmöŮ,?˙Ůíé źűTu ËWž‘^ţňÓk^ű yĽńˇô�ď}űY9µĺ+V¦ /:>“KK­ŮžţóG’Ţ˙ć+Ň›_±*őäz[»©/ý§ŹÜ™ţőÝ–ţŕźż,͚ٛ>ö˙ľ1EťFy,Ż-çkâ[Z<#ŐĽç"ńŤ÷ŕ7ďßh<üHH#@€�QňäVíôGÝ P ,Ę-‡±´Ę–=Óν“oŐ=QŚÖ­MźůÔGŇĚYłÓK^úĘtŰ­ßO‡LSrëó«sëđĹ—\V=÷ôSO¦›żőŐ´~íÓGťJ+/ëţˇ_ű?Ňm·ý ]réĺiĹŠ3ŇšÇM·|ď[iíÓOUŰŹµż7˝ĺťéĘ«ŻM3¦ĎLżöáßN?ýémG%ŕő1îĽó'éŇË®H‹/Iż˙»˙ş:îÍ7-Ýs×ĎŞăś}îyéíďřĄô©OţeÚ´qøç5<�˙ő…{Ó‹®=;˝ő•ç>uĆŇŮé7˙îµé7˙ćôó‡6Ąkň8íó?~śţÁ;.O÷<Ľ%ýÍwK;ví©ă·˝jču�;čÜąĺ§kÓwo*íÎÝnŻĽhIz÷ë.Ę­îCنľ—ź˙^~~Ű®ýéŞ<ő_sQ•dÇË#é~çŤć×?ťž\ż+=˙ĘĺéíŻş }áćGŇOîŮ�Î\6;˝÷Ť—¤% fTG›ěöĎ$žíů<˙ěó÷V]ÂŻČńĽ=Çľ`îôN´)ŤOl4Ţó�;Ęw>ýŐŐ驍»ÓŻżďęÔ;Ą'íÎďŁďd«ţ|}ó«^pvş:›ŤVâ=·ąď@Z’߇ń^ܲ{˙h›zśt Tůnţ׺ómw×3î @€Ŕs¦·'ÂßŇwp„gźý‡n˙É­iîśyiĹĘ•ŐÉĽýťďM/~É+sűXşý'?¬’ŮżűÁśfĺy¤˛lůĘôÚ×˝9'͇ŇÝwÝ™V]pazËŰß=¸éXű{řá‡ŇÚśPď?°?ýäÇ?LO=ąfđuÝw⯾ńŤißľýé¶~żz*ë>§iąëö˛e+Ň´©Sź뼺÷ßźg~ä‰-éM/[ŐýpueîÖüÉßs•ôĆ÷®ŢP%_W_˛$EŇ7wÎĚ*1=ďŚ<ËŘĺsß|8ýńg–ž{ĹňôĆ—ś—îČ­Ž˙×˙Ńŕ–őő‡Óźtž˙Ĺ×^”ît[úť˙rk:ńŐďŃ <‘2ÚyíŘľí�—oÝŮn\şpćŕă1�Óćműź—[ çĺ5�ë ńgÍOŹĺ °®Ě]iG*űô§Ď}ýÁô+ďĽ*˝:·LFą"oű…o=’b­č2ýůoŢźţ·w_—^vý™Őó×ä„úżó•*™[uf;™~ůőçTÉulđhłz˙c›ÓoĽďÚę;čĺ‹g¦ßů�ďĺ/úÓôÜł Ęd·Ź×L$žŘ.ĘóŻ83}đ­—U÷ĎY97ýöúnőe@oţ^|¬xVćô±žŻăHéă_y0ÝrÇ“éßč%©®—ţţ�ôÄÚmé·>ř‚ôÜç,«~®»lYZ¶h¨ŢŞ“öźÝylv”ú˝8ěiż @€�ŞßĂ˝ńŹŇ©VV,ťš.[ŐţÇďńuŇcO}`é\gĎš’˙ńžťÇ¶ ¤[~zô,«#˝ćT|l"q,YŘ›.żpfš7»7mÝy(=ôÄľ´ióńm}Zşdjz׫¦ŤŰ¦Ď}ăČ•§˘›s"p˛fv’‘˝yýŢS±LźŃ˙Ţ´rĺYU2öŕ�÷ žęĆőëŇćMŇů\4jâűđę·_óŘ#©''¦K—.OłgĎyFűÜYםG]ÝőŰÄîŽv^Ăß©ąő2Ę�®:şóţÍé?ţĺŹô¶/IďË]Š'SÖĺÉ”ęO‘śŐeŃüéWrWé(ŹŻÝ™[ĘŞîÍőó3r�‹W-Mw>°)Ő‰ŕ5—uĺ]‘g'Ţł/’ćö+–çőˇŁÄcuâ;ŮíŰ{šř_tőŠÁŤĎ=cnuóööL×cĹç<ÖóuĽůĄr üćôoţŮKŽHj§LiUÝŃ˙óGoO/˝ţÜtýĺËŇUąŐ}ÚÔ±ż©ß{ő{qđäÝ!@€ŁtňÝž<ąŐ©—ř>ďň9éyĎi'ľ­Ů?nâ;kVOzĺősň·ä§wâ;^ĎżjNzíóç¦iÓ:ź’rĺľö…sÓŹîîK7ݲc”Ş>ňá7Ľt~Zľ¨7Ý~ßžtĎęö‡›áŹ-™ß›[ zÓ¬CÇ9r/~#ĐL�˝ą%.ĘĚ©íÖ¸SMᬳϭNi}NpW­ş°şß·űČ/cöçąóF_6©?OŽU—�<“n]ęI &»żúő“ąŤd{xíĽ†o-ą‘”E"ş|q;‘ŚäîŻţă[ŞM?üű·Tł:ÝDźŃiőľ}ýOéĚŹsVLMÓs÷¬í»űÓ÷îÜť|´ť�v??szOZ›»$ýG;Ó†Mí®ÉăĹŃŤ}^NV§ç–Ţř÷ŮomK;r7ç(ŃĘ˝hţ”47·zGë�ď˙…ĹŮĄťđF˛|aN–8|Ôcßą}WşńóŇŽĎźqó„=†Çsç�{Ó‹Żž]ťă}¦=öđ9ą›öůçĚ\?aúŘşĽöh6«MŞ ü‡Ŕ)*PŹí]<»=éŇłyšóć/HW\uMţ»0#-]¶<]űÜçĺe{zŇ×núëę´öôíNkÖ<–^üŇW¤;¶§Ý»w¦—ĽěŐŐs?É]M¶Ld»sëňüy Ň oxiz$Ov32O¤<ž»T_yőui×®ťŐšÄĎ{ÁŘ­~ăíó˝o¸8ýöÜ’Ç«ţ ˝ďM—ä.¶łň:˝;ŇG˙ćŢtÍe+ÓĹçťř.š?˝szď#[óßĆŁ[Ä#ˇ~îĺg¤OŢôP5†vfNr?ú7¤őą tÝýůWť™ţňŻďOţ;צ9ł¦Văău/ż®=ćwĽó>žĎŹĎxÇŠó+žńžŻ÷-Ö±nňo}đů)Öő˝ôĽEŐÄ^ŰóXě_ýżż‘ţÎ[®Hݏţ¬jbŻÍŰvçmĂę@ýňŁne×(ő{ń¨ <@€Fhĺ,h¨ŰŃČŰśÜGݏ°Ýrůřúýigßá*ń˝äĽéG$ľď~Í´,ŹuŤ2mj+Ýřü9GśäćśÜ^qţŚtÉą3߸ uډť6­'˝őĺ ŇŠśônßŐ_%“Ń ş<'Ňż÷çëóŻVµíĽśLöžŰĘÉá@Ő˝¸ęN}p`đ\Ţű†EéÜśôF—Żľ}‡ÓŮ˧¦÷ܸ }ü«yLÓľT?ă âů‹ÎžžÎYľ$}ňkŰŇŁyÜňxqtőÔúé`>vśŰ/޸0ýô�ľÜ˛°?}ů;Ű»7ó�‡"kÎ˙ŹăˇűsKËHŹŐń×WFýűxďyí´tÁP˝ĽáEsS´TGrey»ýŽW.¨ęlĂ–ö¸¶«r<No^Ncň-PŐNý‡ŔIŘš—RéË]7çßór—ÖgkIŁxG]”×ËŤźĂ‡űÓ¶­[ŇŁ9ŃüĆ׿’[s7Š|ňc˙+˝áŢ–ŢűËŻzlWN~?˙ŮOڏśQý˘�şk~ žŤx` ýéxű‹µ…źsůUéMo~gş=ĎěüĹ/|şŢí�·?řţ·ÓÜąóŇŤŻycÚľmkžíů–ôĘW˝.uwµ뼆ď<­ßýő—ć$ôľô»ňŁl4�'łšž^őÂóŇ{ňňC#•� ůyy˘§óßżź“±+GÚ$ýÓ÷\™ţđ“wĄ˙ŢÍU—Ýç^~fúŤÜ]·.żúKW奉­–L Ć3—/H˙öW_2¸śQ˝]}˝žGęů\˙í­·«o'łýDâ©öŰu°áç2^<ă=_źwÜ>çüEŐşĘ˙í“wä/S_Q}yđkď»>×Ń=é/żxwZ˛hNzÍ‹VĄ_xéŞô˝»ě~éŕýxĎĹRFńŚ÷˘B€&"Đă;ĐúßďŽ�GžjMťČ‹Oä6˙ü�+rw§žôĄďíL[¶L˙-‹« 4ţđӛҶÜ*{íĺłŇŰsŇÉägoŢžgË<�Ţž“©‹Ďť^%X˙ţO×UÝx?ôžĄ©•[˙[~Ýžśtţćű—U-Śü…ö‡Â� +ĘľŮî–ö;ż˛2wSkĄOä¤4>°Ľďő «çżřÝŐXŘ÷Ľna•¸>°f_úÄßnMW]2+˝ëŐíóřdîĽú±śčľqQşěĽUËđ#OíĎ ę‚Ô·÷púÓ|Ě-ŰĄwć„ýę‹f¦űň¶ćÄxĽ8ŞčúĎ‹Żť›^ýĽ9U2ȉđc9ţ[ďę«ĆA_yń¬1Źů©›¶¦ż˙öĄéĽ•SÓ7Ľ+Ż5Ů®÷áŹE«{Ä­Ý˙écRý{s4Źër˝ĽmX˝Ľ-×Ë%]őňÜ+椷ľl^^˙ňpú/źh·˙ÂËćWőöőî¬Zźă �Ń˝ő·Ó–ÍëG{ú¤<ţšçäÖŐ<đÍ«·¤ź=9Ô¤>řĘ3WĺäscN¶†ĆĘÖĎŤuŰ“§\X´xYZ÷ôccmöŚž‹őrcÜěćMí÷ÝEä‡,@IDAT3ÚI׋ĆŰ_Ś!Žâčj=™2Á‚ÜMűČ/ó&óúŘö?|čŇ#^łĚç1cZűKą#žá—Ř>&^ú­?šä+6ëŢoLś-™1óńHĺŔÁĂů;Ć�Üč٦3‘xFЎű±ńâďůî} ż_®ěÉëóF y]†Ű׏_söĽôŞ<“öťyöíoÜ7±ŢőkÝ @€@s®şňĘtÓWţz_ţ$�łĽS¤\}٬*é=-¤{c9‡žŞ»íü9Sňş‹łŞnĆ˶˙qÜ�[uďëŚq˝;ŹIŤÄ·.[s‚üä¦CU‚Ýj÷î?\ug^›»?ť[NŁÜš?Ü\—»PđíKŇüŮSޤ7źť»/ďŢÓţ°¶+·8ßqo_<śyę@•řÎË3HGY™[/٬ßz¨Jzăţ'ż˛µjMŽÖ…Wż°˝tEt{WNxŁDWĺ( çöćëńă¨6îúĎrwď‡rKńµ—ĚĚ-ÇÓÓ™K{«ÄňśÜműŁůŘg,kďs´cvíęÝËcéHńäú‰Ä·.1;÷Á�s«:ţđű—§ő[¦§6ĺÄýÎ>IoŤäö”X˝qw•ř^{ÖĽt×Ó»ŞÖÄî“Ţł{Gž?‘xĆ;ŢxńŚ÷üXűŹ/ş“ŢѶŤ×â=%Ţ� �¸@;ßÍł:Oü%'zËËWµ»9Ç�Ű_Î-¬Ý%ĆŮ~űÇ©jµŤÇóŞ�e_§+íŕůÎ�Źď«ßUą sŚcŤňĐű«ŰhÁŚ.ÉŃ 7ƱnΉňÔüagîě#ż˝ŹîŔuéľ_?·‘¤w—úXőG§h=Î=Š«˛wß@zrßÁ´mס ÇQď{eNjä„9ZKżţ�v+SŚ©}în˝`î”Ń=?ĂífäÉźÎ;ŁÝ:ř@NZ÷ĺnĽQbĚŃŐąŐ6ZXc}ßč~eE^’gţĽŢj’§‹rR<ĽÜůŔžôŠçÎÉ“(őćV—”ús‚úłüX”Ks˘Iďęśôo¶äÖŢžôá÷-ľ‹18żöĹWÍÎ㔦¦X·/·żć†yUWçŐOîω÷ţtC~~îňö'źkŹ_]•'“ŠYŞ7n=�ćç$6ĘxqÔ'6oČűß““ç˙ž'ŠÚ™öŤy˛¬�ťä|ZîrwOnaë�őľâ6&–^Fzlř6ŁýľeG§^ň¸ésó— Oo8PMbŐ˝ý…ŮýěĺӪ٨ď¸wKŐ…ú—^ł -Éă‚/Îë6?ôX{R°î׸OŕT�/–ľóĐćôާ»Ú›^uýÜjmÝ_}ϲĽ ġ<űeî™÷­¬1iWL�5Ö1c_;űÂ`jzn¶ŚnÝ_ţζŁ‹}M¶Üy˙žtCž):Îé�y\öŢ<®zfîŞŢ]˘E=&‹±É‘ČÇóŃ /ĆkoČÝž§‹Ŕ“[÷¤6ěJ—.ź›ŢzŐŠô™;Öĺ^(ť®§K…žçhăCŹ5ܵßc=ŻgúúÓ%žŢÜťč­W-Ołr÷ňxĎĹ{O!@€“hcÚ30Á1h“Űů䷮ǂ>ž—·‰D¨»<š—ŠrAN–˘eőË·lO1Ţ4ĺXîč'÷Ťüá}]-�Ń[—;rËďëV]›źź'eб·1Ëp”zöŇę~őČ‘˙é>łŹäÖâ×ěŻ’Ń r+g| ýĂ»űŇŹďjŹ?ŠÖäh˝^ś[4ŻĽ`Fžˇ4Ą;rűÝśřN&Ž8�Řţă_Ý–Öm>TťoĚ IoĚJýµĽDŇyśs”±ŽĎß~ßŢŞŐ<’ĐËóĚף=V=1ě?ݱ×OŐŹEď?ËKĹ’O‘ôĆŇNßĚqv—;s=ýčžľjńkň$_ćőšźÎă®oúá®Á噺·wźŔ©,đŐ{7¤Ť»÷§s§Ą7\ľ4÷˘8ň‹žSůÜť�ÓA ŢSoxÎ’ü›^˝×â=§ @€�É ÔůJžŐů§yVçÓs˘�łs"Ľ!w÷­ÇŐNai^ľ(†ŕ­ď¬«;Ů××ŰÇ2=3rëetďž´Ç6ń|t#®'ÖŞ_WßN6ŽX«7Ząc–ëş»ŢW};Ţ1ăůÝůË�H¨ë2ŇcősăÝFwďËňŘéXO¸^ëřE×Ě©şgÇ8ę˙ď#CX˘UüĚÜ:ç]¶8fuî>×yyĚá^pNš5mJZżë@úŇ]ëŹčöÜ˝­űL\ ş7GKo$˝{ňňEąí‰Ľ|�žA´%ÔťYť÷ö¶Z§o+Ĺ“k‡ZqëŔ&s»)ʵ=eĂ8‰óxĎO6ŽčĆýČc'Śăs¤çGzl˘>3óRPŻ}ÁÜjvěËň$e}űúÓYK§U/_Ó™I»ŢW|9°¦ÓŠ_?ć–Ŕé(Äăů;®=Łú€ţ�çź™n}l{ş{íŃł=źŽń9g'[ foމ¬^śÇôÎĚÝ›ŁWĹçď\+é=Ůáx(H łŽo«÷Téę\�m#CŮś——úüw¶§—_7'-Í]»—ä±ĎňROOäVđoç5�Ą Dňűń?™»;/ŻĆüľúâĹ麼Ţčťy¶çG7ďÉŘ'·žo©Nâ"0–ŔĽ™˝éü%łŞ%‹böć(1¦÷¦Ü˝ůŕ!ăçDzóŚ-Đ™Üjŕ”™ŐyěÓőěé ăŚë±Ć§Ăů:GÇK >�˙őĎץ;íHݏxIµÔŃ«.Zśâgs_žXoĎÁÔ—»kîŢ' >^ćösú ̙ޛ˘Kó˘YSÓ’ŮíBU,ł7›ČęôŻc @ŕÔhŹňÍ]ť'¶Îä©qŇ΂§®@|P˙Xîú|îâŮé˘esŇĹů'>Đw¨?uĎŢ™xö⋡‡6îN«óO¬ÓkɢgŻ.™Ą t&·jiń-­fĹC€Ŕł*ŘßÜWý|óţ ińśéiö´Ţj¬ą3üÉ}V+ÇÁO)�]ąDL\ŐwŕPÚ’ÇňJvO©ęq2(F`°«ó@2v¦�Z§”@|�߼kÚśŽm"ľS*('C€�gA >¸v&¨yŽî�śŢí6ß<ĄsëôŽĂŮ @ A‹.NóćÍ?"âůó¤ĺËWńŘ´iÓÓŠ+óRj˝�·Glpšţ˛lŮň4}z{ ňá!ôôôS¬S§N«^?|ż§ËďS§MK«VťźfĚ�y̧|¬–Ç|…ě`Ľ:™¬ó”ĽcĽŻ;­…(Ćpł�ůśsÎK_rYZ´hń‘űídű ?×a§ăWN-�Vď�ß˙wéî{î=µNËŮ @`�ů ĆţĐ3ÂKŠ{hJ^âeŢüyiçÎ�±-ZĽ8MωîöíŰŇţýíÖĺ9sć¤Ůłg§M›6¦ ¦­[·¦ÔúO®5Á‚´gO_Žs0üÁ;­Vë�bííí­^ż~ýşÁ}žNw÷÷Wő߸˝Ü]|I‰Ő3‰çX-O'·y®ăŐÉdť{rďç ëןČÓ~V÷=Ü,ľŘëťÚ›Ö¬y,ĂßSĂĎuĚ“ó$϶ŔŔé»�ďłMçřxúvďÉ-ž3S|Ŕ‹ŇŰ;µJz§YłfžQ$˝}}}�żçś0ż.Ź7ž='Ňíá%ö7+?7sÖ¬#žŠ–Ą™3gUŻ|ýz ëţ˘Ą:ε:öÇ‹–ťHî#ţ±ĘhçQż¦>Îxű©·~[ď?ZY‡»ÖĎÍ쪗xý�iOŐ:[ÇŹĎČ­ŮÝő8™m÷N۶mK‡JŃâ?-·G‹T»ţF˙çľ6¨ĎcxڱʍűşÄö±m]Z­žŁŽ1Ň>‡ď'^?uęÔ#Z¨Ă,ę=ęw´ŇmçQŮg»áqĆńâ'ĘĐkZCď�üčđ2‘ăŹc,ʉÖÉxďĎáçůL~źŚAűoÁĐß“±Ž7Ü#¶©^ÇţűŇľ†˘.»Íâş›•˙&,®}zíŕ‰EKE<)ŔˇC‡Ň“O>‘j>ăĚłŞm[©'íŮ»;­[».ow0'Ř=éÜsĎK}ą•ućĚśÜĺ˙ĚŹ?±fMőüŕşîDBz,ű[¸pQŐu;Ž?eĘÔ´wďžôäkÇ÷-\´(ť‘ăď?Ô_%ľ6®OŰsÜĂËXçŰÎť7/ť±ňĚ~ńauďŢ˝Ăw1ćďGîżUy>őÔ“Ő‡ôúąĂ‡W­®{÷ěIëÖiş'?6}Ćô4ĄgJÚÝ·;ĹěNńĹCO^maßľ˝U Wí?™m}ô‘Ü*¸ ‘ŃNNW¬X‘žzę©[ËFłî|îěąiᢅé‘G®^´dIZ”ë(Ž×Đě|ś3Ď<;­^ý`5AŐhűŚ„véŇĄéáŐĺíÚă­˘ŰîˇÜR˝níÓiĹň•)Zô#ɉÄcóćŤiË–ÍݧRÝŻM˘ľfĚ�‘“ý­iKn Śë´>§Řpqî_F¬[·vB×ńDŹ?ŹVnqŻĎk¤:9ÜŐ:?Úűó€ě×Ć3)µŰXďĺ±®ŰŃŽ_˛-]şlÔz­÷ŻíďËhu×R÷uĽníÚ4#żGF»Ž6ç.cůÔç2Öűr×®üw5_ÓqŤNÉő÷ôÓOĺżµ»G|OEýŐőď…‰ě,˙ŃŚ=N€Ŕq0«óqa´ś$�ř0IЬüa3ßřĐIÓîľ]U˛‰i{ŚgOęËIrݦôVç„#�sr‚0wŢÜ´eóţ*á[ąňŚŞĄpó¦MUKŰYgť•–.[V%#uX‘®~衜ĵҪóĎOsçέŽúůú6ČcŮ_Ľ>><>őÔŐ‡ÍH€Î?˙Âś Îü@Ź=ňđęś°ö§ř`Ľ,źëŽÜÍ»»ŚwŃ’çąy˦찹r8çěs»w1ćýz˙;wěHó‡í(ńú…9ŮÚĽySŰ w/Ź®ćŃ"zöŮgWçą6'xu‰n›O>ą&'»łókĎI;r÷őH ŁĄvŐŞ ŞÖßř’!Ęd¶Ťí7nÜP%Ü­\_‘,ŚTƲîîRş+_[Ë–/ŻZ‘«÷9�Ťqíµź[}1R%cÔß®ßňüLuÍćD"ę`Ö¬v]G‹ű‚… Ó“ąŢűňsń@téŤëµN’‡ÇP›Äq»[ć†o×ýűh×ńdŽ?Źîó©NęçG{vź{ÜŹ/ü±áOę÷Ń ękzŰ8×íđ�íÚą3×ëŠëupźą'ÂX_F«Ëş›~÷uď‰Ń®Ł±|Ďeśř˘N^˝şşĆcî„Hę#ń«ţÂd˘ű͸«ß 8îýŁ÷}:îDzC8ń!lvŐz›Ş›}»űŞd8’ŢHŁĺ(Z=büY]¶mŰžâĂu$‹»ŁEŁÓývfîZ8%w]Ý—“éH6¦ĺ®…űöí«Z”ë×ĆíöüúHvÚŻo'&ÝĎ×÷ŹuqÎŃ]%÷9)Š$87…V-{ő1âüă<˘ěر=Gť“ýÜÝ]Ć;ŹŘľ''¤;r\Q⸻víěŢĹ�÷ëýG‹dĽ6~Öä„dSN8ëç˘Űq”pŰ–óHřâ\ëĎW_dä/.˘4Ăř=ĆiG"0-×e]&łmýšńn'bű8xŕ@Ú_µěήZbŁkŚź=§Ývv®Ł¨“(cíłşvň—5s;“łE‹{˙Á´'wÉŹdŕpvZ�“ݸ6÷íÝW}ń2ZŇÇŠs�kz2e´ëx2ÇźŚÇDĎm´÷çXŻŹdý¬ü…I÷Ot»íńz_ŁŚwÝŽ¶ß¨×]Ăëőp»^ë}Ž÷÷e2u9ÖuTÇ8Ňm}.ă˝/cţ„xĎF‰yb2˝�}Ľ2ŃýŹć?Ţţ=O€Ŕ± čę|ě†ö@€�“*“;EKDŚiŚd‘8DÝ#ąŠ.ĄU×Ů®łŠnĂu‰më¬nqZ’» v—HşKtŹ®KĽ>’ĺ‘ʱî/ZGĎ8ăĚ*áŤÖ›}ű÷ĺŘr’;”/ćÄph‚›8—Řnfţpz kĆ«ńÎ#şGڇşâ¬')®áŹĹţc©8v]ŞĄVr[=űî2Źs łŢś4ÖÝ[ă܇J~u~m]ânWČĂĽ±·­÷1ŢíD¬ë}Dď‚ř"bŚë,’�ó­ŞĆËF"_ĆDoźńEĹ™ą[ýú\ĎóćÎĎ­Ü;Ű]ŘsŔO<ţxZş|Yn?'ďi ·–oĘÉí–jż#ýçHż‘¶8ú±Ń®ăH°'sü‰z}#?Ň}­D\őűsä­ŰŹFr¶'éŐ]g·VŽeÄÇ;Žf0‘ëv¤ýĆnwŻ×ízď}XźűdërÔë¨Ţá·‰/^Öýwŕ@ţ[ďËyHÂîÝCďőv?á÷ýhţ#íÓc/�ę_ÔQ>ąŻcŘw�ř`”Ł ^´ÎÖÉ[_n9ś3gnŐ:]l'R"‰‰ňTÇ-YQĆę^Zm0ĆŽusrŇ1¬yüŃ*éŤsąđ‹Ž8âĚ™3rKoűˇř0]�v%ćńĚxçă†#Ť k˘»n”á{UŽňźŘ$Ď1IMýA9ş`ÇůVĎĺ}Ç$<µéŚ1f0µÇMçq�'Żt§ĎGu"Öő+vďÚťÇő¶gUʤ/bŽ–·ö5¸w0Éoź1áZ$9í–ÝYiÆˇ´D×ď'ž¨ .Č]Řs÷ęť91îN ëó~‰křF>XźąU?Ţ-“9ţD=F>öču2ňö#?ţ[·ŤüĹŔHŹ×IčČ{kżgâ=1ÚuĆ#í7ö7Z˝Ž÷>í\Ć{|´ăŤőş‰ľ/c.�řű%z†ä·ô_pů•ÔĐ'ş˙ˇW¸G€ŔÉČ˙@äNI'ó_ß“›# @ p�ÝąĄ'ş4÷íi·´E¸Ńĺ9>´E«ÜľNB;Ct©Ťn¬1f.fŽä-ÖĚŚńŻĎ¤ëţú;] cR č¶ą$'÷Ă[—Ł‹l´vÇůHĽ˘ĺ5&Śé.ăťG´ÔFÜKňDM±źy¦ěůyżÝ%şđ¶»Zw?Úľ_ďÉ’8żxýŚtîyçU-îősK—,­b�äzQžjç®vWćŁ÷vb‰ÄhVľ"9©q"Öő™Ĺő-ŐŃ› ľ`‰­Ľń%ELT—ńöÉS$ł1†<şă×_ĸô‹.ş$wźÎłŽçşĎ “*‘LG˛˛xń’jĚtÔËhë=Ź´ăɢÏ5^ť ßţdţ~,×mÔkŚQ^Żő>Ź×ß—Úc´ë¨~~¤Űú\Ć{_ĆŘňxĎTň߇ýą×I´üF«ţ&ş˙‘ÎÍcśh�ę Ç<ąU|•Ą @€Ŕi% oĚÉn]öíß[ußÝłgo•üÖŹ}[}ó9řđ“y†ćeyćßóVťź[ÍÚ]Y7mśX‹ńŕNşîËţöćvWN#ůŽž"ÎŞ;q×)ďŘľ˝ę­{‡ňѧźzş[:ĽEkĽó�Iź˘Űm$\ŃŞ¸#7#GâT—Ĺ‹–TÉYÝŤ·~Ľľ]›[ÉŁ[vĽţđ@îîą«ĎDZWśqFşđ˘‹«–Čľ:OL$ľ¸žĎ9çÜę ·řr,ŢçőűkĽú›ČţÇ;GĎ p"Ş?$ý­7˝ůíwßsď‰8‚} @ภĚ_°8ĎŔ;vňp\ذťE GL04ŮńvŁ1Ëţb†ÔřZ6ZXF+±˙ş›÷hŰÄăăťGŚ“)Áşđ˘‹Ň†u뫉{ĆÜ~}ŚŽ/ †—Řw´”/ÓáűźČďa“NŤV&b=ÚkG{|¬}Ćz®gźuvzřᇎŞßžÜÚ“ŽM¤^G:v´lÇ ŔĎÔűXŹ?Ň9ŤôŘxu2ŇkNćc'âşď}8ŮřĆşŽĆŰ×HńĹgń%V,‰Câ÷Ń®Ăńęo¤ýŹwNž'@ŕÄ \uĺ馯|yĎČł“ś¸ăÚ3ś˘Ł}Č{¦§{,ű+Q«Ďg˘űo»‘’ŢřĐ;Ą§7/ ŐëWs¤Ű‘^_o7Öső6'úv<Ëńž&ç7Ú>—ć.őór+ôÖÜş;Ň—‘°Ć˙ži‰–ą�ĂG1Ńýëń'|ś1ľ��č>Näv'âşď}8™xĆ»ŽĆŰ×xńĹu4ÖůŽv}×Ço˙ővn 8YUç^‰ďÉňv8m˘őö‰Ľ¬ŇxpO›€N�m™0Ąš©y{î®®x&'ę:jŹŢá=˙L*ĹkśňŐ˘‡Ťń=ĺ+Ę  @€ŔÉ�–żŻŞ?�há]ż~íńۡ=5RŕD]Gńž_»öéFš š@ůU‹ŻYťËŻh @€ @ ™ť ęň,#LĆŃLQ @€ @€@Iő’~y±ĽŞé·¤ŘÄB€ @€´řR¸C€ @€ äĹň´řXŻB"@€ @€�Îß� @€ @ D�ˇ1ľ&·*±~ĹD€ @€©ZÇ7ińu) @€ @€@‘í´7YÇ·ČÚ @€¤NW缎ŻÉ­\ @€ P¤@ÝŐŮß"«WP @€hş@§«ł1ľMżÄO€ @€�R:]ťórFş:—ZÇâ"@€ @€@Ł:ëř¶z’®ÎŤľO€ @€�R†Z|KŤP\ @€ Đp�zr+]ť~!ź @€e t&·Ę]ť @€ P ŔPWgc| ¬^! @€ @€@Jş:»  @€ @ |ł:—_Ç"$@€ @€@Łň:ľ  @€(W Gâ[n劌 @€R2«ł«€ @€Šč×â[tý Ž @€$ľ® @€(T qM‘řZ˝Â"@€ @€�jßĂ_W @€*PµřćÉ­ZŐťB� @€4W jńÍ“[ Twšë r @€(Z@W碫Wp @€h˛@ŐĂąWâŰäk@ě @€(Z žÜĘߢ«Yp @€h®@ŐâkVçć^"'@€ @€@Ů©jńí1ąUŮő,: @€4V •Ú«YΨ±—€Ŕ  @€ Đ -ľÍ¨gQ @€ @ ©--ľM­zq @€ @ p�ÎßË^ŃÂ#@€ @€@S†ĆřTł\5ŐAÜ @€ P¬@;ßŐâ[l Ś @€Íč4óZÇ·Ů—�č  @€ P®@§«s^Ç·Ő^רÜPEF€ @€Í¨»:ăŰĚú5 @€ ¨g´2Ć·đŠ @€š*Đéęś—3ŇŐą©×€¸  @€ P´@gßVOŇŐąčŠ @€š*0ÔâŰTq @€ @€@áőäVş:^ŃÂ#@€ @€@3:“[ĺ®Î  @€(P`¨«ł1ľVŻ� @€ @ %]ť] @€ Pľ€YťËŻc @€ @ Ńy_… @€”+Đ#ń-·rEF€ @€)™ŐŮU@€ @€E ôkń-ş~G€ @€_× @€*ĐŠ¸¦H| ­^a @€ @€@µŽďa‰Ż+� @€ ¨Z|óäV­ęNˇA ‹ @€š+PµřćÉ­Ş;Íu9 @€- «sŃŐ+8 @€4Y ęáÜ+ńmň5 v @€-POneŚoŃŐ,8 @€4W jń5«ss/‘ @€ @ l��Tµřö�ÜŞěz @€+ĐJíUŚ,gÔŘK@ŕ @€h†€ßfÔł(  @€ ĐT�–ߦV˝¸  @€ P¸@gŚď€ĺŚ Żhá @€ @ ©Cc|ŞY®šę n @€(V ťďjń-¶‚F€ @€�f tšy­ăŰěË@ô @€(W ÓŐ9ŻăŰjŻkTn¨"#@€ @€�f Ô]ťŤńmfý‹š @€… Ô3Ză[xE Ź @€MčtuÎËéęÜÔk@Ü @€(Z łŽo«'éę\tE Ž @€MjńmŞ€¸  @€ @ p�zr+]ť Żhá @€ @ ™ťÉ­rWg… @€(0ÔŐŮß«WH @€ �’®Î® @€(_Ŕ¬Îĺ×±  @€ Đh�ĽŽŻB€ @€Ęč‘ř–[ą"#@€ @€�”Ěęě* @€ @€�˘úµř]ż‚#@€ @€‰Żk€ @€ hE\S$ľ…Vݰ @€ @ ZÇ÷°Äו@€ @€… T-ľyr«Vu§Đ …E€ @€Í¨Z|óäVŐťć:�ś @€ŠĐŐąčę @€š,Pőpî•ř6ů; @€Š¨'·2Ć·čj @€š+PµřšŐąą€Č  @€ P¶Ŕ@ŞZ|{LnUv=‹Ž @€ŤhĄö*F–3jě% p @€4C@‹o3ęY” @€hŞ@K‹oS«^Ü @€(\ 3ĆwŔrF…W´đ @€ ĐT�ˇ1ľŐ,WMu7 @€+ĐÎwµř[Á#@€ @€@ł:ÍĽÖńmöe z @€”+Đéęś×ńmµ×5*7T‘ @€ @€@3ę®ÎĆř6łţEM€ @€�Âę­Śń-Ľ˘…G€ @€�¦ tş:çĺŚtunę5 n @€-ĐYÇ·Ő“tu.ş˘G€ @€�¦ µř6U@Ü @€ P¸@=ą•®Î…W´đ @€ ĐL�ÎäVą«łB€ @€ ęęlŚo�Ő+$ @€HIWgW @€”/`VçňëX„ @€h´@^ÇW!@€ @€ĺ ôH|Ë­\‘ @€ @€@Jfuv @€ @€@ŃýZ|‹®_Á @€ @€€Ä×5@€ @€… ´"®)ßB«WX @€ P­ă{XâëJ @€ @€�BŞß<ąU«şSh�Â"@€ @€�ć T-ľyr«�ęNsDN€ @€E čę\tő Ž @€M¨z8÷J|›| �ť @€E Ô“[ă[t5 Ž @€Í¨Z|ÍęÜÜ @ä @€([` U-ľ=&·*»žEG€ @€�Ć ´R{#Ë5ö8 @€š! Ĺ·ő,J @€4U ĄĹ·©U/n @€.Đă;`9ŁÂ+Zx @€hŞŔĐß�j–«¦:�› @€Šhç»Z|‹­`� @€ @ Ůťf^ëř6ű2= @€ĘčtuÎëř¶Úë•ŞČ @€ @ ™uWgc|›Y˙˘&@€ @€@áőŚVĆř^ŃÂ#@€ @€@S:]ťórFş:7ő7 @€Šč¬ăŰęIş:]Ń‚#@€ @€@S†Z|›* n @€(\ žÜJWçÂ+Zx @€h¦@gr«ÜŐY!@€ @€ uu6Ć·Ŕę @€¤¤«ł«€ @€Ę0«sůu,B @€4Z Żă« @€ @€�rz$ľĺV®Č @€ @ %ł:»  @€ @ h�~-ľEׯŕ @€ @@âë @€ @€�BZ׉oˇŐ+, @€¨Öń=,ńu% @€ @€@ˇU‹ožÜŞUÝ)4Ha @€ @€@sŞß<ąŐ@u§ą"'@€ @€�˘tu.şzG€ @€�& T=ś{%ľMľÄN€ @€�˘ęÉ­Śń-şšG€ @€�ć T-ľfunî r @€”-0�Şß“[•]Ϣ#@€ @€@cZ©˝Š‘ĺŚ{ ś @€ÍĐâŰŚz% @€š*ĐŇâŰÔŞ7 @€ čŚń°śQá-< @€4U`hŚď@5ËUSÄM€ @€Ĺ ´ó]-ľĹV°Ŕ @€ Đl�N3Żu|›}�ž @€ĺ tş:çu|[íuŤĘ Ud @€ ĐL�ş«ł1ľÍ¬Q @€ @ p�zF+c| Żhá @€ @ ©ť®Îy9#]ť›z �› @€E tÖńmő$]ť‹®hÁ @€ @ ©C-ľM7 @€.POnĄ«sá-< @€4S 3ąUîę¬ @€ @€�†ş:ă[`ő ‰ @€RŇŐŮU@€ @€ĺ �Őąü:! @€-�×ńU @€ @€@ą=ßr+Wd @€ �’Yť] @€ P´@żߢëWp @€ ńu  @€ @€@ˇ­�kŠÄ·Đę @€Tëř–řş @€ @ P�ŞĹ7Onժ @€ @ ąU‹ožÜj şÓ\‘ @€ @€@Ńş:]˝‚#@€ @€@“Şνß&_b'@€ @€@ŃőäVĆř]Í‚#@€ @€@sŞ_ł:7÷9 @€ĘHU‹oŹÉ­Ę®gŃ @€ @ ±­Ô^ĹČrFŤ˝N€ @€�fhńmF=‹’ @€MhińmjŐ‹› @€… tĆřXΨđŠ @€š*04Ćw šĺŞ©â&@€ @€�bÚů®ßb+X` @€h¶@§™×:ľÍľ DO€ @€�r:]ťó:ľ­öşFĺ†*2 @€h¦@ÝŐŮßfÖż¨  @€ P¸@=Ł•1ľ…W´đ @€ ĐT�NW缜‘®ÎM˝ÄM€ @€�˘:ëř¶z’®ÎEW´ŕ @€ ĐT�ˇߦ �› @€ ¨'·ŇŐąđŠ @€š)Đ™Ü*wuV @€ @€@�C]ťŤń-°z…D€ @€)éęě* @€ @€�ňĚę\~‹� @€ŤČëř* @€ @ \�‰oą•+2 @€Hɬή @€(Z _‹oŃő+8 @€�řş @€ @ P�VÄ5Eâ[hő ‹ @€Şu|K|]  @€ P¨@Őâ›'·jUw RX @€ Đ\�ŞĹ7On5PÝi®�Č  @€ @ h]ť‹®^Á @€ @ ÉUç^‰o“ݱ @€ @ h�zr+c|‹®fÁ @€ @ ąU‹ŻYť›{�ś @€e ¤ŞĹ·ÇäVe׳č @€ ĐX�VjŻbd9ŁĆ^'@€ @€@3´ř6ŁžEI€ @€�¦ ´´ř6µęĹM€ @€�Â:c|,gTxE Ź @€Mă;PÍrŐTq @€ @€@±í|W‹o±,0 @€4[ ÓĚkßf_˘'@€ @€@ąť®ÎyßV{]ŁrC @€4S îęlŚo3ë_Ô @€(\ žŃĘßÂ+Zx @€hŞ@§«s^ÎHWç¦^â&@€ @€@Ńťu|[=IWç˘+Zp @€hŞŔP‹oSÄM€ @€… Ô“[éę\xE Ź @€ÍčLn•»:+ @€ @ @�ˇ®ÎĆřX˝B"@€ @€�”tuv @€ @€@ůfu.żŽEH€ @€�F äu| @€ P®@ŹÄ·ÜĘ @€¤dVgW @€-ĐŻĹ·čú @€H|] @€ P¨@+âš"ń-´z…E€ @€Ő:ľ‡%ľ® @€(T jńÍ“[µŞ;…), @€h®@Őâ›'·¨î4×Aä @€ P´€®ÎEWŻŕ @€ Đd�އsŻÄ·É×€Ř  @€ P´@=ą•1ľEWłŕ @€ Đ\�ŞĹ׬ÎÍ˝DN€ @€�˛RŐâŰcr«˛ëYt @€h¬@+µW1˛śQc/� @€ @ Z|›QϢ$@€ @€@SZZ|›Zőâ&@€ @€@áť1ľ–3*Ľ˘…G€ @€�¦ Ťń¨fąjŞ�¸  @€ @ X�vľ«Ĺ·Ř  @€š-Đi济oł/Ń @€ @ \�NW缎o«˝®QąˇŠŚ @€š)Pwu6Ć·™ő/j @€.PĎheŚoá-< @€4U ÓŐ9/g¤«sSŻq @€ @ h�Î:ľ­ž¤«sŃ-8 @€4U`¨Ĺ·©â&@€ @€�ÂęÉ­tu.Ľ˘…G€ @€�f t&·Ę]ť @€ P ŔPWgc| ¬^! @€ @€@Jş:»  @€ @ |ł:—_Ç"$@€ @€@Łň:ľ  @€(W Gâ[n劌 @€R2«ł«€ @€Šč×â[tý Ž @€$ľ® @€(T qM‘řZ˝Â"@€ @€�jßĂ_W @€*PµřćÉ­ZŐťB� @€4W jńÍ“[ Twšë r @€(Z@W碫Wp @€h˛@ŐĂąWâŰäk@ě @€(Z žÜĘߢ«Yp @€h®@ŐâkVçć^"'@€ @€@Ů©jńí1ąUŮő,: @€4V •Ú«YΨ±—€Ŕ  @€ Đ -ľÍ¨gQ @€ @ ©--ľM­zq @€ @ p�ÎßË^ŃÂ#@€ @€@S†ĆřTł\5ŐAÜ @€ P¬@;ßŐâ[l Ś @€Íč4óZÇ·Ů—�č  @€ P®@§«s^Ç·Ő^רÜPEF€ @€Í¨»:ăŰĚú5 @€ ¨g´2Ć·đŠ @€š*Đéęś—3ŇŐą©×€¸  @€ P´@gßVOŇŐąčŠ @€š*0ÔâŰTq @€ @€@áőäVş:^ŃÂ#@€ @€@3:“[ĺ®Î  @€(P`¨«ł1ľVŻ� @€ @ %]ť] @€ Pľ€YťËŻc @€ @ Ńy_… @€”+Đ#ń-·rEF€ @€)™ŐŮU@€ @€E ôkń-ş~G€ @€_× @€*ĐŠ¸¦H| ­^a @€ @€@µŽďa‰Ż+� @€ ¨Z|óäV­ęNˇA ‹ @€š+PµřćÉ­Ş;Íu9 @€- «sŃŐ+8 @€4Y ęáÜ+ńmň5 v @€-POneŚoŃŐ,8 @€4W jń5«ss/‘ @€ @ l��Tµřö�ÜŞěz @€+ĐJíUŚ,gÔŘK@ŕ @€h†€ßfÔł(  @€ ĐT�–ߦV˝¸  @€ P¸@gŚď€ĺŚ Żhá @€ @ ©Cc|ŞY®šę n @€(V ťďjń-¶‚F€ @€�f tšy­ăŰěË@ô @€(W ÓŐ9ŻăŰjŻkTn¨"#@€ @€�f Ô]ťŤńmfý‹š @€… Ô3Ză[xE Ź @€MčtuÎËéęÜÔk@Ü @€(Z łŽo«'éę\tE Ž @€MjńmŞ€¸  @€ @ p�zr+]ť Żhá @€ @ ™ťÉ­rWg… @€(0ÔŐŮß«WH @€ �’®Î® @€(_Ŕ¬Îĺ×±  @€ Đh�ĽŽŻB€ @€Ęč‘ř–[ą"#@€ @€�”Ěęě* @€ @€�˘úµř]ż‚#@€ @€‰Żk€ @€ hE\S$ľ…Vݰ @€ @ ZÇ÷°Äו@€ @€… T-ľyr«Vu§Đ …E€ @€Í¨Z|óäVŐťć:�ś @€ŠĐŐąčę @€š,Pőpî•ř6ů; @€Š¨'·2Ć·čj @€š+PµřšŐąą€Č  @€ P¶Ŕ@ŞZ|{LnUv=‹Ž @€ŤhĄö*F–3jě% p @€4C@‹o3ęY” @€hŞ@K‹oS«^Ü @€(\ 3ĆwŔrF…W´đ @€ ĐT�ˇ1ľŐ,WMu7 @€+ĐÎwµř[Á#@€ @€@ł:ÍĽÖńmöe z @€”+Đéęś×ńmµ×5*7T‘ @€ @€@3ę®ÎĆř6łţEM€ @€�Âę­Śń-Ľ˘…G€ @€�¦ tş:çĺŚtunę5 n @€-ĐYÇ·Ő“tu.ş˘G€ @€�¦ µř6U@Ü @€ P¸@=ą•®Î…W´đ @€ ĐL�ÎäVą«łB€ @€ ęęlŚo�Ő+$ @€HIWgW @€”/`VçňëX„ @€h´@^ÇW!@€ @€ĺ ôH|Ë­\‘ @€ @€@Jfuv @€ @€@ŃýZ|‹®_Á @€ @€€Ä×5@€ @€… ´"®)ßB«WX @€ P­ă{XâëJ @€ @€�BŞß<ąU«şSh�Â"@€ @€�ć T-ľyr«�ęNsDN€ @€E čę\tő Ž @€M¨z8÷J|›| �ť @€E Ô“[ă[t5 Ž @€Í¨Z|ÍęÜÜ @ä @€([` U-ľ=&·*»žEG€ @€�Ć ´R{#Ë5ö8 @€š! Ĺ·ő,J @€4U ĄĹ·©U/n @€.Đă;`9ŁÂ+Zx @€hŞŔĐß�j–«¦:�› @€Šhç»Z|‹­`� @€ @ Ůťf^ëř6ű2= @€ĘčtuÎëř¶Úë•ŞČ @€ @ ™uWgc|›Y˙˘&@€ @€@áőŚVĆř^ŃÂ#@€ @€@S:]ťórFş:7ő7 @€Šč¬ăŰęIş:]Ń‚#@€ @€@S†Z|›* S_m…đIDATn @€(\ žÜJWçÂ+Zx @€h¦@gr«ÜŐY!@€ @€ uu6Ć·Ŕę @€¤¤«ł«€ @€Ę0«sůu,B @€4Z Żă« @€ @€�rz$ľĺV®Č @€ @ %ł:»  @€ @ h�~-ľEׯŕ @€ @@âë @€ @€�BZ׉oˇŐ+, @€¨Öń=,ńu% @€ @€@ˇU‹ožÜŞUÝ)4Ha @€ @€@sŞß<ąŐ@u§ą"'@€ @€�˘tu.şzG€ @€�& T=ś{%ľMľÄN€ @€�˘ęÉ­Śń-şšG€ @€�ć T-ľfunî r @€”-0�Şß“[•]Ϣ#@€ @€@cZ©˝Š‘ĺŚ{ ś @€ÍĐâŰŚz% @€š*ĐŇâŰÔŞ7 @€ čŚń°śQá-< @€4U`hŚď@5ËUSÄM€ @€Ĺ ´ó]-ľĹV°Ŕ @€ Đl�N3Żu|›}�ž @€ĺ tş:çu|[íuŤĘ Ud @€ ĐL�ş«ł1ľÍ¬Q @€ @ p�zF+c| Żhá @€ @ ©ť®Îy9#]ť›z �› @€E tÖńmő$]ť‹®hÁ @€ @ ©C-ľM7 @€.POnĄ«sá-< @€4S 3ąUîę¬ @€ @€�†ş:ă[`ő ‰ @€RŇŐŮU@€ @€ĺ �Őąü:! @€-�×ńU @€ @€@ą=ßr+Wd @€ �’Yť] @€ P´@żߢëWp @€ ńu  @€ @€@ˇ­�kŠÄ·Đę @€Tëř–řş @€ @ P�ŞĹ7Onժ @€ @ ąU‹ožÜj şÓ\‘ @€ @€@Ńş:]˝‚#@€ @€@“Şνß&_b'@€ @€@ŃőäVĆř]Í‚#@€ @€@sŞ_ł:7÷9 @€ĘHU‹oŹÉ­Ę®gŃ @€ @ ±­Ô^ĹČrFŤ˝N€ @€�fhńmF=‹’ @€MhőΙ» Í_°¸©â&@ŕ4XĽdĹivĆN— @ŕŮ�·`Qz w÷îiÇö-ĎÖy8. @€8!}»wVűŐŐů„đÚ) @€<ëլΩçY?'@€ @€Nś€u|Oś­= @€ @ŕ˙oď\�۸®;~�7�7Á—HĘő´%ęiY±–,Ű’âD®›ÔqÝN3™N;O3“ÔÓ/ůŢéL2Ť'ß:ýĐńÄÓ6žÚ±ăÚŽťĘŻĘ–dYR%ĹzQ/ŠA¤’ ’x˛ç,¸ °„ $E‚ü_Ď»{ďŢ˝űĂÂć˙žsĎ�y%Pˇ¤3â<ľ©ťy n      łN íę<ą3ë7@‡       °`Ťďř0�9 �ňpž€«ó°E—       €@ĘĂąéŚŔw�!€€€€€€Ě�´ĹwúF—       0ďŇÁ­ŐyŢż @@@@@@`ÎTć¬kt      �€Ńd¦ęš:2™L„”žüŰ%,ŤRßťŠE#%}Sz˝Ž\.™ÍfŇëô%ő±X.J$‰D( Q"‘,é±lžU´~ßĎÉl«çwt‰Ç!žHR$ ËGFá�›%ńT.štu6Č Ź    ‹™€�ކĆĺ”L&)Ęâ$ÉT/ő˘cae0.Ý]ťE‹_˝uuu,öX<�ŹĹYZ,MmQÁâĘ`4�ÓáT�ôôô-~EônýÎë�”Q�ü~ší “sţ¨tń{?\ťM<ócłŰÉér+›ěË9¸źÄŇ«�^¶lBô¦Č ‡¨đŕÉáSlKŻ�Ţ ‹ŢX4¶dEŻpÁ/ „…06űôÂ!5žTPŘä¨*â»:Ďň¬ŚNŻ'—ŰC6›�D eF-ž�+c2ńL›Ëm&»A„ĂĂ d"QÄxŃ@@@@Š' îÍbéE™N ‹•dś÷f±ô˘d˘jŻ7űdGŠ{sí–b“Y`31«k|ŲërWŃđp�âńÔ¬ŹŢ` ŮÔ’`ˇ+łJ¶J;ŮíNżý4T«ń     łO€­F°ôćĆŞpI­�ĚÝŕ.geMݏ7Łd&%­w^ękzł1fÍśŤnJ‘fw]Ô‘ř´{«k•5±X”·[ňş:(®,Śĺ:·ÇËĚěëÍ{MQBc�9'°T×ôć &ůčĚ[ÝĚĄł ]DŻ�Xů’‹ů˘Ąm$2NÖJ›ŇÇĽaŔŤA@@@@@+�ÄŚ-ľâŢ,Ńŕr Ţšę:|ř»´nÍZŽĆ]Am7oŇű|HÝ�î, ˛¨ľŇĆA°řnĎYhp      0C3ľ©@VU$îÍÚârşč•ż˙)ť=wŽ^ýŐŻ”ę];wŇ+Żü„~ńËW9_Řť¬KDüzŞŞiddݞČŕ@@@@`!ps,›şúeÄŐÎ1m†¨íĆ5’Ü­ Ą<ôp őř»i€cč”cR§ÓIăěź›@dm9s<"¬M–wdÇF3=ĽÂH.»Žú‡“ôÉ×c¸“ýNżxȦÔ91–~­šę ôô …Ç“ôÖ‘ŃôůąĂďýŚ„ŻËíVYÉš^mŮ˝{uuuÓoß~;]őßďżGË–ŐŃľ=ŹÓŰż{'}^vÄbśŕPú@t¸,68�_[¶l§Í[Q–÷Ĺ9mŤŃl˘Ť7ÓÉǨŰď›ßÁńÝE4îÜő8ť8~´l…ŻťSź®\ąŠőĹ0ݸq}N�J˙7oŢ ˇˇˇ9éżś:ý»ôŘ3‰,ŚÄ�,&˘˝›Íô»ŁŁôѱ)‘»o›™n¤ _·SG?~ŢAŤŐzz÷Ë.zĺ Ie1JÎHřÚlÎtôfí—l4é‹/ŹjOSŰ­Z˝ŞyÚy9!iŹśN„oN:8     0VŻy�¶>ň(]żÚJ§OźäÜ·ăÔذś۵‡öě}‚Ţ~ëŤeůťFłqĎŞŞ*J°p8d4•Ľ¸łŃ/ú�Nŕ/ź±Ó®3]iŹŃGÂÔć‹ÓŢíVz~ż•^Řo#_o‚.\›îŐ+=ýäĎÔT٧ŹNŽÓ;ź–�đMY|9Źojg:Ť{ś1qŢ.ÉÁ›kmŻ\*ÖÝ\Ą®ľŽBˇÜ3,Ň—lŇ7ň¬ĺ˘‡s    ÷›Ŕ–-ۨ§'@ÇŽ}žľµŻë6}ýŐqzęŕ3´zíCtµőY­•ôčŁ;©ş¦†tśę'ŕďR„˛s‹ěsß{�ZŻ\¤ĺËW��Ľž@€.\8OÁ`j  ®BO[¶mçúÉČ1t‚}wřúŻ”Ą€rcécë¶J˝Ĺb¦Žöv:wöExÉ`ąť^ÇŮ^<ÔŮŮÉ“ äńTQoOOú±Ö®]G�€źE±“\.'ČŤ�źůŤ˛Ű˛Z iٶţUU^Ĺú«ę#çzn^ŃL>_'ŤŽ–� S¦„Ď=›Lä&čź^ ĄŻţâĚ»™'éÇć ߲äľ˙đC­]n¤ĎÎŽÓo>I_» wRß„nŇô[ôXĺÇ(ër‹)+ůEÚ¶y [‚Źßő2yń¤o�o•ś}ÄÁ±kn\oť6”N_ýúµUDŻÉh¦§łŻÄ€€€€Ě.´’ś˛Śĺş�¸č^ľt�׎†8�kĘRi±Z©~YCVó@Źź>űô”s"ľ¶ďŘI¶J;gI1Đ*΄réâ7túÔ Ąľ»ËGx±ÇăĄ$Ń’úógĎĐůóg”z_çm:đíďR3 ŕ7®fݧÜÄú:dŹŇ$ ôS]]UVVfY^ĂáQžPHé�ţţ~jiiáÉ�Zş=yNžą�6Ňnś�g‰UWÄîŻ)–"cr¬!Ń#‹ą¬lL‰Ú[|s•P8I^W¶đmd×ćőÍFKŇ›—™5ś=%¸¨Zsásv›�bńé3ÚćŤ MôňË/ŃoŢř/Žň|V[ŤcX�âń”80ÝĂ#Q˘;w±‹lcÓrZǮ϶`Š(‹Ç˛Łwł‹´ZÄuWÜ—Ýn™ŮŇ(űťíj5»ňv)›śXąrŤR_]SKűžxZi#R).ľW9q1–u˝ť,äe?ÎË)ă¬1ÄBžér,“ ™% )¶3ĎŇFm öQ#[č}>/µ4±µ×Ę"şC­^´źŃÉWŇjJ˝?Ú5˛ćť|íÓUnŽúÜÝ— z¶˙ÍsvúĹŻłż‹tĂ…¸“ruNęřTŇđÉ8Ď”LEűĘ×ÉSO>A§Nť.HôVčt ű?ůúF€€€€Ě�ÁĐ€�¦Ę[=í˛&wÝCXŕrŠ#v‰~îOź§]»öR%‡úy}®amI°¨S‹ş¶T=–Ď»®×ťü›=ÉÖȉ$ÇĹáMöo^żĆQś2»(»}Żí•˛|ůÔ˛ˇEŮ (×ë­RÄľú@ňĽ™%ÁÖa­”)¤ŤÚÇŕŕ _ŻSD·‡'d-v¦ĐVŰ-¶Ď[ľMđC5Őe[uŐçôşő‘SĄg0AżüĎ!:y)BWI‚c•MI˝$ĄGuŽĹb<»d.(‚ť?ĐC­­…ą_ČL—ôŤ    óM Łî®.zxýşví ŤŤMąynć WŚę?;»HÖ‹ůĂśŇ3%x÷ď?Č©T {‚.¶:Šh“ŔVAĺ"‡ÝÉë„WQű­›Š�–ú^v•ľxńŹJ˝üÝ\[łŚzz»łbaw\8­d=ŻX_ý~zPógÝ:¶śł%XM=$9~űúúŇmÔ WéĽSHµ˝äň‘‡]ŮŘ­şo‰¤T•εyë­ĺľ×8˛łZ^8hŁZ·Ž>>ť˝Îąo€ß=vŤţ—7‡©©Ö@¶[¨#Łă犋ů¤Ţçľ~¦,ľĽ�6µSô˝%ę˛NݧäĎ:ÝŁŹŹ?ů¸ ţĺÇ+":„ Ť@@@@î�óçÎĐÁ�ߡĂĎ~ź.]úFńzlXÖD+WŻf! öö6ŞŻK­ĺmŕ%~Ńh”šš V4sJžÜ)a´Ă�ÝŰŰŁ졡A¶>FQ-–ä+—/(±u$Ęń† ›+ĄD™^·îaĂŰżç€O]Ú.ËâXÖńšÍ,˘xťn¦ńKöñŮˢxJřşŘýŮC#|^ÖćZ,VĹM9óAť�ě^m2Űű‚Jî`9'k‹—JaűCąŻü…�Î\‰ŇŔp’V4hË*úôúűw^őď†é§/:čŧmÔáO�/°Ŕ˝uY_J)9ť‘\ńěHaë|Ąý˝ŠD¤Ët˙¸W{Ô�€€€€Ŕ\číőÓ'źü�¶q„ćo=¶[ąťüÍ*‘—Ďp^_1ÉzÜk­Wh}Ë&jáČĚc,Îş88UMumöđ2,ŔŞńHý<ţĹçôřľ'i˙S‡”>űîܡS'Ź)˘W:9ţĺçś7řI�ő�ҧe’űűýľ)‹oF˙Ů7^�G˛Ž7‹RxdşĐęďňÂrŇs$g)=<1PW_O+XđŠńMŇ O¦Rź®�6j[ůâë%p�ĹŠE§,ź™măţg_Źq4p˘ŹZh˙¶Tdlyü+1zý÷Óż‹Ě×ęJ[”ŢűrTľ}ŘF˙řoe±Ţw˘âđłß›¸pńRIß§X|›T^Vő›«ŁCŃőש­­-WµrN,˝2ŰÓyű–’ř® Q    Ex��CŤg¸)yyşąäꕿYC“kÓ“;R')�Twem}!ÇN‡Kq{ §˘ kݱŰś…×a˛ex¶Š…ź«ŁízQÝ5I`(vżeëÖmJÎ]±ţ8>ŻLȢ—jÉ\ă»LN�LĚ8aîçŰŃjĺ™­htú,�Üčź_}5/Cą^ÚuůÚó¶C%€€€€€ŔŇ# “ É{Ä*¤ŤJN2ÉÔrj(ą¦­Í·čs÷ŞĎ˝?Ĺ+ĄäŕV™Đ‚}˝äĺő •6»"~µn™m3÷eŞčő±‹3 €€€€€h \ľ|Y{jÚq!mÔ‹&8BöŐ«­ę!>3�É “[|…‘Ýľ;=äd‹Ż§Şšě~OħůÝ«aŕµ¶ažˇ°•Ň®�r! âÎßu»\†[ă�×ß?Pc-‡AŠŔ;ýîĘa¨e1F±ÄÇă±0v\Ç;ŁzýŇžť)‹o �(�€č\Ž3%ÂwD766:,¨P@@@@@@@`±ť;6::ÂĎӧ膭VËby6<€€€€€€�Őjĺ¨ă}cŚâ–.đWÚív2R)‡Ŕ@@@@@@Ę™€č[»ÝF¬w­ü­:λ5ˇ×WtK46(wnÖ·˝®{ ż‚źĄ“ă;ÓńógĎD;+b{ą?Ć      °„ 8X×ÚYßž;{:ĘŽóÖ-áśëÇĆĆÜV‹9ęöxk'&óy-aNxt�2$ Ć\§ÓI}˝ţ‹ťťť×ř>ĺmP„oo}}}»Íš¨«[V#aźc±%“Ĺ'^ćľP@@@@@@@ŕľ0 Tĺń�ŰăŃ{©µőJ;ßüŢäX¶ř˙ri6", ˝µIEND®B`‚collective-icalendar-138c845/docs/_static/contributing/development-icalendar-git-workflow.svg000066400000000000000000001172771524134033600325650ustar00rootroot00000000000000collective-icalendar-138c845/docs/_static/edit-pencil-icon.png000066400000000000000000000231501524134033600242550ustar00rootroot00000000000000‰PNG  IHDR<uÓ%] ĺiCCPICC ProfileH‰•—TSنϽ鍖)ˇ7A:¤„Š D%$! %„TěĘŕŽPGtD@ÁŃ�± l�`ű€ Ę8X°ˇ2x„™yë˝·Ţ^ëä|Ůwź}öľëśµţ %™#‘dÂjd‰ĺҨ _FBb×H@�¸p¸2 +22 65˙ÝŢuh|ľe3žëßź˙WÓŕńe\ d„Sy2nÂ-ČřĘ•Hĺ Ć‹ĺ’qţ aš)áă,�`4yśS'™1凰x2‡#@öAüŚ\®ÉCNAŘN̉ނ°WČá!Ü�𬬬ěqţڰ/€b‚03ő/9ËźŞĚĎá”<ŮׄáýE2I&gé˙ůjţ·ee*¦ö0CY( ŽšdčnFv¨’Ĺ©s#¦XěЇî Á±SĚ•ů%M1ŹăŞ\›97lŠÓD�le9;fŠů˛€č)–fG)÷J“ú±¦�#ťŢW‘«ô ůleţŢś_¶d©T$Ę,äňl1×vĂÁÎÁ€ńű€únÚgüzň®śîŕ*¤ą“>ôř�* m Ś�°ŔxB@ŇI"X¸H?YH'‹Ár°€"°ěe`/Ř�#ŕh§Ŕ9p \ŕxz@?x†Á;0 A˘@TH2€L!kČbB^PEA‰P $€Ä�Z­�Š b¨ ÚUC?B'ˇsШşőB�Đk茂É0 Ö�ÍŕŮ0fÁˇp ĽŔ9pśo‚KáJř0Üź�ŻÁwŕř<‚(ŠŽ2DŮ �(?T* •†’˘V˘ Q%¨JTŞŐ†ş…ęA ˇ>˘±h*š�¶A{ �ѱh.:˝˝]†>„n@_@ßB÷˘‡Ń_1Ś.ĆăŽac0ĚbL¦ssssÓŹy‡ĹbéXs¬+6›�MÇ.ĂnÄîĆÖc[°ťŘ>ě‡ÓĆYăŕIxĽ>ź„ă×âKđ5ř3ř›řgřQ‚Á”ŕN� đK › Í„„~Â(QťhNô$ĆÓ‰k�ĄÄ:âEâCâ‰dDr#Í#‰H«IĄ¤Ł¤Ë¤^ŇG˛ŮŠěGN&+Č›ČUäň=ň …bFńˇ$Qä”M”jĘyĘcĘŞŠ­ […§˛JĄ\ĄAĺ¦ĘKU‚Ş©*Kuˇjžj‰ęqŐŞCj535?5ŽÚJµrµ“jÝj#ęTu{őő,őŤę5ęWÔ4pf<Ť|Ťýç5ú¨(Ş1ŐŹĘĄ®Ł ^¤öÓ°4s›–N+˘ˇµÓ†554ť4ă4—h–kžÖ졣čft6=“ľ™~ŚŢE˙4CokƆu3nÎxŻ5SËG‹ŻU¨UŻuGë“6C;@;C{«vŁö#´Ž•Î<ťĹ:{t.ę ͤÍô�ÉťY8óŘĚűş°®•n”î2Ýýş×uGôôő‚ô$z»ôÎë éÓő}ôÓő·ëźŃ4 x� ¶ś5xÎĐd°™ŚRĆưˇ®a°ˇÂpźa»á¨‘ąQ¬ŃZŁzŁGĆDc¦qšńvăVăa“p“ĺ&µ&÷M ¦LSˇéNÓ6Ó÷fćfńfëÍÍ̵ĚŮćyćµć-(Ţ9•·-±–LË ËÝ–V°•ł•ĐŞÜę†5líb-˛ŢmÝ9 3Ëm–xVĺ¬n˛ Ë&צ֦זnf»Ö¶Ńöĺl“ŮIł·În›ýŐÎŮ.Óî€Ý{ űűµöÍöݬ¸ĺ·)Ž�Ž«›_9Y;ńťö8Ýu¦:‡;Żwnuţââę"u©st5qMq­pífŇ�‘ĚŤĚËn7_·Un§Ü>ş»¸ËÝŹą˙áaă‘áQă10Ç|Î�9}žFžĎ}ž=^ ŻŻď˝zĽ ˝9Ţ•ŢO|Ś}x>}ž±,Yé¬Ă¬—ľvľRßľďýÜýVřµřŁü�ü ýŰ4bĘ k‡�ś�–µc‚C�·włőŘ\v5{8Ä5dEČ…PrhthYč“0«0iXs8ľ-üá\ӹ⹍ ‚±-âQ¤ydNäĎó°ó"ç•Ď{eµ<Ş-š˝(ş&ú]ŚoĚć�±±ŠŘÖ8Ő¸ä¸ę¸÷ńţńĹń= łV$\KÔI%6%á’â’&ŤĚ�żc~˛srAr×óK\Y¨ł0sáéEŞ‹8‹Ž§`RâSjR>s"8•ś‘TvjEę0׏»“ű‚çĂŰÎä{ň‹ůĎŇ<ÓŠÓž‚m‚Aˇ·°D8$ň•‰^Ą§ďMź‘Q•1–źYź…ĎJÉ:)Ög�/dëg/Éî”XK $=9î9;r†ĄˇŇ�2H¶@Ö$§!ÂéşÂBńŤ˘7×+·<÷Ăâ¸ĹÇ—¨//ąľÔj醥Ďňó~X†^Ć]ÖşÜpůšĺ˝+X+ö­„V¦®l]eĽ*U˙ę Ő‡Ö×d¬ůe­ÝÚâµo×ĹŻkÎ×Ë_ťß÷MĐ7µ*Ň‚îőë÷~‹ţVôműÇ »6|-ä^-˛+*)úĽ‘»ńęwöß•~7¶)mSűf—Í{¶`·�·tmőŢz¨X˝8ݏo[ř¶†íŚí…ŰßîX´ăJ‰SÉŢťÄťŠť=ĄaĄM»LvmŮőąLXv§Ü·ĽľB·bCĹűÝĽÝ7÷řě©Ű«··hď§ďEßßÝ´ŻˇŇ¬˛d?vîţ§â´ýŔüˇú ÎÁ˘�_ŞÄU=‡˘]¨v­®®Ń­Ů\ ×*j'î8⤩Φn_=˝ľč(8Ş8úüÇ”»Ž…k=Î<^÷“éO'¨'  †Ą ĂŤÂĆž¦Ä¦Î“!'[›=šOülűsŐ)ĂSĺ§5Oo>C<“fělŢŮ‘IËĐ9ÁąľÖE­Î'śż}aŢ…ö‹ˇ/_ ĽtľŤŐvö˛çĺSWÜŻśĽĘĽÚxÍĺZĂuçë'~qţĺD»K{Ă ×MnÍťs:ĎÜôľyî–˙­K·Ů·ŻÝ™{§ł+¶ënwrwĎ]ŢÝ�{™÷^ÝĎ˝?ú`őCĚĂÂGjŹJë>®üŐň×ú—žÓ˝ţ˝×źD?yĐÇí{ń›ě·ĎýůO)OKž<«p858Řń|ţóţ’ŁCż«˙^ńŇâĺOřüq}8a¸˙•ôŐŘëŤo´ßT˝uzŰ:9ňř]Ö»Ń÷…´?úČüŘö)ţÓłŃĹźqźKżX~iţúőáXÖŘ�„#ĺLH2ŕ´4^W!z9Ń�–&ÎźÔŰM~#LřO<©É'Ě€*bW†h”=Č0E�ŚĚă2)ĆŔŽŽĘń/“Ą9:Lć"#jóal썸fľHÇĆFwŹŤ}9€{€–śIť?nXäë§ŘśNĂŮ^»]ţi“ßéńź3ŻŔ üsţ�Ź"‘ú`ź·VeXIfMM*‡i’†D < uASCIIScreenshotăR ŞÖiTXtXML:com.adobe.xmp 316 Screenshot 117 =&ˇúIDATxíť\TWľÇ˙ô" ( ‚]•˘¬±×XbM˛ĆÝM±ÄhÚ&Ů·»/oł‰iľM4űVcş¦7vcšŤXQ1 HP‘" `¤}˙˙�;ŽĂť™;Ě 3÷Î˙|>Ě˝súůžËoNżn�ÁmŻ&Ŕ�€ pw�2r™`‚ ?L€ ¸ <—©j.(`,xü 0&ŕ2Xđ\¦Şą L€ °ŕń3Ŕ�€Ë`Ás™Şć‚2&Ŕ‚ÇĎ`.C€ĎeŞš Ę� ?L€ ¸ O—))Tó"ŁzA×îÝ!$4 |Ľ}4_Ţć,`UuäCfF¤§ťnΤmš–掠)OŽĚÚ´ �!ĂFBHH¨Rw˝$ `˙žáňĺBŐžOuUĆÖ'@b7aňTnŃéCi†{jńíŘňŐ‰Źá5ĂĂÁIŘŹµě¸űj?ľĆb&ćÄ^m†Om5ĆůŐ 1;îĆęp4ű ±§:P“aÁSSmq^o"@lK@muŔ‚çŘç…S·‚ÍƲq,µŐ žcźNÝ 5ŐŐ•pölF“Óőóóń$? ŐUU˛ń >N¦ü ĹEE˛îć,�Z¶„ľ}âŕÄÉ¨ŞŞ†�`ďľ$¨­©�ČČpíÚ5ČČřÍ\4ŠÝŐ¤!Ľ—Vqµ˛G5^ßČÝśĹСC`ÜŘQBÔŠ‹‹!ÓOO¨Ă–ťˇqss�Ţ1˝ ¦wOÜ[GŽ&CIq‰ˇ7Í|gÁÓLUrA¬%°ŕá ,,»­Ő‰]żÚ›˘6t0”•–ÁqÓëŤáîîţ8&׫W4>|´‘ŕÉů‘"ś}×,pswqÄ ŕřĺlß¶S8ŘjpĽŤD0>>fÍśU•UPSW }đ»ľ_)>şĆb[?O'SNA@@ôOěIUAă1Ăyóî]wÉ^=Łač­�aŐ»k  ż@?jÍÜłŕi¦*ą J,^ôŽo]×yMKK‡í;ľ�D»ź÷ě�Ý»­©??ąĽĽĽt~őoŢxóxö™'LviĺütďŢMD“—ź/şÂÔÂzü±G„�I‚§źŤżŐT×Ŕ˛—— ëŃ8ľ×ó)g–żľ˘QžbpŇĹ�éÓ7^�ÝćÍŰáĐá#ŕëë O=ąţp×Lxűí ¦j{/śşŇÓNCaˇ6Ź1˛Kł%öTj2ü5Őçµ� ç3ˇUp°8಑#[ŘŤ@-žÍGěŐô‚áŃMfc‹Ý8qÄLŔć‚�[C˙��ńôá66Ź›#lL ¨č2úĺ7í%CŤcl>ĽćcÍ)Ů™@·î‘бs<"˝ x{yŰ95'ŽŢŤň&>,Ě$¶}Ś4ŞkŞńřË�uţü–‘naĽÎăťĎyę‚sÂlF 6ľÄĆőQ_ĘÉă�râ¸b˙jőČ“j­9Î70A€Ä‹DL‰q±#,xJžöĂTH@‰čą’ŘQ˛ŕ©đAć,3ĄL‰ž«‰1cÁSúä°?& Rr˘çŠbGŐÇ‚§Ň‡�łÍ,! /z®*vÄ‹gi-yjŘ/`Ş&Ŕ-‡ÎW Ł]Z»vřÎÍęęj¸‚o`§•hUěTRWÉf-ľőľ˘˛÷MCľřą=>ôl°aj$`Tđ¨eG-ş |u&@oŢ*--…JěŢÖ·ú™ PYÁŁ1;ęƲةŻBíťcz/*usëź{§Ćń3Ű�<š  1;6LŔ�µô*Ę+ >#l�€ÚČNZĐl,ugŮ09Ő5µĐĘßOÎÉ%ě|}|aĆwBÇNť•÷JI lüúKČż”§Čż«{2Ć7ëÂyŘřĺ8¬ŇôĆ�¬ŕą»{đ…«?u&Ę_[[ÓäĺI-[µ‚ÄÄÖ¶­‰Ě;ĺ_ş‡“ć6ŁÇŤW,v”7*óŚ™wŔżVżÝÜY5›ž—_ôť» Â{Â…=ďCĆÇçŃ_ú�!·­›˙c¶\Ć<Č ž1ĎlϬ%0fĚxčem4BpZ¶l_őąŐqYAPË–"ČúO>ju�3÷?đ„…Y'đćŇhŠ;‰]âÂŹ 0˘—Ţ}üđ î§6<Ű”člFŽ/µú–<ńHnMMŚŻ©ä8\“HbGbaŤ™Î¦§A[2K»«żă† k‰±›ŕÝ7w. 6ü¦ĽáK@’“ŹÁúO>ąÉ^+_`Á˘Ĺ7§ g”Îź;ď®^W˙]¸M�8&Mš۶n�Ö­»É?q4n4jě8ÜV×JdŠ&J¨EˇVŁ/vÔŤ-Îřbfż,ţ±+Śâw5ç ¤nz±Y‹HOÖLDX’Y» ^DD„Ř{Ië¶Č¸ąą‰ďť»t�="áź˙řaŻĄzM í7Ą2Ó6<©Ě!!ˇ°ňťU0ďľ{yö[E>—žčŹŃ$‰ZĎPě¤n,U‰ž$v‡±[[SQ˙ĂÜÜUE]۬ Ě.ߡYďŽ;á;;NXśE» ž”“ ؚ۴éqŇĘôł`ƬY‹ďm EEę{ŻĄT.S×´´Txîo^né—~ćđőő…řľ}!ůčQSAŮ͉Hb'M°X;î訢™»ŕîE¶¨eçH±ŁI Ç#3×Ú»učp|#[ĽŘăoiťŘ]đ¤J¦#†>۰†ŹmBB`ŕ AŘĄŰ ž^^đä“OATϞТE ¨©©Á×Ĺť€×^}n: ^°’’Ŕ;+V�¨^ző5čСĽůúr8rř�żvÝűP‚Í⥋‘’sšë±ŁG ;;‘:Bxűö�l$g�†»fߍ6´-Cęţľőćp*%E„x˙ŁŹára!ţú]‚>·Ü"üüzę|Ś‚ú¬XëEýřţ{Xűď˙ÓĄŇEvŢCăţ×aGc†Ë_y™Çu„ĚßXúOe>ĆćóaJěbgżáýg�n¬#ĹŽhĐá$d”ŚăI~¤0" ÂŮ­e Ă6ɉ™ěěqýűsĎÁ- âT–“ÇŹCvéűüů ůŘQđBAě�ďŘ$ăáá]°KLvŁFŹv‰‰‰ŘzôÁc‹ ÄwgřđŔ…ŰţţţĐnúĚ™B ëęę`ĎĎ?ËfŹşÁŹ.}L�Ý™3żŠf=Uę_˙ţ߸Ŕ·ľŠ¨Ś(ô±ńńÂťşÍÔR~ţĹe"Îśś±ÇuÜřńÝSŘőčŃžţËA(}ućôiČĚü h¨áĄ×–‹®·lfŘR3Ô"vĽ)»PšĆî-ĽAC†�UőÔĄ‹Ť‹_??ŃŠ;qĽľ­óĂîďá—¤$ŘŽ­=÷ŠŠŽ‚ç_X1ńqxŕäU ™úç÷ňö†�‰– ‰GĎ^ő‹% ™=?É‹‰pl揑‘đŢęR%qZ˙駢<:K˝˙°aý§�–š ©gΗ×ßZááᏗś|Ł]řřŇ%P�çöŃĐŔ°EH[ç?ô óŹ^b×/ˇ¤¦ž�Ůsţ(sÝÚµ°cű6áçégź…~ ‰0 [Ú[7oÖËßZJ€žis†&>ZŐ/T6ç×–îj;ýrKĂúv†÷Jü†‘ľŰ]đşvëô'±ź^úŠ­žź`,®·ůç˛e„† O˙l�.;´Ň^kSţČŤü6Ÿ7%�’0×¶ý^�]©}{ö�ŮTšm]¸h‘č†Ň:=—Ś“#GŤĆ_ÍCŤ˘OÚż_ŘŃi»ŇÖţ}ű„MM;â·Q&ŃâZC·‘ş˘ 8{L“$v4±°cŰ6<hu}0,/‰Ń{kţ-¸Ps}â¤É†KS2QĐ…ąv�_˝EçśµAĽI‚ß_ŐŘÝ1r$ÎlŹ-bš ‘&ŤnŠÓ�_¨%JF˙A–î;uîěŔś)Oš&×B|*JhCëÎÓ7@ě‹ö×ďĹÖ1GŻłSNDŻÄ�é91ćnÎ^öEÜ´ôŁ ™ßŕEűKKŠ/c+ŻÚ\žíâî o-#ˇ¤ĺ,ö`/µĄ k!ŻĚs笍F„'ˇżÇôhFs®Ż”x”Ž˘ďp˝aS–!X“Á§˙RżxüŐ—^0Ť”W:˝c�•'Á�MĚ„‡ľsWCXěť:ÚB–±cĄĂvPč2c憞�ů oŢši,Čűď­iŇóЬ]Zc™'{š©tuC-Xúł‡±•ĐŮ#o'ß.ÄŽ„ŤNÖ7dGb2÷´J;ôÝíyOă„49˘d‚E[˘ Gš‚”ú!ź"ą‚”ď€şµj14nGĂqq}ŔŰ×Çh¶iě´©?~N#xFKÇš' -1!aٱ<9ăŁ`Í›\8kěhR„Ž(R:ÁBKOŚŤÓZ“KÂŇ©'Í}–ť%ů3ç—Ćjí9^ë4]Zs ěíî ]Z{—Ń–ń۲KkË|q\LŔ»MZ�J”Ý�`Ž +x´|AÚĂé�LqšÎM€–µĐ3† ¨Ť€¬ŕŃL)mĐgĂäxyy:l6].?lÇ”�Ľňň2q~›ŇHŘźë CMi?n>#l�€ÚČ ŢŐ«ĄXŽëř`›? BmćüZG€Ö ŇVżúgÄş¸84hn˛‚G™(,,‹`Yôš»Jś3=jŮŕÉ-ľ>ŢâŮpÎ\r®�€iF×áUWăé%yů¸5Ş5x{ű�Ó č4bÚĆĆuĐ…Őĺçď‡-»ZČĹg‚ž 6L@Ťd×á$00[{-Ä{)Üń4_-ÜçĎF†ÍĆŇ$ŤŮq7V[©Š€Ńž~)čAç‡]źß3& FFÇđÔXÎ3`LŔ{{ title|striptags|e }}{%- for parent in parents %} – {{ parent.title }}{%- endfor %}{{ titlesuffix }} {%- endblock %} {% block body %}

Page not found

We could not find the page you requested in the icalendar documentation.

You can search the icalendar documentation.

You can use the version switcher to go to other versions of icalendar.

Why is this happening?

The icalendar documentation team has restructured documentation to redirect visitors to the most current and accurate information, archive previous versions, and identify outdated pages. If you would like to report an issue or request a redirect be set up, you can create a new issue in the GitHub issue tracker.

{% endblock %} collective-icalendar-138c845/docs/_templates/donate.html000066400000000000000000000005251524134033600232740ustar00rootroot00000000000000

You can support icalendar by funding our work through Open Collective.

Fund via Open Collective

collective-icalendar-138c845/docs/_templates/nlnet.html000066400000000000000000000002021524134033600231320ustar00rootroot00000000000000

icalendar is supported through a generous grant from the NLnet Foundation.

collective-icalendar-138c845/docs/_templates/rtd-pr-previews.html000066400000000000000000000001251524134033600250700ustar00rootroot00000000000000

Pull request previews by Read the Docs.

collective-icalendar-138c845/docs/conf.py000066400000000000000000000206501524134033600202770ustar00rootroot00000000000000# icalendar documentation build configuration file import datetime import os import sys from pathlib import Path import icalendar HERE = Path(__file__).parent SRC = HERE.parent / "src" sys.path.insert(0, str(SRC)) # update docs from icalendar source for livehtml extensions = [ "notfound.extension", "sphinx.ext.apidoc", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx_autodoc_typehints", # must be loaded after sphinx.ext.napoleon. See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/15 "sphinx_copybutton", "sphinx_design", "sphinx_icalendar", "sphinx_issues", "sphinx_llms_txt", "sphinx_reredirects", ] source_suffix = {".rst": "restructuredtext"} master_doc = "index" project = "icalendar" this_year = datetime.date.today().year # noqa: DTZ011 copyright = f"{this_year}, Plone Foundation" # noqa: A001 # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # Define the json_url for our version switcher. json_url = "https://icalendar.readthedocs.io/en/latest/_static/version-switcher.json" # Define the version we use for matching in the version switcher. version_match = os.environ.get("READTHEDOCS_VERSION") release = icalendar.__version__ # If READTHEDOCS_VERSION doesn't exist, we're not on RTD # If it's an integer, we're in a PR build and the version isn't correct. # If it's "latest", change to "dev" in the version switcher. if not version_match or version_match.isdigit() or version_match == "latest": # For local development, infer the version to match from the package. if "a" in release or "b" in release or "rc" in release or "dev" in release: # Override the fully qualified URL in dev mode. json_url = "_static/version-switcher.json" version_match = "dev" else: version_match = f"v{release}" elif version_match == "stable": version_match = f"v{release}" exclude_patterns = [ "reference/api/modules.rst", ] html_theme = "pydata_sphinx_theme" html_theme_options = { "icon_links": [ { "name": "GitHub", "url": "https://github.com/collective/icalendar", "icon": "fa-brands fa-square-github", "type": "fontawesome", "attributes": { "target": "_blank", "rel": "noopener me", "class": "nav-link custom-fancy-css", }, }, { "name": "PyPI", "url": "https://pypi.org/project/icalendar", "icon": "fa-custom fa-pypi", "type": "fontawesome", "attributes": { "target": "_blank", "rel": "noopener me", "class": "nav-link custom-fancy-css", }, }, ], "footer_start": ["nlnet", "donate"], "footer_center": "copyright", "footer_end": ["rtd-pr-previews", "theme-version", "sphinx-version"], "logo": {"text": "icalendar"}, "navbar_start": ["navbar-logo", "version-switcher"], "navigation_with_keys": True, "search_bar_text": "Search", "secondary_sidebar_items": ["edit-this-page", "page-toc", "sourcelink"], "show_nav_level": 2, "show_toc_level": 2, "show_version_warning_banner": False, "switcher": { "json_url": json_url, "version_match": version_match, }, "use_edit_page_button": True, } html_context = { # "github_url": "https://github.com", # or your GitHub Enterprise site "github_user": "collective", "github_repo": "icalendar", "github_version": "main", "doc_path": "docs", } html_static_path = [ "_static", ] html_js_files = [ ("js/custom-icons.js", {"defer": "defer"}), ] pygments_style = "sphinx" smartquotes_action = "De" # -- linkcheck builder configuration ---------------------------------- # Ignore localhost linkcheck_ignore = [ # Ignore pages that require authentication r"https://app.readthedocs.org/dashboard/icalendar/users/create/", r"https://docutils.sourceforge.io/rst.html", r"https://github.com/collective/icalendar/fork", r"https://github.com/collective/icalendar/issues/new", r"https://github.com/collective/icalendar/settings/", r"https://groups.google.com/g/icalendar-coc/", r"https://pypi.org/manage/project/icalendar/collaboration/", # Ignore specific anchors r"https://github.com/actions/python-versions#support-policy", r"https://github.com/collective/icalendar/blob/7ca9db18c0847d1530520e01baf75f8ab8f4fa06/pyproject.toml#L32", r"https://github.com/collective/icalendar/blob/main/CODE_OF_CONDUCT.md#enforcement", r"https://github.com/collective/icalendar/blob/main/README.rst#related-projects", r"https://github.com/pre-commit/pre-commit-hooks#debug-statements", r"https://up-for-grabs.net/#/filters", # Ignore links that are unstable r"https://www.unicode.org/cldr/cldr-aux/charts/29/supplemental/zone_tzid.html", ] linkcheck_anchors = True linkcheck_timeout = 5 linkcheck_retries = 1 # -- notfound.extension configuration ---------------------------------- # https://sphinx-notfound-page.readthedocs.io/en/latest/configuration.html notfound_template = "404.html" # -- sphinx.ext.apidoc options ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/apidoc.html# apidoc_modules = [ { "path": "../src/icalendar", "destination": "reference/api", "exclude_patterns": [ "**/tests*", "**/timezone/equivalent_timezone_ids_result*", ], "separate_modules": True, "automodule_options": { "ignore-module-all", "members", "show-inheritance", "undoc-members", }, } ] autoclass_content = "both" # -- sphinx.ext.autodoc options ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html autodoc_default_options = { "ignore-module-all": True, "no-inherited-members": "object,CaselessDict,str,bool,NamedTuple,int,float,Enum", "members": True, "show-inheritance": True, "undoc-members": True, } # -- sphinx.ext.intersphinx configuration ---------------------------------- # This extension can generate automatic links to the documentation of objects # in other projects. Usage is simple: whenever Sphinx encounters a # cross-reference that has no matching target in the current documentation set, # it looks for targets in the documentation sets configured in # intersphinx_mapping. A reference like :py:class:`zipfile.ZipFile` can then # linkto the Python documentation for the ZipFile class, without you having to # specify where it is located exactly. # # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "sphinx": ("https://www.sphinx-doc.org/en/master/", None), "typing": ("https://typing.python.org/en/latest/", None), } # -- sphinx.ext.napoleon configuration ---------------------------------- napoleon_use_param = True napoleon_google_docstring = True napoleon_attr_annotations = True # -- sphinx_copybutton configuration ---------------------------------- # https://sphinx-copybutton.readthedocs.io/en/latest/use.html # Exclude line numbers, prompts, and console output when copying code blocks. copybutton_exclude = ".linenos, .gp, .go" # -- sphinx_issues configuration ---------------------------------- issues_github_path = "collective/icalendar" # -- sphinx_reredirects configuration ---------------------------------- # https://documatt.com/sphinx-reredirects/usage.html redirects = { "about": "index.html", "api": "reference/api/icalendar.html", "changelog": "reference/changelog.html", "cli": "how-to/cli.html", "credits": "contribute/credits.html", "contributing": "contribute/index.html", "install": "how-to/install.html", "license": "https://github.com/collective/icalendar/blob/main/LICENSE.rst", "maintenance": "contribute/maintenance.html", "security": "https://github.com/collective/icalendar/blob/main/SECURITY.md", "usage": "how-to/usage.html", } man_pages = [("index", "icalendar", "icalendar Documentation", ["Plone Foundation"], 1)] htmlhelp_basename = "icalendardoc" collective-icalendar-138c845/docs/contribute/000077500000000000000000000000001524134033600211535ustar00rootroot00000000000000collective-icalendar-138c845/docs/contribute/code-of-conduct.rst000066400000000000000000000002611524134033600246550ustar00rootroot00000000000000=============== Code of Conduct =============== Contributors are expected to follow the Python Calendaring Ecosystem's `Code of Conduct `_. collective-icalendar-138c845/docs/contribute/credits.rst000066400000000000000000000040741524134033600233470ustar00rootroot00000000000000======= Credits ======= When you :doc:`contribute ` to icalendar, your name will appear as in the list of contributors, provided you have not set your GitHub profile to be anonymous. The following people have made contributions to icalendar. The maintainers of icalendar are grateful for their support. Maintainers ----------- .. include:: ../_include/maintainers.inc Documentation lead ------------------ `@stevepiercy `_ Original author --------------- - Max M Contributors ------------ Become part of the list of :doc:`contributors `. .. image:: https://contrib.rocks/image?repo=collective/icalendar&max=120&columns=10 :alt: icalendar contributors :target: https://github.com/collective/icalendar/graphs/contributors :loading: lazy To print complete a list of all contributors sorted by their name and commit email address with their count of commits, issue the following command from your checkout. .. code-block:: shell git shortlog -s -e Supporters ---------- Financial support of the developers of icalendar makes it possible to add features, fix bugs, write documentation, and create an amazing community around the project. We're grateful for the financial support of `Nicco Kunzmann `_ by the following kind people and organizations through `Nicco's GitHub Sponsors page `_. - `Kraken Tech `_ - `Abe Hanoka `_ - `RPdenBoer `_ You can also donate to the icalendar project through `icalendar's Open Collective donation page `_. History ------- `Max M `_ had often needed to parse and generate iCalendar files. Finally, he got tired of writing ad-hoc tools. This package is his attempt at making an iCalendar package for Python. The inspiration has come from the email package in the standard library, which he thinks is pretty simple, yet efficient and powerful. collective-icalendar-138c845/docs/contribute/development.rst000066400000000000000000000210251524134033600242270ustar00rootroot00000000000000=========== Development =========== This chapter describes how to set up icalendar for development and to contribute changes. .. _development-prerequisites: .. include:: ../_include/prerequisites.inc .. _development-configure-git: .. include:: ../_include/configure-git.inc .. include:: ../_include/configure-git-card-dev.inc .. include:: ../_include/configure-git-steps.inc Install icalendar for development --------------------------------- To install icalendar for development—including all of its dependencies for tests, documentation, and formatting code, as well as a Python virtual environment—run the following command. .. code-block:: shell make dev .. _development-make-commands: :program:`Make` commands ------------------------ With all :ref:`prerequisites ` installed, you can run tests, reformat code, make various documentation builds, perform quality checks, and clean your environment. All :program:`Make` commands use the file :file:`Makefile` at the root of the repository. The :program:`Make` commands may invoke :program:`uv`, :program:`tox`, :program:`Sphinx`, :program:`Vale`, shell commands, and other programs. pre-commit ---------- `pre-commit `_ is automatically installed as one of the development requirements when running the following command. .. code-block:: shell make dev That command installs a supported Python, creates a Python virtual environment, and installs package and development requirements. When you commit code to icalendar with ``git commit``, pre-commit runs the following code quality checks and reformats code automatically for you. `debug-statements `_ Checks for debugger imports and Python 3.7+ ``breakpoint()`` calls in Python source code. `ruff check --fix `_ Runs the Ruff linter on Python files and fixes issues according to the configuration in :file:`pyproject.toml`. `ruff format `_ Runs the Ruff formatter on Python files and fixes issues according to the configuration in :file:`pyproject.toml`. :program:`ai-prompt-auto-commit` Automatically records AI prompts and appends them to your commit messages. The configuration file for pre-commit, :file:`.pre-commit-config.yaml`, is located at the root of the repository. .. _pre-commit-ai-prompts: AI prompt automation '''''''''''''''''''' To comply with the :ref:`artificial-intelligence-policy`, you must include AI prompts in your commit messages. You can automate this by using the :program:`ai-prompt-auto-commit` hooks. If you use :program:`Claude Code`, recording is automatic, once the repository is prepared. For other AI tools, you may need to manually record prompts or instruct the AI to do so. Consider adding support for your AI tool in the `project repository `_. Contributors to icalendar are encouraged to use pre-commit. Any issues that would be caught by pre-commit shall be caught by GitHub workflows when you push commits to a pull request for icalendar. This could delay merging of your pull request. However, you may opt out of using pre-commit. You can use the ``--no-validate`` flag for the ``git commit`` command. .. code-block:: shell git commit -m "My commit message" --no-validate Alternatively, configure your editor to use ``--no-validate`` for all commits. The screenshot below shows how to configure PyCharm to disable pre-commit by searching for "git commit hooks" in its settings. .. card:: .. image:: ../_static/contributing/development-disable-pre-commit.png :alt: Disable pre-commit in PyCharm settings :target: ../_static/contributing/development-disable-pre-commit.png +++ *Disable pre-commit in PyCharm settings* Run tests --------- Through :program:`uv`, :program:`tox` manages all test environments across all Python versions. To run all tests in all environments, run the following command. .. code-block:: shell make test The following command shows how to run tests in Python 3.14. .. code-block:: shell make test TOX_ENV=py314 .. seealso:: tox's `CLI interface documentation `_. Format code ----------- icalendar requires code formatting. You can run the following command to automatically format the code. .. code-block:: shell make format .. _make-zizmor: Run zizmor ---------- `zizmor `_ is a static analysis tool for the security of GitHub Actions workflows. The command ``make zizmor`` runs the same analysis as the ``zizmor static analysis`` GitHub Actions workflow. .. code-block:: shell make zizmor Pass options to zizmor in the ``ZIZMOROPTS`` environment variable. In the following example, generate a SARIF report of the results. .. code-block:: shell make zizmor ZIZMOROPTS="--format sarif" The command ``make zizmor`` automatically installs zizmor into your Python virtual environment when you invoke it the first time. If your virtual environment already exists, update it with the following command. .. code-block:: shell make dev .. seealso:: - :file:`.github/workflows/zizmor.yml` - zizmor's `documentation `_ Code conventions ---------------- icalendar has adopted code conventions to help make its code more legible and understandable. Internal use only ''''''''''''''''' There are no truly private methods in Python. However, icalendar follows the :pep:`8` Python style guide regarding the use of a single leading underscore character ``_`` to the object as "internal use only." "Internal use only" methods and variables are not part of icalendar's public API, and developers should not use them in their code. These are implementation details that may change without notice. In addition, "internal use only" objects are not displayed in the documentation. Their docstrings, of course, remain in the Python source code. .. _type-hints: Type hints '''''''''' Type hints in Python help developers catch errors early, improve code documentation, and enhance the functionality of IDEs and linters. icalendar uses type hints, and supports rendering them in its :doc:`reference API documentation `. icalendar was originally written before the existence of type hints in Python. As such, there are many Python objects in the code base that lack type hints. The icalendar team welcomes contributions to add type hints. When the type hints in the standard library are not sufficient, you can use subtyping through :doc:`protocols `. The following example demonstrates subtyping through the usage of a protocol and a method using a literal :ref:`ellipsis ` ``...`` to indicate that the method signature exists, but the implementation details aren't necessary. .. code-block:: python class HasToIcal(Protocol): """Protocol for objects with a to_ical method.""" def to_ical(self) -> bytes: """Convert to iCalendar format.""" ... .. seealso:: - GitHub issue :issue:`Add type hints <938>` - :ref:`markup-and-formatting` - :doc:`typing:index` - Python standard library :mod:`typing` - :ref:`annotating-callables` Activate a tox environment -------------------------- If you'd like to activate a specific tox virtual environment, use the following command, replacing the Python version accordingly. .. code-block:: shell source .tox/py312/bin/activate Install icalendar manually -------------------------- The best way to test the package is to use tox as described above. However, if you can't install tox, or you'd like to use your local copy of icalendar in another Python environment, this section describes how to use your installed version of Python and pip. .. code-block:: shell cd src/icalendar python -m pip install -e . The above commands install icalendar and its dependencies in your Python environment so that you can access local changes. If tox fails to install icalendar during its first run, you can activate the environment in the :file:`.tox` folder and manually set up icalendar as shown above. To verify installation, launch a Python interpreter, and issue the following statements. .. code-block:: pycon Python 3.12.0 (main, Mar 1 2024, 09:09:21) [GCC 13.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import icalendar >>> icalendar.Calendar() VCALENDAR({}) collective-icalendar-138c845/docs/contribute/documentation/000077500000000000000000000000001524134033600240245ustar00rootroot00000000000000collective-icalendar-138c845/docs/contribute/documentation/build-check.rst000066400000000000000000000226431524134033600267370ustar00rootroot00000000000000.. _build-check-documentation: ============================= Build and check documentation ============================= This chapter describes how to build and check the quality of documentation. .. _documentation-prerequisites: .. include:: ../../_include/prerequisites.inc .. _documentation-configure-git: .. include:: ../../_include/configure-git.inc .. include:: ../../_include/configure-git-card-doc.inc .. include:: ../../_include/configure-git-steps.inc .. _file-locations: File locations -------------- Narrative documentation files are located in the `docs `_ directory. :doc:`API reference documentation files ` are located in the `src `_ directory. .. _documentation-make-commands: :program:`Make` commands ------------------------ With all :ref:`prerequisites ` installed, you can make various documentation builds, perform quality checks, and clean your environment. All :program:`Make` commands use the file :file:`Makefile` at the root of the repository. The :program:`Make` commands may invoke :program:`Sphinx`, :program:`uv`, :program:`Vale`, shell commands, and other programs. Help '''' To get help and see descriptions of the ``make`` commands, use the following command. .. code-block:: shell make help Alternatively, the :file:`Makefile` file at the root of the repository contains all available commands. All commands except the "clean" commands will create a Python virtual environment, and install requirements. .. _make-livehtml: Live HTML preview ''''''''''''''''' ``livehtml`` rebuilds documentation as you edit its files, providing a preview with live reload in the browser. This is the most useful command when editing documentation. .. code-block:: shell make livehtml The console will give you the URL to open in a web browser. .. code-block:: console [sphinx-autobuild] Serving on http://127.0.0.1:8050 HTML '''' To build the documentation as HTML without live reload, run the following command. .. code-block:: shell make html You can now open the output file :file:`_build/html/index.html` in a web browser to view static content. This build must be successful in the :ref:`continuous-integration-checks` for your pull request to be merged. .. _make-linkcheckbroken: Check links ''''''''''' ``linkcheckbroken`` checks all links, returning a list of only broken links. .. code-block:: shell make linkcheckbroken Open :file:`_build/linkcheck/output.txt` for the entire list of links that were checked and their result. This build must be successful in the :ref:`continuous-integration-checks` for your pull request to be merged. .. _make-vale: Spelling, grammar, and style '''''''''''''''''''''''''''' ``vale`` checks American English spelling, grammar, and syntax, and follows the Microsoft Writing Style Guide. .. code-block:: shell make vale Observe the output and adjust Vale's configuration, as described in the next section. .. _vale-options: Vale options ```````````` Pass options to Vale in the ``VALEOPTS`` and ``VALEFILES`` environment variables. In the following example, run Vale to display warnings or errors only, not suggestions, in the console on a single file. .. code-block:: shell make vale VALEOPTS="--minAlertLevel='warning'" VALEFILES="docs/index.rst" The command ``make vale`` automatically installs Vale into your Python virtual environment when you invoke it the first time. This build must be successful in the :ref:`continuous-integration-checks` for your pull request to be merged. .. _vale-integration: Vale integration ```````````````` Vale has `integrations `_ with various IDEs. Integration might require installing Vale using your operating system's package manager. - `JetBrains `_ - `Vim `_ - `VS Code `_ .. _vale-configuration: Vale configuration `````````````````` icalendar configures Vale in the following places: - :file:`.vale.ini` is Vale's configuration file. This file allows overriding rules or changing their severity. It's configured to use the `Microsoft Writing Style Guide `_ for its ease of use—especially for non-native English readers and writers—and attention to non-technical audiences. - :file:`Makefile` passes options to the ``vale`` command, such as the files Vale checks. - icalendar documentation uses a custom spelling dictionary, with accepted and rejected spellings in :file:`docs/styles/config/vocabularies/icalendar/`. Authors should add new words and proper names using correct casing to :file:`docs/styles/config/vocabularies/icalendar/accept.txt`, sorted alphabetically and case-insensitive. If Vale does not reject a spelling that should be rejected, then you can add it to :file:`docs/styles/config/vocabularies/icalendar/reject.txt`. All entries in both files support regular expressions. Use https://regex101.com/ with the :guilabel:`Golang` option selected to test regular expressions. .. seealso:: Vale's `regex guide `_ - Add generic spellings to accept or reject in their respective files inside the :file:`docs/styles/config/vocabularies/Base/` folder. Because it's difficult to automate good American English grammar and syntax, it's not strictly enforced. You can add spellings to Vale's configuration, and submit a pull request. This is an easy way to become a contributor to icalendar. Test code snippets and docstrings ''''''''''''''''''''''''''''''''' To run tests on code snippets and docstrings, use the following command. .. code-block:: shell make doctest This is a shortcut that calls :program:`pytest` to run on a single test file. Build API source files '''''''''''''''''''''' Beginning with Sphinx 8.2.0, the extension ``sphinx.ext.apidoc`` generates source documentation files from which the Sphinx extension ``sphinx.ext.autodoc`` will render the API documentation. See the file :file:`docs/conf.py` for both of their configuration options. This happens automatically with every build of the HTML documentation. .. seealso:: - :mod:`sphinx:sphinx.ext.apidoc` - :mod:`sphinx:sphinx.ext.autodoc` Purge builds '''''''''''' ``clean`` removes all builds and cached files of the documentation. Use this command before a build to troubleshoot issues with edits not showing up and to ensure that cached files do not hide errors in the documentation. This is especially useful when adding a new page to the documentation and it doesn't appear in the navigation. Sphinx only rebuilds changed files. .. code-block:: shell make clean Purge environment ''''''''''''''''' ``clean-python`` cleans the documentation build directory and Python virtual environment. Use this command when packages that you have installed in your virtual environment yield unexpected results. .. code-block:: shell make clean-python Add features ------------ To add features to documentation through Sphinx extensions and other programs, you might need to perform the following tasks. Add a package ''''''''''''' To add a package for documentation, such as a Sphinx extension or other third application, use :program:`uv`. .. code-block:: shell uv add --group docs new-requirement This will add ``new-requirement`` to the list of documentation requirements in the ``docs`` group in the :file:`pyproject.toml` file. .. _configure-a-package: Configure a package ''''''''''''''''''' Most packages require configuration in Sphinx and in the :file:`Makefile` file. Follow the specific directions of the package. For Sphinx, you would typically edit the file :file:`docs/conf.py`, and add both the package's Python name to the ``extensions`` list and its configuration options as a new section. For the latter, use a comment with a visual separator, such as that for configuring the Napolean extension. .. code-block:: python # -- Napolean configuration ---------------------------------- napoleon_use_param = True napoleon_google_docstring = True napoleon_attr_annotations = True If your feature adds a shell command, add helpful options to the :file:`Makefile` file. Document a package '''''''''''''''''' "If it ain't documented, it's broken." After adding and configuring a package, it's important to inform other people what the feature does and how to use it. Pull request previews --------------------- On every pull request, Read the Docs generates a pull request preview of the documentation. It creates a new comment containing a link to the preview in the pull request. `View all builds of documentation on Read the Docs `_. This feature is configured in the file :file:`.readthedocs.yml` and the repository settings. .. _continuous-integration-checks: Continuous integration checks ----------------------------- On every pull request, continuous integration checks attempt to build the documentation, run Vale checks, and check for broken links. If any of these checks fail, then the pull request cannot be merged. You must resolve the build failures, preferably locally, before the pull request can be merged. This feature is configured in the file :file:`.github/workflows/documentation.yml`. collective-icalendar-138c845/docs/contribute/documentation/index.rst000066400000000000000000000025451524134033600256730ustar00rootroot00000000000000============= Documentation ============= This part of the documentation describes how to contribute to icalendar's documentation. For quick or small edits through the GitHub web interface, read the chapter :doc:`small-edits`. For larger edits, the chapter :doc:`build-check` provides a comprehensive command reference to build and check the quality of documentation. Refer to the chapter :doc:`style-guide` when you need examples of language, terminology, code, or markup. Find issues ----------- icalendar's documentation may have errors and omissions. Open issues are available to work on. Contributions to improve the documentation are welcome. This is a good way for first-time contributors to participate in the icalendar community. Find open issues to work on by clicking either button below. .. grid:: 1 2 3 4 .. grid-item:: .. button-link:: https://github.com/collective/icalendar/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22doc%22 :color: success :shadow: Documentation .. grid-item:: .. button-link:: https://github.com/collective/icalendar/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22 :color: primary :shadow: Good First Issue Contents -------- .. toctree:: :maxdepth: 2 small-edits build-check style-guide collective-icalendar-138c845/docs/contribute/documentation/small-edits.rst000066400000000000000000000030261524134033600267750ustar00rootroot00000000000000.. _make-small-edits-via-github: =========================== Make small edits via GitHub =========================== For small edits to the documentation, use the GitHub web interface. #. On every page in documentation, on the right-hand side, there is a link :guilabel:`Edit on GitHub`. When you click that link, you'll be taken to GitHub's web interface where you can edit files. Alternatively, for narrative documentation files, you can navigate into the `docs `_ directory to find the source file to edit. For :doc:`API reference documentation files `, you can instead navigate into the `src `_ directory. For either file type, you can then click the :guilabel:`pencil icon` to edit the file in the browser. .. image:: /_static/edit-pencil-icon.png :alt: GitHub Edit this file #. Make edits. When done click :guilabel:`Commit changes…`. A modal dialog pops up. #. In the modal dialog: - Add a commit message. - Optionally add an extended description. - Select :guilabel:`Create a new branch for this commit and start a pull request`. - Enter a descriptive name for the new branch. - Click :guilabel:`Propose changes`. #. Make your pull request against the ``main`` branch of the repository. #. Members who subscribe to the repository will receive a notification and review your request. #. Request a review from other team members. collective-icalendar-138c845/docs/contribute/documentation/style-guide.rst000066400000000000000000000467051524134033600270250ustar00rootroot00000000000000=========== Style guide =========== This chapter is the documentation writing style guide for icalendar. Refer to this guide when you need examples of language, terminology, code, or markup. It applies to both narrative and API reference documentation. Differences and exceptions for both types of documentation are noted for each. Voice and tone -------------- The icalendar community is warm and friendly, direct and clear, and helpful. icalendar's documentation reflects its community. For professional technical documentation writing guidance, the icalendar community selected the `Microsoft Writing Style Guide `_ because it aligns with its spirit. Key concepts from that guide include the following. - Documentation should be informational, but friendly. - Address the reader by using "you" and "your" instead of "the user" or "the user's." - When giving instructions and in headings, use the `imperative mood of verbs `_. - Use the `active voice `_ whenever possible, avoiding the passive voice. - Headings should be "Sentence cased," not "Title Cased." - Keep sentences short and understandable. .. note:: The style guide is mostly enforced through the use of Vale, but only in the narrative documentation at this time. Specific guidelines ------------------- The icalendar developers have adopted additional guidelines. - For narrative documentation only, use one sentence per line. - For API reference documentation only, line length is automatically enforced by a Python code formatter. - Use dashes ``-`` in filenames and avoid underscores ``_``. - Images should be no wider than 740 pixels to fit within the documentation's main view port. Organization and structure -------------------------- icalendar documentation uses the `Diátaxis framework `_, a systematic approach to technical documentation authoring. Rather than attempt to organize documentation toward a specific role such as developer or user, the Diátaxis framework organizes documentation into the four categories of tutorials, how-to guides, explanation, and reference. In this way, your title or role does not matter. Instead, what you want to achieve matters. By keeping each page focused on one category, readers can focus on getting work done, understanding, or experimenting. .. _markup-and-formatting: Markup and formatting --------------------- When writing documentation, use the `reStructuredText `_ markup syntax. When writing API reference documentation, write `docstrings `_ inside the Python code following the `Google Python Style Guide, Comments and Docstrings `_ format. These docstrings get rendered into the :doc:`API reference documentation <../../reference/api/icalendar>`. Sphinx and its extensions enhance core reStructuredText with additional features. - automatic API documentation file generation in reStructuredText through :mod:`sphinx:sphinx.ext.apidoc` - rendering of API documentation files and Python docstrings to HTML through :mod:`sphinx:sphinx.ext.autodoc`, :mod:`sphinx.ext.napoleon`, and `sphinx_autodoc_typehints `_ - rendering of Python source files to HTML through :doc:`sphinx.ext.viewcode ` - hyperlinking to internal and external documentation through :doc:`sphinx.ext.intersphinx ` - display and one-click copying of code blocks through `sphinx_copybutton `_ - user interface enhancements, including tabular interfaces, cards, and buttons through `sphinx_design `_ - redirects for moved files through `sphinx_reredirects `_ - 404 not found page through `notfound.extension `_ - automatic linking to GitHub issues and pull requests through `sphinx-issues `_ - iCalendar and jCal file syntax highlighting through `sphinx-icalendar `_ For configuration of these features, see :ref:`configure-a-package`. .. _markup-examples: Markup examples --------------- All of the following markup examples will work in both narrative and API documentation. API documentation has additional syntax usage. General cross-references '''''''''''''''''''''''' Sphinx supports various cross-referencing roles to create hyperlinks to other elements in the documentation. In icalendar's documentation, the most frequently used roles are the following. - :rst:role:`doc` to link to a file - :rst:role:`ref` to link to an arbitrary label - :rst:role:`term` to link to a glossary term - :rst:role:`rfc` to link to an RFC - ``issue`` or ``pr`` to link to a GitHub issue or pull request, provided by `sphinx-issues `__ When referencing a specific section in an RFC, copy the anchor name from the URL, that is, the part of the URL including and after the pound sign ``#``, and use the following syntax. .. code-block:: text :rfc:`number#anchor` The following example shows how to link to RFC 6350, Section 6.2.2. .. code-block:: rst :rfc:`6350#section-6.2.2` Which renders as shown. :rfc:`6350#section-6.2.2` When referencing a GitHub issue, use the following syntax. .. code-block:: rst :issue:`1050` Which renders as a hyperlink to the issue on GitHub. :issue:`1050` Similarly, for a pull request, the syntax would be the following. .. code-block:: rst :pr:`808` Which renders as a hyperlink to the pull request on GitHub. :pr:`808` .. seealso:: :doc:`sphinx:usage/referencing` For general Sphinx cross-referencing. `sphinx-issues `_ For additional usage examples of the ``issue``, ``pr``, and other GitHub roles. Cross-reference Python objects '''''''''''''''''''''''''''''' In addition to the general cross-references, Sphinx supports various cross-referencing roles to create hyperlinks to Python objects. As with all cross-references, these forms consist of a role, a target, an optional custom label, and an optional modifier. In icalendar's documentation, the most frequently used roles in the Python domain are the following. - :rst:role:`class ` - :rst:role:`mod ` - :rst:role:`func ` - :rst:role:`attr ` The target must be either a full dotted Python path or use a shortcut to disambiguate to which object it should be hyperlinked. The following reStructuredText source examples show which form to use to get the preferred display. These examples are ordered from most to least preferred. object only Use this form only when the object exists in the rendered HTML page. Thus the object's context is clear to be that within its Python module or class. To do so, use the tilde character ``~`` as a modifier, along with the role and target, to display only the object when rendered. .. code-block:: rst :class:`~icalendar.prop.vXmlReference` The above example will render as shown. :class:`~icalendar.prop.vXmlReference` full dotted Python path Use this form only when the object exists outside the rendered HTML page, such as in a superclass. To do so, use the following syntax consisting of a role and target only. .. code-block:: rst :class:`icalendar.prop.vXmlReference` The above example will render as shown. :class:`icalendar.prop.vXmlReference` custom text label As a compromise to showing the full dotted Python path, while retaining sufficient context of the object's location when displayed, use a custom text label. Use this form only when horizontal space is constrained when rendered to HTML. .. code-block:: rst :class:`prop.vXmlReference ` The above example will render as shown. :class:`prop.vXmlReference ` .. seealso:: :doc:`sphinx:usage/domains/python` .. note:: Because icalendar uses the Python domain exclusively, it's safe to omit the ``:py`` prefix when creating references to Python objects. Target shortcut ``````````````` As well as the above forms, you can use a shortcut to avoid repeating the full dotted path in the target. In the page in which the reference appears, you may use the :rst:dir:`currentmodule ` directive and omit the name of the current module. .. code-block:: rst .. currentmodule:: icalendar.prop The class :class:`vXmlReference` is useful. .. currentmodule:: icalendar.prop The above example will render as shown. The class :class:`vXmlReference` is useful. Calendar file code blocks ''''''''''''''''''''''''' Sometimes reading raw calendar files is challenging, especially when the file contains multiple events. To make them easier to read, use either the ``literalinclude`` or ``code-block`` directive with one of three different lexers, ``calendar``, ``ics``, or ``jcal``. The ``calendar`` lexer will generate a tabbed interface. The first tab displays the calendar's event names and their start and end dates. The second tab displays the file in iCalendar format, and the third in jCal format. sphinx-icalendar supports both iCalendar and jCal source file formats. .. code-block:: rst .. literalinclude:: ../../../src/icalendar/tests/calendars/example.ics :language: calendar .. literalinclude:: ../../../src/icalendar/tests/calendars/example.ics :language: calendar .. seealso:: See more examples and usage in the documentation of `sphinx-icalendar `_. Python docstrings ----------------- Python docstrings provide clear explanations of what code does, making it easier for people to use and maintain it. Sphinx generates documentation automatically from docstrings into the :doc:`icalendar API reference guide <../../reference/api/icalendar>`, enhancing code readability and usability. Python docstrings typically include reStructuredText markup, often including cross-references to narrative and API documentation. :pep:`257` describes core docstring conventions. To enhance the display of its API documentation, icalendar uses the Sphinx extensions :mod:`sphinx.ext.napoleon` and `sphinx_autodoc_typehints `_. The former extension supports Google style docstrings, which are easier to write and read, especially when Sphinx renders them to HTML. The latter extension supports Python 3 annotations, or type hints, for documenting acceptable parameter types and return value types of functions. .. seealso:: `Google Python Style Guide `_ for more examples of docstrings and their format. Docstring structure ------------------- In addition to the structure of docstrings as defined by :pep:`257`, icalendar has adopted conventions from the `Google Python Style Guide `_. A docstring consists of a summary, followed by a possible description, then any helpful sections usually ordered by inputs then outputs. To create a docstring section, use either one of the supported :ref:`section headers ` or any string for a custom section header, followed by a colon ``:``, followed by a block of indented text. Supported section headers enhance formatting, such as structuring method parameters or code examples, whereas custom section headers render as just a heading and content. All items should terminate with a period. The following docstrings items are the most frequently used in icalendar, although they may be others. .. important:: Don't write docstrings for a class's ``__init__`` or ``__new__`` methods, if they're present. Instead, write a docstring for the class itself. Sphinx will automatically append the ``__init__`` or ``__new__`` docstrings to the class's docstring, which may render the documentation in a confusing manner to the reader. If you find docstrings in a class's ``__init__`` or ``__new__`` methods, then please create a pull request that moves and merges their content into the class's docstring, leaving their docstrings empty. .. seealso:: - `Document __new__ methods `_ - `Merge __init__ or __new__ docstrings into their class's docstring `_ Summary Docstrings must begin with a one-line summary of the Python object, terminated by a period. Description When the one-line summary is insufficient to describe the Python object, then write an overall description of what it does, without going into details of how it does it. Leave implementation details to the code, and optionally inline code comments. Separate the summary and description with a blank line. ``Attributes`` Each attribute should consist of its name, type, and a brief description. ``Parameters`` Each parameter should consist of its name and a brief description. Parameters should be sorted, according to the following guidance. - There may be up to two groups of parameters, required and optional. - The parameters in the Python signature should retain their existing order, as changing their order may be a breaking change. - Required parameters must always appear before optional, both in the Python signature and in the docstring. - Within each group of required and optional parameters, sort them in alphabetical order in the docstring. - Required attributes should have a term of "Required." in their description, preferably immediately after their name. The following abstraction illustrates a typical example. .. code-block:: python def some_function(creq, breq, areq, bopt=2, aopt=1, copt=3) -> type_hints: """Summary. Longer description. Parameters: areq: Required. A string. breq: Required. A string. creq: Required. A string. aopt: An integer. bopt: An integer. copt: An integer. """ pass If the Python signature for a parameter lacks type hints, you must either add :ref:`type hints ` or specify the parameter's type manually. The Sphinx extensions and the use of type hints automatically render the type for you. Using type hints is preferred. .. seealso:: :ref:`type-hints` .. note:: Use "Parameters" instead of "Args" or "Arguments." The term "Parameters" is preferred for consistency with Python's official terminology. See `Python FAQ on arguments and parameters `_ and :ref:`Napoleon's supported sections `. ``Returns`` The return value consists of its return type and a brief description. ``Raises`` The ``Raises`` section is a list of all exceptions that are relevant to the interface. ``Examples`` Usage examples of the Python object. These must be valid examples, including imports, as they must pass tests. Not only are these helpful to developers, but they're fun to write, and rewarding when they pass. .. seealso:: :ref:`sphinx:Sections` Escape docstrings ----------------- Avoid double-escaping in docstrings. Use the raw ``r`` indicator immediately before the leading docstring delimiter ``"""``. Thus the docstring and the rendered content will have the same level of escapes. It will also be less confusing for readers of the source code. .. seealso:: :ref:`parser-split_on_unescaped_semicolon` example. Docstring examples ------------------ The following examples are actual docstrings in icalendar that will render properly. ``Event.new()`` ''''''''''''''' See the rendered view of this class method at :meth:`Event.new() `. .. literalinclude:: ../../../src/icalendar/cal/event.py :pyobject: Event.new ``Component.register`` '''''''''''''''''''''' See the rendered view of this class method at :meth:`Component.register `. .. literalinclude:: ../../../src/icalendar/cal/component.py :pyobject: Component.register .. _parser-split_on_unescaped_semicolon: ``parser.property.split_on_unescaped_semicolon`` '''''''''''''''''''''''''''''''''''''''''''''''' See the rendered view of this class method at :meth:`parser.property.split_on_unescaped_semicolon `. .. literalinclude:: ../../../src/icalendar/parser/property.py :pyobject: split_on_unescaped_semicolon Style and quality checks ------------------------ When making a contribution to documentation, style and quality checks must pass both locally while developing and in continuous integration after opening a pull request. .. seealso:: See :doc:`build-check` for how to set up and run the builds and checks on documentation. In addition to automated checks, perform visual checks to ensure that documentation renders as intended. icalendar is configured with Read the Docs to build pull request previews of documentation. When a pull request is opened on a branch and there are changes to any documentation files, Read the Docs will build a preview of the documentation, and insert a link to the build in the pull request description. If there's no link, either there are no changes to documentation files or the branch isn't in the icalendar repository. All pull request preview builds are listed on Read the Docs at `icalendar builds `_. Always pay attention to errors and continuous integration failures, and attempt to resolve them. Warnings may provide helpful information. Link checks ''''''''''' All links must be valid. .. seealso:: See how to check links in :ref:`make-linkcheckbroken`. Spelling and grammar '''''''''''''''''''' `Vale `_ is a linter for narrative text. It checks spelling, English grammar and syntax, and style guides. icalendar uses American English. Because it's difficult to automate good American English grammar and syntax, it's not strictly enforced. It's understood that contributors might not be fluent in English. If you're more comfortable writing in your preferred language, then you may submit a pull request written in your language, and the icalendar team will use translation tools to translate it to English. You're encouraged to make a reasonable effort, and to request a review of their pull request from community members who are fluent in English to fix grammar and syntax. Please ask! .. note:: More corrections to spellings and Vale's configuration are welcome by submitting a pull request. This is an easy way to become a contributor to icalendar. See :ref:`vale-configuration` for details. .. seealso:: See how to use :ref:`Vale `. collective-icalendar-138c845/docs/contribute/index.rst000066400000000000000000000332671524134033600230270ustar00rootroot00000000000000========== Contribute ========== This guide describes how to contribute to icalendar. A key purpose around icalendar is to foster a warm and welcoming space for people to learn, grow, and contribute. Contributors in the icalendar community find joy in working with other people and raising them up to contribute again and improve. People in the icalendar community strive to uphold the spirit of the Python Calendaring Ecosystem's `Code of Conduct `_. You are invited to read it to help you decide whether you would enjoy being a part of the icalendar community. Examples of how to contribute ----------------------------- - Report security issues per the `Security Policy `_. - Report all other issues in the `issue tracker `_. - Comment on and resolve issues. - Triage open issues and `pull requests `_. - Review, comment on, and make suggestions to change a pull request. See the GitHub documentation `Reviewing proposed changes in a pull request `_. - Submit pull requests from your fork of the icalendar repository. - Extend the :doc:`documentation/index`. - Create or comment on a topic in `Discussions `_. - Write a blog post about icalendar. - Share announcements on social media from :doc:`core contributors ` to icalendar. - Sponsor development of icalendar through `Open Collective `_. .. _first-time-contributors: First-time contributors ----------------------- Contributions to icalendar from people new to icalendar are welcome. Like most free and open source software, icalendar promotes the "free" part, meaning freedom or liberty. - You're free to start work on issues without asking. - You're free to ask questions to clarify the scope of work before you start. - We don't assign issues. - Free other contributors from duplicating your effort by opening a pull request as soon as you can. For people participating in programs to encourage first-time contributions, including `Hacktoberfest `_, `Good First Issue `_, and `Up For Grabs `_, you must comply with their terms and conditions to receive their recognition or rewards. Accordingly, you may request of the maintainers in your pull request that you would like recognition for your contribution. .. seealso:: Find open issues. .. grid:: 1 2 3 4 .. grid-item:: .. button-link:: https://github.com/collective/icalendar/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22 :color: primary :shadow: Good First Issue .. grid-item:: .. button-link:: https://github.com/collective/icalendar/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22doc%22 :color: success :shadow: Documentation .. grid-item:: .. button-link:: https://github.com/collective/icalendar/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahacktoberfest :color: warning :shadow: Hacktoberfest .. _artificial-intelligence-policy: Artificial intelligence policy ------------------------------ We want to protect the joy, goodwill, and volunteer time of the maintainers and contributors of icalendar. Pull request reviewers want to train people, not AI. As such, we take a strong stance against artificial intelligence (AI) abuse. Contributors to icalendar must follow icalendar's AI policy as described in this section. .. _responsible-ai-use: Responsible AI use `````````````````` You may responsibly use AI as a tool to draft a pull request. That means you must comply with :ref:`pull-request-requirements` and follow the Python Calendaring Ecosystem's `Code of Conduct `_. If you use AI in your work: - Before you begin work, you must open an issue per :ref:`issue-requirements`. - In your git commit messages, you must specify both (1) which AI model and version you used, and (2) how you used it, by either including the prompts and interactions you used or summarizing them. See the example commit message below. .. code-block:: text Author: Parker Programmer with CodeLLM-3.4 pp@example.org Date: Sun Jan 18 10:52:08 2026 Generate compliance tests Prompt: Generate tests for compliance with RFC123 messages. Output: (this commit) You can automate this by using the :program:`ai-prompt-auto-commit` pre-commit hook. See :ref:`AI prompt automation ` for setup instructions. - You must disclose that you used AI in your change log entry. This may be a brief disclosure, not as detailed as the git commit messages, such as, "I used AI to assist me with this change." - You must take responsibility for the output, including reviewing and validating the output for accuracy and ensuring it resolves an issue. - You must check the AI's terms of use, and ensure that outputs are not reconstructed from copyrighted sources. - You are expected to understand and be able to explain design and code decisions. - You shall be held accountable for your AI-generated content. AI abuse ```````` You may not abuse AI to generate a pull request that is disruptive to the icalendar community or does not adhere to :ref:`responsible-ai-use` described in the previous subsection. Examples of such abuse and irresponsible use include the following actions. - You claim no responsibility for the output of AI generated content. - Your pull request demonstrates no understanding or thought whatsoever to solve an issue. - Your pull request plagiarizes copyrighted or other material to which you have no legal claim. - You ignore or don't respond to feedback. - The GitHub account is itself an AI agent. Report suspected violations ``````````````````````````` To report a suspected violation of this AI policy, see the `Reporting an issue `_ section in the Python Calendaring Ecosystem's Code of Conduct. The maintainers will investigate and collect information from various sources, including but not limited to the use of automated GitHub workflows to identify suspected AI use. The maintainers may close pull requests without providing feedback that they deem to be spam, AI slop, abuse, or that do not comply with :ref:`pull request requirements `. The maintainers may also take further action, including suspend, ban, or report GitHub users, as described in Python Calendaring Ecosystem's `Code of Conduct `_. .. _issue-requirements: Issue requirements ------------------ An issue should precede a pull request. When `creating a new issue `_, you'll be presented with an issue selector. Follow the templates guidance as much as practical. It might be helpful to include an iCalendar file to help reproduce your issue and to use in a test to ensure a bug fix actually resolves your issue. To remove private information from an iCalendar file, use :program:`icalendar-anonymizer`, either through its `website `_ or install it locally. See icalendar-anonymizer's `Installation `_ and documentation for details. Anyone may comment on an issue. Discussion of the issue helps collaborators understand its importance and how to resolve it. .. _pull-request-requirements: Pull request requirements ------------------------- When submitting your pull request, complete the description template on GitHub, and ensure you have met the following requirements. #. In your pull request description, link to relevant issues. #. Add a change log entry as described in :ref:`change-log`. This is required and enforced by GitHub checks. #. Add a test which proves your fix and passes. #. Run all tests to ensure your changes don't break any existing functionality. #. :doc:`Add or edit documentation `, both as docstrings to be rendered in the :doc:`API reference documentation <../reference/api/icalendar>` and narrative documentation, as necessary. The maintainers may close pull requests without providing feedback that they deem to be spam, AI slop, abuse, or that do not comply with pull request requirements. The maintainers may also take further action, including suspend, ban, or report GitHub users, as described in Python Calendaring Ecosystem's `Code of Conduct `_. .. _change-log: Change log requirements ----------------------- This section describes how to write a change log entry that satisfies the requirements of its :ref:`file name ` according to :ref:`types ` and which :ref:`summarizes changes ` in your contribution, including compliance with icalendar's :ref:`responsible-ai-use` policy. .. _change-log-file-name: Change log entry file name `````````````````````````` To create a change log entry or news item, create a file in the :file:`news` directory, located in the root of the package. .. important:: Never edit a change log entry that you didn't create. The change log entry's format must be ``#.type``, where ``#`` is the referenced GitHub issue or pull request number, ``.`` is the literal extension delimiter, and ``type`` is one of the following strings described in the next section, :ref:`change-log-types` To avoid a filename conflict with an existing file or another pull request for the same issue number, append a period (``.``) and an integer to the filename, incrementing it as needed to make the entire filename unique. .. code-block:: text 1158.documentation 1158.documentation.1 1158.documentation.2 For orphan change log entries—that is, those that don't need to be linked to any issue ID or other identifier—start the file name with ``+``. The content will still be included in the change log, at the end of the category corresponding to the file extension. .. code-block:: text +anything.bugfix .. note:: icalendar uses `towncrier `_ to automatically update the :doc:`../reference/changelog` from entries stored in the :file:`/news` directory at the root of the project. It generates links to the issue numbers and organizes the change log entries according to their filename issue numbers and types for each release. .. _change-log-types: Change log types ~~~~~~~~~~~~~~~~ ``breaking`` For changes that break the existing API. ``removal`` For removals and deprecations. ``feature`` For new functionality. ``bugfix`` For bug fixes and error corrections. ``security`` For security fixes. These must go through the `security report protocol `_. ``documentation`` For changes to the documentation, docstrings, spelling dictionary, or any other documentation matter. ``deps`` For changes to project dependencies. ``internal`` For internal changes, such as issue templates. ``chore`` For routine tasks that shouldn't be published, but will satisfy the checker for the presence of a change log entry. .. _write-a-good-change-log-entry-label: Write a good change log entry ````````````````````````````` .. important:: These change log entries become narrative documentation. The content of this file must include the following. - A brief message that summarizes the changes in your contribution. - Use :ref:`reStructuredText markup ` to link to relevant RFCs, API usage, and other references. - A brief disclosure of AI use, per icalendar's :ref:`responsible-ai-use` policy, if applicable. - An attribution to yourself, in the format of ``@github_username``, at the end of the entry. You can write a good change log entry with the following guidance. - Use a narrative format, in the past tense, proper English spelling and grammar, and inline markup as needed. - Write your change log entry for its appropriate audience. - Most entries should address *users* of the software. - An entry for a change to a public API should address *developers*. - If you fix a bug, write what was broken and is now fixed. You should not write *how* you fixed it. - If you add or change a feature or public API, write a summary of previous behavior, what it does now, and how to use it. - Refer to narrative documentation as needed. The following text is an example of a good change log entry, placed inside :file:`/news/1360.feature`. .. code-block:: rst Created a :meth:`~icalendar.prop.recur.weekday.vWeekday.ical_value` property for the :class:`~icalendar.prop.recur.weekday.vWeekday` component, mirroring the existing pattern on :class:`~icalendar.prop.boolean.vBoolean`. @mvanhorn The following would be a poor change log entry. .. code-block:: rst Fix #123456 by chaning config of file [did_not_read_this_guide] Set up for development ---------------------- If you would like to set up icalendar to contribute changes, see :doc:`development`. .. toctree:: :hidden: documentation/index development join-the-icalendar-team code-of-conduct credits maintenance collective-icalendar-138c845/docs/contribute/join-the-icalendar-team.rst000066400000000000000000000104341524134033600262700ustar00rootroot00000000000000======================= Join the icalendar team ======================= This guide describes how to join the icalendar team. Purpose ------- The icalendar team reviews pull requests and issues for quality, makes releases to PyPI, and protects and promotes the icalendar community. To familiarize yourself with the icalendar community, read :doc:`index`. To encourage collaboration and community, the icalendar team is actively seeking contributors who are interested in helping to review and triage issues and pull requests. Good candidates for the team demonstrate the following qualifications: - Be friendly, humane, and value diversity and inclusion. - Frequently engage with other contributors. - Frequently triage and review pull requests and issues. Members ------- The members of the icalendar team are listed below under their role. Maintainers ``````````` .. include:: ../_include/maintainers.inc Documentation lead `````````````````` `@stevepiercy `_ Triage `````` Apply to add your name here. Apply ----- If you're interested in applying to join the team, you may: - Create a new `Discussion topic `_ under the Collaborator nominations category. - Post a message to the `Matrix icalendar chat `_. Additionally, the icalendar team may contact you to encourage you to apply. The icalendar team recognizes that many individuals might not feel qualified to apply. Please rest assured that the worst that can happen is that the team will say "not yet, but here's a path you can follow to become qualified." The icalendar team wants to lift people up. The icalendar team values diversity, equity, and inclusion. The icalendar team encourages women, people of color, LGBTQ+ individuals, people with disabilities, neuro-divergent people, members of ethnic minorities, residents of countries outside western culture, people of all ages, and people whose primary language is not English to apply. Members of the icalendar team are committed to building a team that is inclusive of a variety of backgrounds and perspectives. Elevated access roles --------------------- The icalendar team may grant elevated levels of access—what GitHub calls "roles"—to the icalendar repository for contributors based on qualifying criteria. GitHub describes `repository roles for an organization `_ in which it defines access levels. Because icalendar is a public open source software repository, anyone with a GitHub account can perform any action granted by the Read role. The Triage role has the following additional permissions: - Apply and dismiss labels. - Close, reopen, and assign all issues and pull requests. - Apply milestones. - Mark duplicate issues and pull requests. - Request pull request reviews. - Hide anyone's comments. - Move a discussion to a different category. - Lock and unlock discussions. - Individually convert issues to discussions. - Delete a discussion. The primary criteria that the icalendar team uses to consider whether to grant elevated access include, but are not limited to: - Previous contributions to icalendar and related projects, especially pull request reviews and issue triage - Previous contributions to other projects - Demonstrated understanding of the RFCs and code base without AI assistance - Two-factor authentication must be enabled on their GitHub account - Strive to uphold the spirit of the Python Calendaring Ecosystem's `Code of Conduct `_ - Join the `#icalendar Matrix chat `_ - Join a video call to get to know each other The icalendar team will review your application, and collect relevant information for evaluation. If the icalendar team determines that you qualify for elevated access, then you'll receive an invitation through an email notification from GitHub to accept. If the team determines you're not yet qualified for elevated access, then you'll receive information about next steps to qualify. collective-icalendar-138c845/docs/contribute/maintenance.rst000066400000000000000000000351741524134033600242010ustar00rootroot00000000000000=========== Maintenance =========== This chapter describes the maintenance structure of icalendar. icalendar Maintainers --------------------- Currently the maintainers are the following people. .. include:: ../_include/maintainers.inc Maintainers need the following permissions. ``Admin`` access to the `repository `_. These can be enabled by a current maintainer or a GitHub organization administrator in the `settings `_. ``Maintainer`` or ``Owner`` access to the `PyPI project `_. Each owner and maintainer needs a PyPI account. All PyPI accounts require two-factor authentication (2FA) enabled. Owners can invite either new owners or maintainers in the `collaboration Section `_ on PyPI. ``Maintainer`` access to the `Read the Docs project `_. Existing maintainers can invite another maintainer through the `Maintainers `_ page. ``Environments/Configure PyPI`` access for GitHub Workflows to grant new releases from tags. Organization owners and repository administrators can grant this access in :menuselection:`Settings --> Environments --> PyPI`, or at https://github.com/collective/icalendar/settings/environments/674266024/edit, by adding their GitHub username to the list of :guilabel:`Required Reviewers`. ``Owner`` or ``Manager`` access to ``icalendar-coc@googlegroups.com`` This Google Group is used for managing `Code of Conduct `_ infringement reports. ``Manager``\ s may manage and moderate messages, whereas ``Owner``\ s may also manage members. Management is performed through `Google Groups icalendar-coc settings `_. ``Registered`` access to the `OSS Fuzz issue tracker `_ for icalendar. icalendar contributors use this issue tracker for managing :doc:`../how-to/fuzz-testing` issues that arise from time to time. New issues do not get immediately disclosed to the public, and require that you register with a Google Account. Add your Google Account's email address to `google/oss-fuzz `_, and create a pull request, to request the following access: - instant notification about fuzzing errors - undisclosed fuzzing issues Existing issues will be disclosed after some time to the public. .. seealso:: - `Discussion about how to be added to OSS Fuzz `_ - :issue:`562` Maintainer in :file:`pyproject.toml` Maintainers should be mentioned with or without email address in the :file:`pyproject.toml` file's `maintainers' section `_. Collaborators ------------- Collaborators have write access to the repository. As a collaborator, you can - merge pull requests, - initiate a new release, and - become a :ref:`code-owner`. The maintainers of icalendar want to have as many knowledgeable people with write access taking responsibility as possible for these reasons: - a constant flow of fixes and new features - better code review - lower bus factor and backup - future maintainers Nobody should merge their own pull requests. If you like to review or merge pull requests of other people and you have shown that you know how to contribute, you can ask to become a collaborator. A maintainer may ask if you would like to become one. .. _code-owner: Code owner ---------- A code owner is a type of collaborator or maintainer who is responsible for a specific part of the code. Code owners are automatically requested for review when someone opens a pull request that modifies code that they own. You may ask, or be invited, to become a code owner as part of becoming a collaborator or maintainer. When doing so, you or the inviter may submit a pull request to update the :file:`.github/CODEOWNERS` file. .. seealso:: `About code owners `_ Release versions ---------------- .. seealso:: :doc:`/reference/versions-branches` New releases ------------ This section explains how to create a new release on `PyPI `_. Its examples were used for the 7.0.0 release of icalendar, a major release. Adjust the examples for the current release as needed. Since collaborators and maintainers have write access to the repository, they can start the release process. However, only people with ``Environments/Configure PyPI`` access can approve an automated release to PyPI. #. Update the Windows to Olson mapping in the :mod:`~icalendar.timezone.windows_to_olson` module. .. code-block:: shell make wo If there are any changes to the module, then open a pull request and merge it. #. Set an environment variable to use in subsequent commands during the release process. .. code-block:: shell export VERSION=7.0.0 #. Update the ``main`` branch. .. code-block:: shell git checkout main git pull #. If you'll create a new *major* release, update :file:`.github/workflows/tests.yml` to include the new release branch in the list of branches that trigger the CI tests. .. code-block:: yaml :emphasize-lines: 4 push: branches: - main - 7.x tags: - v* .. _hide-version-warning-banner: #. Hide the version warning banner on the "stable" version on Read the Docs. Update the Sphinx configuration file :file:`docs/conf.py`, which you'll commit and tag. Then Read the Docs will recognize the highest version tag as the "stable" version. .. note:: Toward the end of this process, you'll revert this setting on ``main``, so that the "latest" version on Read the Docs continues to display the version warning banner. .. code-block:: python html_theme_options = { # ... "show_version_warning_banner": False, #. Update the change log :file:`CHANGES.rst` with the change log entries in :file:`/news` using `towncrier `_. .. code-block:: shell make changes #. Add the changes, create a commit on the ``main`` branch, and push the changes to prepare a release of this version. .. code-block:: shell git add CHANGES.rst news/ git add .github/workflows/tests.yml # Only for a new major release git commit -m"version $VERSION" git push # to collective/icalendar #. See if the `CI tests `_ are running on the commit. If they are not running, no new release can be issued. If the CI passes, go ahead. #. Create a tag for the release on its release branch ``*.x``, push, and make sure the `CI tests`_ are running. a. First, make sure you're on the ``main`` branch and it's current, in case someone else updated ``main`` while tests ran. .. code-block:: shell git checkout main git pull #. Next, depending on the release type, do one of the following. - For a major release, create a new branch, and check it out. .. code-block:: shell git switch -c 7.x - For a minor or patch release, check out the existing branch, and update it. .. code-block:: shell git checkout 7.x git pull #. Next, merge ``main`` into the release branch. .. code-block:: shell git merge main #. Next, push changes upstream, changing the command according to the release type. - For a major release, push, create, and track the upstream branch. .. code-block:: shell git push -u upstream 7.x # to collective/icalendar - For a minor or patch release, just push. .. code-block:: shell git push # to collective/icalendar #. Next create a tag, and push the tag. .. code-block:: shell git tag "v$VERSION" git push upstream "v$VERSION" # to collective/icalendar .. warning:: Once a tag is pushed to the repository, it must not be re-tagged or deleted. This creates issues for downstream repositories. See :issue:`1033`. #. Once the tag is pushed and its `CI tests`_ pass, check the `GitHub Actions `_, and wait for maintainers to get an email: .. code-block:: text Subject: Deployment review in collective/icalendar tests: PyPI is waiting for your review #. If the release is approved by a maintainer, it will be pushed to `PyPI`_. Don't wait for that, continue. #. Update the version switcher file :file:`docs/_static/version-switcher.json`. .. note:: This file is configured in :file:`docs/conf.py` on *all* branches to use the version on the ``main`` branch, which is "latest" on Read the Docs. .. code-block:: python json_url = "https://icalendar.readthedocs.io/en/latest/_static/version-switcher.json" The following examples were used for the 7.0.0 release. a. Check out the ``main`` branch and update it. .. code-block:: shell git checkout main git pull #. When cutting a new *major* release version, update :file:`docs/_static/version-switcher.json` to match that version. - Duplicate the second previous major version stanza and renumber it to the first previous version. In other words, duplicate the ``5.x`` stanza, and renumber the copy to ``6.x``. .. code-block:: json { "version": "6.x", "url": "https://icalendar.readthedocs.io/en/6.x/" }, - Next, edit the array item for the previous version to reflect the current major release. .. vale off .. code-block:: json :emphasize-lines: 2-3 { "name": "7.x (stable)", "version": "v7.0.0", "url": "https://icalendar.readthedocs.io/en/stable/", "preferred": "true" }, .. vale on #. When cutting a *minor* or *patch* release version, update :file:`docs/_static/version-switcher.json` to match that version's tag name. .. vale off .. code-block:: json :emphasize-lines: 2-3 { "name": "7.x (stable)", "version": "v7.0.1", "url": "https://icalendar.readthedocs.io/en/stable/", "preferred": "true" }, .. vale on #. For all releases, add, commit, and push the changes. .. code-block:: shell git add docs/_static/version-switcher.json git commit -m "Update version switcher for $VERSION" git push # to collective/icalendar #. Revert the change in the :ref:`earlier step ` by updating the Sphinx configuration file :file:`docs/conf.py` to show the version warning banner on the ``main`` branch, which is "latest" on Read the Docs. .. code-block:: shell git checkout main git pull .. code-block:: python html_theme_options = { # ... "show_version_warning_banner": True, .. code-block:: shell git add docs/conf.py git commit -m "Restore version warning banner for 'latest' on Read the Docs" git push # to collective/icalendar #. If you cut a new *major* release version, update the Sphinx configuration file :file:`docs/conf.py` on the *previous* numbered major release branch to show the version warning banner. For example, when releasing 7.0.0, checkout ``6.x``, and update it as shown. .. code-block:: shell git checkout 6.x git pull .. code-block:: python html_theme_options = { # ... "show_version_warning_banner": True, .. code-block:: shell git add docs/conf.py git commit -m "Show version warning banner for previous major version 6.x on Read the Docs" git push # to collective/icalendar #. Manually trigger the "latest" version on `Read the Docs `_. To the right in the row for "latest," click the ellipsis :guilabel:`…`, and from the pop-up menu select :guilabel:`Rebuild version`. #. Add a comment to each of the issues mentioned in the new release. Example: .. code-block:: text This is included in v7.0.0. Links ----- This section contains useful links for maintainers and collaborators. - To remove private information from an iCalendar file for bug reports, use `icalendar-anonymizer `_ either through its `website `_ or `install it locally `_. - `Python Calendaring Ecosystem `_ - `Future of icalendar, looking for maintainer #360 `_ - `Comment on the Plone tests running with icalendar `_ Updating Python versions ------------------------ When adding support for a new Python version, or removing support for an old one, the following files need to be updated: :file:`.github/workflows/tests.yml` Upgrade Python versions. :file:`.github/workflows/*.py` Add or remove the Python version from the test matrix. :file:`tox.ini` Update the ``envlist`` to include or remove the Python version. :file:`pyproject.toml` Update the ``requires-python`` line and the ``classifiers`` list. :file:`docs/reference/versions-branches.rst` Update the compatibility information. :file:`docs/maintenance.rst` Update this list if any new files need to be modified. `Branch Protection Rules `_ Update the branch protection rules so that they match the required test names. Remember to write tests that completely cover the changes, and update any documentation that mentions supported Python versions. collective-icalendar-138c845/docs/explanation/000077500000000000000000000000001524134033600213175ustar00rootroot00000000000000collective-icalendar-138c845/docs/explanation/api-design.rst000066400000000000000000000252471524134033600241030ustar00rootroot00000000000000========== API design ========== This chapter describes the API design of icalendar. icalendar's API aligns with iCalendar components and properties as defined in :rfc:`5545` and related RFCs. .. seealso:: :doc:`../reference/rfc-support` This chapter begins with a description of the structure of an iCalendar file, then proceeds into details of its parts. At their core, iCalendar components are dictionaries with case insensitive keys. The preferred way to access their values is through lower case properties as described in :ref:`property-access`. Advantages and disadvantages of the other approaches are discussed below. .. _icalendar-files: iCalendar files =============== iCalendar files have both a format and a structure. .. _icalendar-file-format: iCalendar file format ===================== An iCalendar file is a text file with UTF-8 character encoding in a special format. It consists of content lines, with each content line defining a :term:`property` that has three parts: name, parameters, and values. Parameters are optional. The following examples illustrate the file structure. The following example iCalendar file consists of a single content line, with only a name and value. .. code-block:: ics BEGIN:VCALENDAR The next example iCalendar file consists of a property with a name, parameters separated by a semicolon (``;``), and a value. .. code-block:: ics ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:example@example.com In the previous iCalendar file example, its parts are the following. name ``ATTENDEE`` parameters ``CN=Max Rasmussen;ROLE=REQ-PARTICIPANT`` value ``MAILTO:example@example.com`` For long content lines, icalendar usually "folds" them to less than 75 characters. .. _icalendar-file-structure: iCalendar file structure ======================== An iCalendar file's structure has :term:`components` and may have nested subcomponents. A component consists of one or more properties with values. The values have special types, including integer, text, and datetime. These values are encoded in a special text format in an iCalendar file. icalendar contains methods for converting to and from these encodings. The following example is a ``VCALENDAR`` component representing a calendar. .. code-block:: ics BEGIN:VCALENDAR ... vcalendar properties ... END:VCALENDAR The most frequent subcomponent to a ``VCALENDAR`` component is a ``VEVENT``. This following example is a ``VCALENDAR`` component with a nested ``VEVENT`` subcomponent. .. code-block:: ics BEGIN:VCALENDAR ... vcalendar properties ... BEGIN:VEVENT ... vevent properties ... END:VEVENT END:VCALENDAR .. _api-design-components: Components ========== Components are the building blocks of an iCalendar file. Typically, you would create a calendar, then create a component, such as an event, and add the event component to the calendar. Components may be nested. Components are represented in icalendar as Python classes. icalendar offers shortcuts to import its classes. This is the preferred, stable, public interface. Avoid using their fully qualified Python path, as these paths may change, and break your project. .. _property-access: Property access =============== As described in the foregoing section, icalendar builds components and subcomponents. It then adds, modifies, or removes properties to components and subcomponents, all within the RFC requirements. All property value data types specified by :rfc:`5545#section-3.3` and subsequent RFCs can be found in :mod:`icalendar.prop`. Accessing components by their properties is preferred over using the :ref:`dictionary-access` for the following reasons. * Values are checked for correctness. * Values are converted to the correct type. * Properties and parameters are typed for type checking and auto completion in editors when writing code that uses the icalendar package. * Default values require fewer logical coding checks for presence. The example below creates an event with useful default values, using :meth:`~icalendar.cal.event.Event.new`. :rfc:`5545` requires ``DTSTAMP`` and ``UID`` values to be present. They are automatically added. .. code-block:: pycon :emphasize-lines: 3-6, 11-12 >>> from icalendar import Event >>> from datetime import date >>> event = Event.new( ... summary="New Year's Day Celebration", ... start=date(2022, 1, 1) ... ) >>> print(event.to_ical()) BEGIN:VEVENT SUMMARY:New Year's Day Celebration DTSTART;VALUE=DATE:20220101 DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT Based on the created event above, the following example shows how to access values by their properties. .. code-block:: pycon >>> print(event.summary) New Year's Day Celebration >>> print(event.start) 2022-01-01 Property naming convention ========================== Property names are either in lower case or upper case. - Upper case property names refer simultaneously to both the Python component property name and the iCalendar :rfc:`5545` property name. For example, ``DTEND`` occurs in :rfc:`5545#section-3.6.1` and as a property in :class:`~icalendar.cal.event.Event`. - Lower case property names refer to only the Python component property name. These properties are calculated from one or more upper case properties. For example, :attr:`~icalendar.cal.event.Event.end` is calculated either from :attr:`~icalendar.cal.event.Event.DTEND` or from both :attr:`~icalendar.cal.event.Event.DTSTART` and :attr:`~icalendar.cal.event.Event.DURATION`. All properties can be accessed as attributes. Lower case properties --------------------- Continuing from the previous example, the next example shows how to access values by attributes. .. code-block:: pycon >>> print(event.summary) New Year's Day Celebration While some values are not set, they can be calculated from other values. The event in this example has a duration of one day. .. code-block:: pycon >>> event.start datetime.date(2022, 1, 1) >>> event.duration datetime.timedelta(days=1) >>> event.end datetime.date(2022, 1, 2) While some attributes might be empty, they can have useful default values. .. code-block:: pycon >>> event.rdates # no RDATE is set [] Upper case properties --------------------- Upper case properties are either empty, or :obj:`None`, if they're not set. They can be accessed as attributes. .. code-block:: pycon >>> event.DTSTART datetime.date(2022, 1, 1) >>> print(event.DURATION) None >>> print(event.DTEND) None .. _parameter-properties: Parameter properties -------------------- Parameters can be accessed using specified properties. The following example creates a new attendee for the previously created event from above. .. code-block:: pycon >>> from icalendar import vCalAddress, ROLE >>> attendee = vCalAddress.new( ... "maxm@example.com", ... cn="Max Rasmussen", ... role=ROLE.REQ_PARTICIPANT ... ) >>> print(attendee.ROLE) REQ-PARTICIPANT Similar to the casing of names of properties, lower case parameters calculate the property, whereas upper case parameters directly access the iCalendar property. .. code-block:: pycon >>> print(attendee.email) # calculated maxm@example.com >>> print(attendee.CN) # direct access Max Rasmussen The parameters turn up the iCal representation. .. code-block:: pycon >>> event.attendees = [attendee] >>> print(event.to_ical()) BEGIN:VEVENT SUMMARY:New Year's Day Celebration DTSTART;VALUE=DATE:20220101 DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:mailto:maxm@example.com END:VEVENT .. _dictionary-access: Dictionary access ================= The lowest level is the dictionary interface. It is stable since version 4.0. Although it's possible to directly set :obj:`str` values, it's preferred to use :meth:`~icalendar.cal.component.Component.add()` instead. As mentioned in :ref:`property-access` above, some properties won't get set or validated through this method. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar() # create and empty calendar >>> calendar.add("prodid", "-//My calendar product//mxm.dk//") >>> calendar.add("version", "2.0") >>> print(calendar.to_ical()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//My calendar product//mxm.dk// END:VCALENDAR The component is a dictionary, so you can access properties by key. All keys are case insensitive. This is implemented in :class:`~icalendar.caselessdict.CaselessDict`. .. code-block:: pycon >>> calendar["version"] == calendar["VERSION"] True The values can be retrieved by using :meth:`~icalendar.cal.component.Component.get()`. The values are encoded as property values specified in :mod:`icalendar.prop`. .. code-block:: pycon >>> calendar.get("version") vText(b'2.0') >>> print(calendar.get("name")) None Parameter dictionary -------------------- All property values are defined in :mod:`icalendar.prop` and have parameters. :class:`~icalendar.prop.text.vText` is used for ``VERSION``, ``PRODID``, and ``DESCRIPTION``. Preferably, use the :ref:`parameter-properties` to get and set the values. You can also access them using ``.params``. The following example sets the ``DESCRIPTION`` from :rfc:`7986#section-5.1`, and adds a ``LANGUAGE`` parameter: .. code-block:: pycon >>> from icalendar import vText >>> description = vText("This is my personal calendar.") >>> description.params["language"] = "en" # set language to English >>> calendar["description"] = description >>> print(calendar.to_ical()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//My calendar product//mxm.dk// DESCRIPTION;LANGUAGE=en:This is my personal calendar. END:VCALENDAR .. _import-shortcuts: Import shortcuts ================ icalendar offers shortcuts to import its Python classes, which align with their counterpart iCalendar components. The iCalendar :rfc:`5545` refers to components, such as events and alarms. In the foregoing examples, you might have noticed imports such as the following. .. code-block:: pycon >>> from icalendar import Event By virtue of placing imports into each module's :file:`__init.py__` file, it brings in its Python classes and functions to the root of the icalendar package. Without this convenience, imports would require the fully qualified Python path. .. code-block:: pycon >>> from icalendar.cal.event import Event This provides a nice API for developers, and establishes consistency across the API, ensuring backward compatibility as much as practical. collective-icalendar-138c845/docs/explanation/index.rst000066400000000000000000000003641524134033600231630ustar00rootroot00000000000000=========== Explanation =========== This part of the documentation explains concepts of icalendar, the Python package, and its relationship to the iCalendar standards and other related standards. .. toctree:: :maxdepth: 2 api-design collective-icalendar-138c845/docs/how-to/000077500000000000000000000000001524134033600202125ustar00rootroot00000000000000collective-icalendar-138c845/docs/how-to/attendees.rst000066400000000000000000000067311524134033600227270ustar00rootroot00000000000000=============== Event attendees =============== This chapter explains how to use the event attendee information in iCalendar files with the icalendar library. Attendees are present inside of events, alarms, and other calendar components, and occur as a :class:`~icalendar.prop.cal_address.vCalAddress`. .. seealso:: :attr:`Event.attendees ` Add attendees to an event ------------------------- To add attendees to an event in icalendar, import the required classes. .. code-block:: pycon >>> from icalendar import Event, vCalAddress, CUTYPE, ROLE, PARTSTAT Then create the attendee object and set its parameters. .. code-block:: pycon >>> attendee = vCalAddress.new( ... "emily.smith@example.com", # email address ... cn="Emily Smith", # common name ... cutype=CUTYPE.INDIVIDUAL, # calendar user type ... role=ROLE.CHAIR, # role ... partstat=PARTSTAT.ACCEPTED, # participation status ... rsvp=True, # RSVP requirement ... ) .. note:: Apart from the email, all parameters are optional. Use the enumerations defined in :mod:`icalendar.enums` to ensure valid values. Finally, add the attendee to the event. .. code-block:: pycon >>> event = Event.new() >>> event.attendees = [attendee] # set the attribute The resulting event looks like this: .. code-block:: pycon >>> print(event.to_ical()) BEGIN:VEVENT DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 ATTENDEE;CN="Emily Smith";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR;R SVP=TRUE:mailto:emily.smith@example.com END:VEVENT Access attendee information --------------------------- After parsing an iCalendar file with :meth:`Calendar.from_ical `, access any of its events. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.example("property_params") # load a calendar >>> event = calendar.events[0] # get the first event Events can have multiple attendees. Access them using the :attr:`~icalendar.cal.event.Event.attendees` property. .. code-block:: pycon >>> attendees = event.attendees # get the attendees The standard parameters of each attendee can be accessed as attributes. The following example prints the email, common name, participation status, role, and RSVP requirement of each attendee. .. code-block:: pycon >>> for attendee in attendees: ... print("Email:", attendee.email) # print the email ... print("- CN:", attendee.CN) # print the common name ... print("- PARTSTAT:", attendee.PARTSTAT) # print the participation status ... print("- ROLE:", attendee.ROLE) # print the role ... print("- RSVP:", attendee.RSVP) # print the RSVP requirement Email: rembrand@xs4all.nl - CN: RembrandXS - PARTSTAT: NEEDS-ACTION - ROLE: REQ-PARTICIPANT - RSVP: True Email: rembrand@daxlab.com - CN: RembrandDX - PARTSTAT: NEEDS-ACTION - ROLE: REQ-PARTICIPANT - RSVP: True Email: rembspam@xs4all.nl - CN: RembrandSB - PARTSTAT: NEEDS-ACTION - ROLE: REQ-PARTICIPANT - RSVP: True All parameters can also be accessed using dictionary syntax. .. code-block:: pycon >>> attendee = attendees[0] # get the first attendee >>> print(attendee.params["CN"]) # print the common name RembrandXS collective-icalendar-138c845/docs/how-to/cli.rst000066400000000000000000000030271524134033600215150ustar00rootroot00000000000000====================== Command line interface ====================== This chapter describes how to use the command line interface (CLI) for icalendar. The CLI consists of the ``icalendar`` command followed by arguments. Help ==== To get information about all the commands, use the ``-h`` option. .. code-block:: shell icalendar -h Read calendar from file ======================= To view a readable summary of an event file that is in iCalendar format, pass the path of the iCalendar file to icalendar. .. code-block:: shell icalendar myfile.ics The following is example output. .. code-block:: text Organiser: Secretary Attendees: John Doe Randy Summary: Yearly evaluation. When: Tue 14 Mar 2017 11:00-12:00 Location: Randy's office Comment: Reminder. Description: Your yearly evaluation is scheduled for next Tuesday. Please be on time. Read calendar from URL ====================== Download the :file:`.ics` file first, then pass it to icalendar. .. code-block:: shell curl -fsSL "https://www.gov.uk/bank-holidays/england-and-wales.ics" -o cal.ics icalendar cal.ics Shell-based mail client ======================= To use icalendar in terminal-based mail clients such as mutt, add a new MIME type as the root user as shown. .. code-block:: shell cat << EOF > /usr/lib/mime/packages/icalendar text/calendar; icalendar '%s'; copiousoutput; description=iCalendar text; priority=2 EOF update-mime collective-icalendar-138c845/docs/how-to/configure.rst000066400000000000000000000002131524134033600227210ustar00rootroot00000000000000========= Configure ========= Use :mod:`icalendar.config` to adjust runtime behavior, such as the cap on ``VALARM`` ``REPEAT`` expansion. collective-icalendar-138c845/docs/how-to/custom-components.rst000066400000000000000000000214501524134033600244430ustar00rootroot00000000000000================= Custom components ================= This chapter describes how to use custom components in icalendar. icalendar automatically handles X-components and IANA-components not in :rfc:`5545`. :rfc:`5545` defines two custom component types. X-Components Vendor-specific components, for example, ``X-MYAPP-SETTINGS``. IANA-Components IANA-registered components, but not in :rfc:`5545`. icalendar preserves all custom components through dynamic component creation using :class:`~icalendar.cal.component_factory.ComponentFactory`. Parse custom components ======================= Parse custom components using either :meth:`Component.from_ical() ` or :meth:`Calendar.from_ical() `. ``Component.from_ical()`` ------------------------- Parse any component type, including custom ones. The following example shows how to parse a component, then display its name and ``SUMMARY``. .. code-block:: pycon >>> from icalendar import Component >>> ics_data = b"""BEGIN:X-MYCOMPONENT ... SUMMARY:Custom component example ... X-CUSTOM-PROP:Some value ... END:X-MYCOMPONENT ... """ >>> component = Component.from_ical(ics_data) >>> component.name 'X-MYCOMPONENT' >>> str(component["SUMMARY"]) 'Custom component example' ``Calendar.from_ical()`` ------------------------ Parse a calendar containing custom components. The following example shows how to parse a component, get its subcomponents, then display its name and ``UID``. .. code-block:: pycon >>> from icalendar import Calendar >>> ics_data = b"""BEGIN:VCALENDAR ... VERSION:2.0 ... PRODID:-//Example//EN ... BEGIN:X-CUSTOM ... UID:custom-1 ... SUMMARY:A custom component ... END:X-CUSTOM ... END:VCALENDAR ... """ >>> cal = Calendar.from_ical(ics_data) >>> custom = cal.subcomponents[0] >>> custom.name 'X-CUSTOM' >>> str(custom["UID"]) 'custom-1' Access custom components ======================== Custom components work exactly like standard components. The following example shows how to access a custom component to display its name and other attributes. .. code-block:: pycon >>> from icalendar import Component >>> # Create custom component using factory >>> MyComp = Component.get_component_class("MYCOMP") >>> comp = MyComp() >>> comp.name 'MYCOMP' >>> >>> # Add properties >>> comp.add("summary", "Test Summary") >>> comp.add("x-custom-field", "Custom Value") >>> >>> # Access properties >>> str(comp["SUMMARY"]) 'Test Summary' >>> str(comp.get("X-CUSTOM-FIELD")) 'Custom Value' >>> >>> # Add subcomponents >>> from icalendar import Event >>> event = Event() >>> event.add("uid", "123") >>> comp.add_component(event) >>> len(comp.subcomponents) 1 Round-trip preservation ======================= Custom components are fully preserved during round-trip parsing. The following example demonstrates this feature. .. code-block:: pycon >>> from icalendar import Calendar >>> original = b"""BEGIN:VCALENDAR ... VERSION:2.0 ... PRODID:-//Test//EN ... BEGIN:X-VENDOR-COMPONENT ... X-VENDOR-PROP:proprietary ... UID:vendor-123 ... END:X-VENDOR-COMPONENT ... END:VCALENDAR ... """ >>> cal = Calendar.from_ical(original) >>> regenerated = cal.to_ical() >>> # Custom component and its properties are preserved >>> b"X-VENDOR-COMPONENT" in regenerated True >>> b"X-VENDOR-PROP" in regenerated True Which ``from_ical()``? ====================== Use ``Component.from_ical()`` for standalone components and fragments. Use ``Calendar.from_ical()`` for complete iCalendar files. It also handles timezones. The following example shows the different usage of both methods. .. code-block:: pycon >>> from icalendar import Component, Calendar >>> >>> # Standalone custom component - use Component.from_ical() >>> standalone = b"""BEGIN:X-MYCOMP ... UID:123 ... END:X-MYCOMP ... """ >>> comp = Component.from_ical(standalone) >>> comp.name 'X-MYCOMP' >>> >>> # Complete calendar - use Calendar.from_ical() >>> calendar_data = b"""BEGIN:VCALENDAR ... VERSION:2.0 ... PRODID:-//Test//EN ... BEGIN:X-MYCOMP ... UID:123 ... END:X-MYCOMP ... END:VCALENDAR ... """ >>> cal = Calendar.from_ical(calendar_data) >>> len(cal.subcomponents) 1 Register custom component classes ================================== Register custom component classes with special behavior. The following example shows how to create and register a custom component with validation and helper methods. .. code-block:: python from icalendar import Component class XAcmeComponent(Component): """Custom ACME component with validation.""" name = "X-ACME" def validate(self): """Validate required properties.""" required = ["UID", "X-ACME-ID"] for prop in required: if prop not in self: raise ValueError(f"Missing {prop}") def get_acme_id(self): """Get ACME ID.""" return self.get("X-ACME-ID") # Register the component Component.register(XAcmeComponent) After registration, parsing ``BEGIN:X-ACME`` uses your custom class: .. code-block:: pycon >>> from icalendar import Component >>> class XAcmeComponent(Component): ... """Custom ACME component with validation.""" ... name = "X-ACME" ... def validate(self): ... """Validate required properties.""" ... required = ["UID", "X-ACME-ID"] ... for prop in required: ... if prop not in self: ... raise ValueError(f"Missing {prop}") ... def get_acme_id(self): ... """Get ACME ID.""" ... return self.get("X-ACME-ID") >>> Component.register(XAcmeComponent) >>> ical_data = b"""BEGIN:X-ACME ... UID:123 ... X-ACME-ID:acme-1 ... END:X-ACME ... """ >>> comp = Component.from_ical(ical_data) >>> comp.validate() >>> str(comp.get_acme_id()) 'acme-1' :rfc:`5545` compliance ====================== The icalendar library is fully compliant with :rfc:`5545` requirements for custom components. Preserves unknown components Custom components are never dropped. Maintains data integrity All properties and subcomponents are preserved. Round-trip safe Parse to serialize, and back to parse, produces equivalent results. No special handling X-components and IANA-components are treated identically. icalendar implements a permissive approach. Rather than rejecting unknown components, it preserves them while making them accessible through the same API as standard components. Nested custom components ======================== Custom components can contain standard components, and vice versa. .. code-block:: pycon >>> from icalendar import Component, Event >>> # Custom component containing a standard event >>> ics_data = b"""BEGIN:X-CONTAINER ... SUMMARY:Container ... BEGIN:VEVENT ... UID:event-1 ... DTSTART:20240101T120000Z ... SUMMARY:Event inside custom component ... END:VEVENT ... END:X-CONTAINER ... """ >>> container = Component.from_ical(ics_data) >>> container.name 'X-CONTAINER' >>> event = container.subcomponents[0] >>> event.name 'VEVENT' >>> str(event["SUMMARY"]) 'Event inside custom component' Use cases ========= Custom components extend the functionality beyond icalendar's core. Use cases include the following: - vendor extensions and proprietary features - experimental or draft component types - legacy system support - data preservation during round-trips The following example shows how to add a feature for a calendar, where an application supports dark mode in its theme. .. code-block:: pycon >>> from icalendar import Calendar, Component >>> cal = Calendar() >>> cal.add("prodid", "-//My App//EN") >>> cal.add("version", "2.0") >>> # Create custom component using factory >>> CustomSettings = Component.get_component_class("X-MYAPP-SETTINGS") >>> custom = CustomSettings() >>> custom.add("uid", "settings-1") >>> custom.add("x-theme", "dark") >>> cal.add_component(custom) >>> b"BEGIN:X-MYAPP-SETTINGS" in cal.to_ical() True Related content =============== :class:`~icalendar.cal.component.Component` Base component class :class:`icalendar.cal.component_factory.ComponentFactory` Component factory :meth:`Component.from_ical ` Parse components :meth:`Calendar.from_ical ` Parse calendars :rfc:`5545` iCalendar specification collective-icalendar-138c845/docs/how-to/fuzz-testing-log.txt000066400000000000000000007255621524134033600242240ustar00rootroot00000000000000``` 2025-11-22T11:51:33.9175548Z Current runner version: '2.329.0' 2025-11-22T11:51:33.9199376Z ##[group]Runner Image Provisioner 2025-11-22T11:51:33.9200136Z Hosted Compute Agent 2025-11-22T11:51:33.9200800Z Version: 20251016.436 2025-11-22T11:51:33.9201366Z Commit: 8ab8ac8bfd662a3739dab9fe09456aba92132568 2025-11-22T11:51:33.9202091Z Build Date: 2025-10-15T20:44:12Z 2025-11-22T11:51:33.9202754Z ##[endgroup] 2025-11-22T11:51:33.9203248Z ##[group]Operating System 2025-11-22T11:51:33.9203806Z Ubuntu 2025-11-22T11:51:33.9204287Z 24.04.3 2025-11-22T11:51:33.9204773Z LTS 2025-11-22T11:51:33.9205222Z ##[endgroup] 2025-11-22T11:51:33.9205756Z ##[group]Runner Image 2025-11-22T11:51:33.9206273Z Image: ubuntu-24.04 2025-11-22T11:51:33.9206787Z Version: 20251112.124.1 2025-11-22T11:51:33.9208111Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20251112.124/images/ubuntu/Ubuntu2404-Readme.md 2025-11-22T11:51:33.9209669Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20251112.124 2025-11-22T11:51:33.9210655Z ##[endgroup] 2025-11-22T11:51:33.9211643Z ##[group]GITHUB_TOKEN Permissions 2025-11-22T11:51:33.9213952Z Metadata: read 2025-11-22T11:51:33.9214626Z SecurityEvents: read 2025-11-22T11:51:33.9215130Z ##[endgroup] 2025-11-22T11:51:33.9217731Z Secret source: None 2025-11-22T11:51:33.9218705Z Prepare workflow directory 2025-11-22T11:51:33.9597124Z Prepare all required actions 2025-11-22T11:51:33.9651456Z Getting action download info 2025-11-22T11:51:34.2947138Z Download action repository 'google/oss-fuzz@master' (SHA:472ccba0e753a44ca318f0fe8aa1b778b53e2c1d) 2025-11-22T11:51:36.0623374Z Download action repository 'actions/upload-artifact@v5' (SHA:330a01c490aca151604b8cf639adc76d48f6c5d4) 2025-11-22T11:51:36.1666956Z Download action repository 'github/codeql-action@v4' (SHA:e12f0178983d466f2f6028f5cc7a6d786fd97f4b) 2025-11-22T11:51:37.1760705Z Complete job name: Fuzzing 2025-11-22T11:51:37.2187767Z ##[group]Build container for action use: '/home/runner/work/_actions/google/oss-fuzz/master/infra/cifuzz/actions/build_fuzzers/../../../build_fuzzers.Dockerfile'. 2025-11-22T11:51:37.2236405Z ##[command]/usr/bin/docker build -t 207f17:c56653790b0c4db091b6057e2ac6f688 -f "/home/runner/work/_actions/google/oss-fuzz/master/infra/cifuzz/actions/build_fuzzers/../../../build_fuzzers.Dockerfile" "/home/runner/work/_actions/google/oss-fuzz/master/infra" 2025-11-22T11:51:40.2140685Z #0 building with "default" instance using docker driver 2025-11-22T11:51:40.2141062Z 2025-11-22T11:51:40.2141247Z #1 [internal] load build definition from build_fuzzers.Dockerfile 2025-11-22T11:51:40.2141635Z #1 transferring dockerfile: 1.26kB 0.0s done 2025-11-22T11:51:40.2141910Z #1 DONE 0.0s 2025-11-22T11:51:40.2142028Z 2025-11-22T11:51:40.2142228Z #2 [internal] load metadata for gcr.io/oss-fuzz-base/cifuzz-base:latest 2025-11-22T11:51:41.2059557Z #2 DONE 1.1s 2025-11-22T11:51:41.3296386Z 2025-11-22T11:51:41.3297021Z #3 [internal] load .dockerignore 2025-11-22T11:51:41.3297752Z #3 transferring context: 125B done 2025-11-22T11:51:41.3298149Z #3 DONE 0.0s 2025-11-22T11:51:41.3298320Z 2025-11-22T11:51:41.3298456Z #4 [internal] load build context 2025-11-22T11:51:41.3298861Z #4 transferring context: 1.88MB 0.1s done 2025-11-22T11:51:41.3299254Z #4 DONE 0.1s 2025-11-22T11:51:41.3299422Z 2025-11-22T11:51:41.3299966Z #5 [1/4] FROM gcr.io/oss-fuzz-base/cifuzz-base:latest@sha256:d137a7eab493c797f9b730fdaed45e9314ec9dc4acc6077ee9a58868b5451696 2025-11-22T11:51:41.3300822Z #5 resolve gcr.io/oss-fuzz-base/cifuzz-base:latest@sha256:d137a7eab493c797f9b730fdaed45e9314ec9dc4acc6077ee9a58868b5451696 done 2025-11-22T11:51:41.3302027Z #5 sha256:b549f31133a955f68f9fa0d93f18436c4a180e12184b999a8ecf14f7eaa83309 0B / 27.50MB 0.1s 2025-11-22T11:51:41.3302638Z #5 sha256:57e06e329388159c4aef03a043b49a3355e8f3eb1639aa35eff74e9467b82d6a 0B / 175B 0.1s 2025-11-22T11:51:41.3303248Z #5 sha256:d137a7eab493c797f9b730fdaed45e9314ec9dc4acc6077ee9a58868b5451696 7.04kB / 7.04kB done 2025-11-22T11:51:41.3303899Z #5 sha256:4c72b8f9e1996ea2bc542d272a87e30e814ef70b9bc31d2d35f233fd27377447 17.16kB / 17.16kB done 2025-11-22T11:51:41.4299858Z #5 sha256:57e06e329388159c4aef03a043b49a3355e8f3eb1639aa35eff74e9467b82d6a 175B / 175B 0.2s done 2025-11-22T11:51:41.4300636Z #5 sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 0B / 83.66MB 0.2s 2025-11-22T11:51:41.4301286Z #5 sha256:3ecf89a071184cf99fd636d5d6d087cc9e7bfca1590391d89ecdf11c4e705202 0B / 2.33MB 0.2s 2025-11-22T11:51:41.5338923Z #5 sha256:b549f31133a955f68f9fa0d93f18436c4a180e12184b999a8ecf14f7eaa83309 10.49MB / 27.50MB 0.3s 2025-11-22T11:51:41.5340093Z #5 sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 15.73MB / 83.66MB 0.3s 2025-11-22T11:51:41.6696910Z #5 sha256:b549f31133a955f68f9fa0d93f18436c4a180e12184b999a8ecf14f7eaa83309 27.50MB / 27.50MB 0.4s done 2025-11-22T11:51:41.6698999Z #5 sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 51.38MB / 83.66MB 0.4s 2025-11-22T11:51:41.6702220Z #5 sha256:98616c90e2c9cba243b8003431853cbd1210679c1996d3aac4d519f571a4f162 0B / 7.51MB 0.4s 2025-11-22T11:51:41.6703225Z #5 extracting sha256:b549f31133a955f68f9fa0d93f18436c4a180e12184b999a8ecf14f7eaa83309 2025-11-22T11:51:41.7724630Z #5 sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 83.66MB / 83.66MB 0.5s 2025-11-22T11:51:41.7726078Z #5 sha256:3ecf89a071184cf99fd636d5d6d087cc9e7bfca1590391d89ecdf11c4e705202 2.33MB / 2.33MB 0.5s done 2025-11-22T11:51:41.8760683Z #5 sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 83.66MB / 83.66MB 0.5s done 2025-11-22T11:51:41.8761471Z #5 sha256:38c8ce617e522855c2a700ff19527183d4e8d0669f346a05db133627f288995e 0B / 3.30kB 0.6s 2025-11-22T11:51:41.8762558Z #5 sha256:1fba94949cc24d0b68d21a858ddca73c0e029272d9d1f0e525b2608fda663dc0 0B / 7.34MB 0.6s 2025-11-22T11:51:41.9766165Z #5 sha256:98616c90e2c9cba243b8003431853cbd1210679c1996d3aac4d519f571a4f162 4.19MB / 7.51MB 0.7s 2025-11-22T11:51:42.0771745Z #5 sha256:98616c90e2c9cba243b8003431853cbd1210679c1996d3aac4d519f571a4f162 7.51MB / 7.51MB 0.7s done 2025-11-22T11:51:42.0775353Z #5 sha256:38c8ce617e522855c2a700ff19527183d4e8d0669f346a05db133627f288995e 3.30kB / 3.30kB 0.7s done 2025-11-22T11:51:42.0779903Z #5 sha256:1fba94949cc24d0b68d21a858ddca73c0e029272d9d1f0e525b2608fda663dc0 7.34MB / 7.34MB 0.8s done 2025-11-22T11:51:42.0781063Z #5 sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 0B / 42.89MB 0.8s 2025-11-22T11:51:42.0782107Z #5 sha256:21a3de1cbbd552f73677e9ecbc57b6e1e5210758dbcf31343e2030b4c6fd571a 0B / 218.81kB 0.8s 2025-11-22T11:51:42.0783120Z #5 sha256:adcd2f319a3963b2653ebc5b3968339fa9302e2d9c816d155a9b121b949d3808 0B / 332B 0.8s 2025-11-22T11:51:42.2296147Z #5 sha256:21a3de1cbbd552f73677e9ecbc57b6e1e5210758dbcf31343e2030b4c6fd571a 218.81kB / 218.81kB 1.0s done 2025-11-22T11:51:42.2297275Z #5 sha256:adcd2f319a3963b2653ebc5b3968339fa9302e2d9c816d155a9b121b949d3808 332B / 332B 0.9s done 2025-11-22T11:51:42.2298226Z #5 sha256:bf0f7f962c9ba2c22c273b854bdccd793599ca64dcd61952665baee7130653f2 0B / 3.34kB 1.0s 2025-11-22T11:51:42.3404047Z #5 sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 8.39MB / 42.89MB 1.1s 2025-11-22T11:51:42.4587872Z #5 sha256:3f1021e01b645a5ab966de745ea025285a16fae1aa8a9caa37cc72fb8aa28ee2 0B / 704B 1.1s 2025-11-22T11:51:42.4589092Z #5 sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 29.27MB / 42.89MB 1.2s 2025-11-22T11:51:42.4590286Z #5 sha256:bf0f7f962c9ba2c22c273b854bdccd793599ca64dcd61952665baee7130653f2 3.34kB / 3.34kB 1.2s done 2025-11-22T11:51:42.4591355Z #5 sha256:3f1021e01b645a5ab966de745ea025285a16fae1aa8a9caa37cc72fb8aa28ee2 704B / 704B 1.2s done 2025-11-22T11:51:42.4592216Z #5 sha256:b34531bbb6407c37a2cb406789910078de9c1577ff3aefc56e35f0a2754f9bfb 0B / 5.16MB 1.2s 2025-11-22T11:51:42.4593261Z #5 sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 0B / 47.03MB 1.2s 2025-11-22T11:51:42.6057687Z #5 extracting sha256:b549f31133a955f68f9fa0d93f18436c4a180e12184b999a8ecf14f7eaa83309 0.9s done 2025-11-22T11:51:42.6058815Z #5 sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 42.89MB / 42.89MB 1.2s done 2025-11-22T11:51:42.8297239Z #5 sha256:a77212fc188d8b3d356b21b910a8c2a3af4a2fe454b4cc0064f2c40f82a42758 0B / 9.29kB 1.3s 2025-11-22T11:51:42.8298518Z #5 extracting sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 2025-11-22T11:51:42.8299448Z #5 sha256:b34531bbb6407c37a2cb406789910078de9c1577ff3aefc56e35f0a2754f9bfb 5.16MB / 5.16MB 1.5s done 2025-11-22T11:51:42.8300468Z #5 sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 13.63MB / 47.03MB 1.6s 2025-11-22T11:51:42.8301515Z #5 sha256:a77212fc188d8b3d356b21b910a8c2a3af4a2fe454b4cc0064f2c40f82a42758 9.29kB / 9.29kB 1.5s done 2025-11-22T11:51:42.8302314Z #5 sha256:5566081492aa65e7813031426c391f7c6ae30f9c1075d3b519c3352d1933b51f 0B / 1.01kB 1.6s 2025-11-22T11:51:42.8302961Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 0B / 102.25MB 1.6s 2025-11-22T11:51:43.0301359Z #5 sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 36.72MB / 47.03MB 1.8s 2025-11-22T11:51:43.0302490Z #5 sha256:5566081492aa65e7813031426c391f7c6ae30f9c1075d3b519c3352d1933b51f 1.01kB / 1.01kB 1.6s done 2025-11-22T11:51:43.0303398Z #5 sha256:3c578b5f0ee26865e66d9064bf236fecc9d5b17454b59b598f9ae77594dfaa13 0B / 978B 1.8s 2025-11-22T11:51:43.1579308Z #5 sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 47.03MB / 47.03MB 1.9s done 2025-11-22T11:51:43.1581119Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 10.49MB / 102.25MB 1.9s 2025-11-22T11:51:43.1582203Z #5 sha256:3c578b5f0ee26865e66d9064bf236fecc9d5b17454b59b598f9ae77594dfaa13 978B / 978B 1.8s done 2025-11-22T11:51:43.1583211Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 0B / 139.23MB 1.9s 2025-11-22T11:51:43.1584465Z #5 sha256:0e006b8e9370e442d0839533b84c61083bb9fad8e971821bfad1c763b5f054a2 0B / 759.28kB 1.9s 2025-11-22T11:51:43.2695537Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 41.94MB / 102.25MB 2.0s 2025-11-22T11:51:43.4122139Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 67.11MB / 102.25MB 2.1s 2025-11-22T11:51:43.4124106Z #5 sha256:0e006b8e9370e442d0839533b84c61083bb9fad8e971821bfad1c763b5f054a2 759.28kB / 759.28kB 2.1s done 2025-11-22T11:51:43.4125146Z #5 sha256:a5acae780e6ee608bfe4be73937b4d29961f6734292a8920811a5124a9edaaaf 0B / 784B 2.1s 2025-11-22T11:51:43.5162465Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 83.89MB / 102.25MB 2.2s 2025-11-22T11:51:43.5163730Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 8.39MB / 139.23MB 2.2s 2025-11-22T11:51:43.6173016Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 35.65MB / 139.23MB 2.3s 2025-11-22T11:51:43.6174524Z #5 sha256:a5acae780e6ee608bfe4be73937b4d29961f6734292a8920811a5124a9edaaaf 784B / 784B 2.3s done 2025-11-22T11:51:43.6175685Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 0B / 72.63MB 2.3s 2025-11-22T11:51:43.7210899Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 98.89MB / 102.25MB 2.4s 2025-11-22T11:51:43.7212056Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 60.82MB / 139.23MB 2.4s 2025-11-22T11:51:43.8296950Z #5 sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 102.25MB / 102.25MB 2.4s done 2025-11-22T11:51:43.8299772Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 112.89MB / 139.23MB 2.6s 2025-11-22T11:51:43.8300915Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 6.19MB / 72.63MB 2.6s 2025-11-22T11:51:43.8301934Z #5 sha256:95711837916f6af5b3d0367df43b9543a012c7fbaf9c0f68a8483cc79cea81ef 0B / 7.43MB 2.6s 2025-11-22T11:51:43.9302084Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 139.23MB / 139.23MB 2.7s 2025-11-22T11:51:43.9309241Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 13.94MB / 72.63MB 2.7s 2025-11-22T11:51:44.0585131Z #5 sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 139.23MB / 139.23MB 2.7s done 2025-11-22T11:51:44.0586464Z #5 sha256:95711837916f6af5b3d0367df43b9543a012c7fbaf9c0f68a8483cc79cea81ef 7.43MB / 7.43MB 2.7s done 2025-11-22T11:51:44.0587535Z #5 sha256:5fda42719bb7f5b81a129d414c4aa6a37d5bd4be7bb91f6a52382ba4cb8e9bf6 0B / 13.41MB 2.8s 2025-11-22T11:51:44.0588142Z #5 sha256:d2ea87f44aba6488185e17100b2f5dfe99d2d35916fcd868903fddfc94ab62fc 0B / 369B 2.8s 2025-11-22T11:51:44.1823315Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 29.36MB / 72.63MB 2.9s 2025-11-22T11:51:44.3298193Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 56.62MB / 72.63MB 3.1s 2025-11-22T11:51:44.3299528Z #5 sha256:5fda42719bb7f5b81a129d414c4aa6a37d5bd4be7bb91f6a52382ba4cb8e9bf6 8.39MB / 13.41MB 3.1s 2025-11-22T11:51:44.3300721Z #5 sha256:d2ea87f44aba6488185e17100b2f5dfe99d2d35916fcd868903fddfc94ab62fc 369B / 369B 2.9s done 2025-11-22T11:51:44.3301767Z #5 sha256:8586f3c6f26210e2cf95bcf39a922611a52d632dac168b45e0edcd98bd9cbcb2 0B / 321.19kB 3.1s 2025-11-22T11:51:44.5294940Z #5 sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 72.63MB / 72.63MB 3.2s done 2025-11-22T11:51:44.5296135Z #5 sha256:5fda42719bb7f5b81a129d414c4aa6a37d5bd4be7bb91f6a52382ba4cb8e9bf6 13.41MB / 13.41MB 3.2s done 2025-11-22T11:51:44.5297118Z #5 sha256:8586f3c6f26210e2cf95bcf39a922611a52d632dac168b45e0edcd98bd9cbcb2 321.19kB / 321.19kB 3.2s done 2025-11-22T11:51:44.5298176Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 0B / 76.27MB 3.3s 2025-11-22T11:51:44.5298940Z #5 sha256:ee321efb6d11d91f235280b8fdb18bcae6d1bb0082076d2f1565b7eee491e666 0B / 25.38kB 3.3s 2025-11-22T11:51:44.5299788Z #5 sha256:446da3748792339e76ae2c851dde700723c49092a726ee1043613beb88aa8e5e 0B / 197B 3.3s 2025-11-22T11:51:44.6296072Z #5 sha256:ee321efb6d11d91f235280b8fdb18bcae6d1bb0082076d2f1565b7eee491e666 25.38kB / 25.38kB 3.4s done 2025-11-22T11:51:44.6296991Z #5 sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 0B / 102.62MB 3.4s 2025-11-22T11:51:44.7518245Z #5 sha256:446da3748792339e76ae2c851dde700723c49092a726ee1043613beb88aa8e5e 197B / 197B 3.4s done 2025-11-22T11:51:44.7519096Z #5 sha256:949ad9d5cd553a38154c4701544a1f2aab656674162f1793a9d3c4ebd3ac9e85 0B / 3.57MB 3.5s 2025-11-22T11:51:44.8537099Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 8.39MB / 76.27MB 3.6s 2025-11-22T11:51:44.9612091Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 23.07MB / 76.27MB 3.7s 2025-11-22T11:51:44.9613273Z #5 sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 18.80MB / 102.62MB 3.7s 2025-11-22T11:51:45.1049796Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 41.94MB / 76.27MB 3.8s 2025-11-22T11:51:45.1051254Z #5 sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 47.64MB / 102.62MB 3.8s 2025-11-22T11:51:45.1052722Z #5 sha256:949ad9d5cd553a38154c4701544a1f2aab656674162f1793a9d3c4ebd3ac9e85 3.57MB / 3.57MB 3.7s done 2025-11-22T11:51:45.1053737Z #5 sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 0B / 56.03MB 3.8s 2025-11-22T11:51:45.2297113Z #5 extracting sha256:7d8f09f272dbd3d238ada5823dec59d0b8fc9464980134c4f491958d24c3506b 2.5s done 2025-11-22T11:51:45.2298738Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 67.11MB / 76.27MB 4.0s 2025-11-22T11:51:45.2299864Z #5 sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 102.62MB / 102.62MB 4.0s 2025-11-22T11:51:45.3300084Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 76.27MB / 76.27MB 4.1s 2025-11-22T11:51:45.3301199Z #5 sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 102.62MB / 102.62MB 4.1s done 2025-11-22T11:51:45.3302334Z #5 sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 8.39MB / 56.03MB 4.1s 2025-11-22T11:51:45.5302706Z #5 sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 76.27MB / 76.27MB 4.3s done 2025-11-22T11:51:45.5303859Z #5 sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 35.65MB / 56.03MB 4.3s 2025-11-22T11:51:45.7300524Z #5 sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 56.03MB / 56.03MB 4.5s 2025-11-22T11:51:46.1801146Z #5 sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 56.03MB / 56.03MB 4.8s done 2025-11-22T11:51:46.2231514Z #5 extracting sha256:57e06e329388159c4aef03a043b49a3355e8f3eb1639aa35eff74e9467b82d6a 2025-11-22T11:51:46.3685217Z #5 extracting sha256:57e06e329388159c4aef03a043b49a3355e8f3eb1639aa35eff74e9467b82d6a done 2025-11-22T11:51:46.3686300Z #5 extracting sha256:3ecf89a071184cf99fd636d5d6d087cc9e7bfca1590391d89ecdf11c4e705202 0.0s done 2025-11-22T11:51:46.3687623Z #5 extracting sha256:98616c90e2c9cba243b8003431853cbd1210679c1996d3aac4d519f571a4f162 0.1s done 2025-11-22T11:51:46.4726791Z #5 extracting sha256:38c8ce617e522855c2a700ff19527183d4e8d0669f346a05db133627f288995e done 2025-11-22T11:51:46.4728320Z #5 extracting sha256:1fba94949cc24d0b68d21a858ddca73c0e029272d9d1f0e525b2608fda663dc0 0.1s done 2025-11-22T11:51:46.6759221Z #5 extracting sha256:21a3de1cbbd552f73677e9ecbc57b6e1e5210758dbcf31343e2030b4c6fd571a 0.0s done 2025-11-22T11:51:46.6760308Z #5 extracting sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 0.1s 2025-11-22T11:51:48.3660842Z #5 extracting sha256:007a87f88e298306ff8941cd4bffc3132192163e7d0ee0ba86eb818eeed44c5e 1.7s done 2025-11-22T11:51:48.3661836Z #5 extracting sha256:adcd2f319a3963b2653ebc5b3968339fa9302e2d9c816d155a9b121b949d3808 2025-11-22T11:51:48.5631756Z #5 extracting sha256:adcd2f319a3963b2653ebc5b3968339fa9302e2d9c816d155a9b121b949d3808 done 2025-11-22T11:51:48.5633191Z #5 extracting sha256:bf0f7f962c9ba2c22c273b854bdccd793599ca64dcd61952665baee7130653f2 done 2025-11-22T11:51:48.5634196Z #5 extracting sha256:3f1021e01b645a5ab966de745ea025285a16fae1aa8a9caa37cc72fb8aa28ee2 done 2025-11-22T11:51:48.5635256Z #5 extracting sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 0.1s 2025-11-22T11:51:49.7113961Z #5 extracting sha256:f5fa40f73ccbe8c17f7d29285123024be8e58e5f4e3b7255a60fa90d3cdfe7c7 1.2s done 2025-11-22T11:51:49.7115314Z #5 extracting sha256:b34531bbb6407c37a2cb406789910078de9c1577ff3aefc56e35f0a2754f9bfb 2025-11-22T11:51:49.8155726Z #5 extracting sha256:b34531bbb6407c37a2cb406789910078de9c1577ff3aefc56e35f0a2754f9bfb 0.1s done 2025-11-22T11:51:49.9662517Z #5 extracting sha256:a77212fc188d8b3d356b21b910a8c2a3af4a2fe454b4cc0064f2c40f82a42758 done 2025-11-22T11:51:49.9663506Z #5 extracting sha256:5566081492aa65e7813031426c391f7c6ae30f9c1075d3b519c3352d1933b51f done 2025-11-22T11:51:49.9664452Z #5 extracting sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 0.1s 2025-11-22T11:51:51.4416584Z #5 extracting sha256:d91da64f524581ff5e5b7e68a1e41c8fecd4e1925dc29c4f50cdc5e519ffbaf8 1.4s done 2025-11-22T11:51:51.5148389Z #5 extracting sha256:3c578b5f0ee26865e66d9064bf236fecc9d5b17454b59b598f9ae77594dfaa13 2025-11-22T11:51:51.6855009Z #5 extracting sha256:3c578b5f0ee26865e66d9064bf236fecc9d5b17454b59b598f9ae77594dfaa13 done 2025-11-22T11:51:51.6856689Z #5 extracting sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 0.1s 2025-11-22T11:51:53.3660570Z #5 extracting sha256:c4be6c6806fe445a78feb51cde3038b2185c31dd6ebe996e3bca0d01ce6cb933 1.6s done 2025-11-22T11:51:53.9124559Z #5 extracting sha256:0e006b8e9370e442d0839533b84c61083bb9fad8e971821bfad1c763b5f054a2 2025-11-22T11:51:54.0782225Z #5 extracting sha256:0e006b8e9370e442d0839533b84c61083bb9fad8e971821bfad1c763b5f054a2 done 2025-11-22T11:51:54.0783396Z #5 extracting sha256:a5acae780e6ee608bfe4be73937b4d29961f6734292a8920811a5124a9edaaaf done 2025-11-22T11:51:54.0784425Z #5 extracting sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 0.1s 2025-11-22T11:51:57.2327744Z #5 extracting sha256:a255ea5bc8450c662edc9601def1323a366d80561cc6565242e2577acbd97238 3.1s done 2025-11-22T11:51:57.5088202Z #5 extracting sha256:95711837916f6af5b3d0367df43b9543a012c7fbaf9c0f68a8483cc79cea81ef 2025-11-22T11:51:57.6165943Z #5 extracting sha256:95711837916f6af5b3d0367df43b9543a012c7fbaf9c0f68a8483cc79cea81ef 0.1s done 2025-11-22T11:51:57.6166992Z #5 extracting sha256:d2ea87f44aba6488185e17100b2f5dfe99d2d35916fcd868903fddfc94ab62fc done 2025-11-22T11:51:57.6169454Z #5 extracting sha256:5fda42719bb7f5b81a129d414c4aa6a37d5bd4be7bb91f6a52382ba4cb8e9bf6 2025-11-22T11:51:59.1854814Z #5 extracting sha256:5fda42719bb7f5b81a129d414c4aa6a37d5bd4be7bb91f6a52382ba4cb8e9bf6 1.5s done 2025-11-22T11:51:59.1855895Z #5 extracting sha256:8586f3c6f26210e2cf95bcf39a922611a52d632dac168b45e0edcd98bd9cbcb2 2025-11-22T11:51:59.2999876Z #5 extracting sha256:8586f3c6f26210e2cf95bcf39a922611a52d632dac168b45e0edcd98bd9cbcb2 0.0s done 2025-11-22T11:51:59.3001025Z #5 extracting sha256:ee321efb6d11d91f235280b8fdb18bcae6d1bb0082076d2f1565b7eee491e666 done 2025-11-22T11:51:59.3002001Z #5 extracting sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 2025-11-22T11:52:00.5380566Z #5 extracting sha256:2a06cd1af3e0b4befa481b22e2af52eea6a55afc9108689e27ea536205b5575d 1.2s done 2025-11-22T11:52:00.8627776Z #5 extracting sha256:446da3748792339e76ae2c851dde700723c49092a726ee1043613beb88aa8e5e 2025-11-22T11:52:01.0139887Z #5 extracting sha256:446da3748792339e76ae2c851dde700723c49092a726ee1043613beb88aa8e5e done 2025-11-22T11:52:01.0140900Z #5 extracting sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 0.1s 2025-11-22T11:52:04.5927565Z #5 extracting sha256:9dbf87995c555e567a672b2bb695cc5b88d3ac646d55c768a51494c776ecec04 3.5s done 2025-11-22T11:52:05.0059060Z #5 extracting sha256:949ad9d5cd553a38154c4701544a1f2aab656674162f1793a9d3c4ebd3ac9e85 2025-11-22T11:52:06.6459844Z #5 extracting sha256:949ad9d5cd553a38154c4701544a1f2aab656674162f1793a9d3c4ebd3ac9e85 1.6s done 2025-11-22T11:52:06.9239144Z #5 extracting sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 2025-11-22T11:52:09.7696321Z #5 extracting sha256:8ec03f1e3be02700e46964b207550f3a16e86ac7db491efbc361fa23c3648316 2.7s done 2025-11-22T11:52:10.2827499Z #5 DONE 29.1s 2025-11-22T11:52:10.4044559Z 2025-11-22T11:52:10.4045304Z #6 [2/4] WORKDIR /opt/oss-fuzz/infra 2025-11-22T11:52:10.4045739Z #6 DONE 0.0s 2025-11-22T11:52:10.4045906Z 2025-11-22T11:52:10.4046031Z #7 [3/4] ADD . /opt/oss-fuzz/infra 2025-11-22T11:52:10.4046386Z #7 DONE 0.1s 2025-11-22T11:52:10.5559202Z 2025-11-22T11:52:10.5559890Z #8 [4/4] RUN python3 -m pip install -r /opt/oss-fuzz/infra/cifuzz/requirements.txt 2025-11-22T11:52:10.7955610Z #8 0.390 Requirement already satisfied: clusterfuzz==2.5.9 in /usr/local/lib/python3.11/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.5.9) 2025-11-22T11:52:10.8987953Z #8 0.391 Requirement already satisfied: requests==2.28.0 in /usr/local/lib/python3.11/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2.28.0) 2025-11-22T11:52:10.8989925Z #8 0.391 Requirement already satisfied: protobuf==3.20.2 in /usr/local/lib/python3.11/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 3)) (3.20.2) 2025-11-22T11:52:10.8991782Z #8 0.392 Requirement already satisfied: gsutil==5.20 in /usr/local/lib/python3.11/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (5.20) 2025-11-22T11:52:10.8993963Z #8 0.393 Requirement already satisfied: google-api-python-client in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.187.0) 2025-11-22T11:52:10.8996390Z #8 0.393 Requirement already satisfied: google-auth>=1.22.1 in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.39.0) 2025-11-22T11:52:10.8998800Z #8 0.394 Requirement already satisfied: google-auth-oauthlib in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.2.3) 2025-11-22T11:52:10.9001384Z #8 0.394 Requirement already satisfied: google-cloud-core in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.5.0) 2025-11-22T11:52:10.9003508Z #8 0.395 Requirement already satisfied: google-cloud-datastore==1.12.0 in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.12.0) 2025-11-22T11:52:10.9005682Z #8 0.395 Requirement already satisfied: google-cloud-logging in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.1.2) 2025-11-22T11:52:10.9008037Z #8 0.396 Requirement already satisfied: google-cloud-monitoring in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.28.0) 2025-11-22T11:52:10.9010245Z #8 0.396 Requirement already satisfied: google-cloud-ndb<2.0.0 in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.12.0) 2025-11-22T11:52:10.9012423Z #8 0.397 Requirement already satisfied: google-cloud-storage in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.23.0) 2025-11-22T11:52:10.9014489Z #8 0.397 Requirement already satisfied: grpcio in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.76.0) 2025-11-22T11:52:10.9016494Z #8 0.398 Requirement already satisfied: httplib2 in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.20.4) 2025-11-22T11:52:10.9019009Z #8 0.398 Requirement already satisfied: mozprocess in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.4.0) 2025-11-22T11:52:10.9021123Z #8 0.399 Requirement already satisfied: oauth2client in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.1.3) 2025-11-22T11:52:10.9023191Z #8 0.399 Requirement already satisfied: psutil in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (7.1.3) 2025-11-22T11:52:10.9025163Z #8 0.399 Requirement already satisfied: pytz in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2025.2) 2025-11-22T11:52:10.9027119Z #8 0.400 Requirement already satisfied: PyYAML in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (6.0.3) 2025-11-22T11:52:10.9029286Z #8 0.400 Requirement already satisfied: six in /usr/local/lib/python3.11/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.15.0) 2025-11-22T11:52:10.9031355Z #8 0.402 Requirement already satisfied: charset-normalizer~=2.0.0 in /usr/local/lib/python3.11/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2.0.12) 2025-11-22T11:52:10.9033534Z #8 0.402 Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (3.11) 2025-11-22T11:52:10.9035533Z #8 0.403 Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.11/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (1.26.20) 2025-11-22T11:52:10.9037781Z #8 0.404 Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2025.11.12) 2025-11-22T11:52:10.9039833Z #8 0.406 Requirement already satisfied: argcomplete>=1.9.4 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.6.3) 2025-11-22T11:52:10.9042052Z #8 0.407 Requirement already satisfied: crcmod>=1.7 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.7) 2025-11-22T11:52:10.9043986Z #8 0.407 Requirement already satisfied: fasteners>=0.14.1 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.20) 2025-11-22T11:52:10.9045996Z #8 0.408 Requirement already satisfied: gcs-oauth2-boto-plugin>=3.0 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.3) 2025-11-22T11:52:10.9048245Z #8 0.410 Requirement already satisfied: google-apitools>=0.5.32 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.5.35) 2025-11-22T11:52:10.9050321Z #8 0.410 Requirement already satisfied: google-reauth>=0.1.0 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.1.1) 2025-11-22T11:52:10.9052330Z #8 0.410 Requirement already satisfied: monotonic>=1.4 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.6) 2025-11-22T11:52:10.9054303Z #8 0.411 Requirement already satisfied: pyOpenSSL>=0.13 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (25.3.0) 2025-11-22T11:52:10.9056355Z #8 0.411 Requirement already satisfied: retry_decorator>=1.0.0 in /usr/local/lib/python3.11/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.1.1) 2025-11-22T11:52:10.9059362Z #8 0.414 Requirement already satisfied: google-api-core<2.0.0dev,>=1.14.0 in /usr/local/lib/python3.11/site-packages (from google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.34.1) 2025-11-22T11:52:10.9062080Z #8 0.416 Requirement already satisfied: pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2 in /usr/local/lib/python3.11/site-packages (from httplib2->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.2.5) 2025-11-22T11:52:10.9064906Z #8 0.419 Requirement already satisfied: googleapis-common-protos<2.0dev,>=1.56.2 in /usr/local/lib/python3.11/site-packages (from google-api-core<2.0.0dev,>=1.14.0->google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.72.0) 2025-11-22T11:52:10.9068184Z #8 0.424 Requirement already satisfied: grpcio-status<2.0dev,>=1.33.2 in /usr/local/lib/python3.11/site-packages (from google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.49.0rc1) 2025-11-22T11:52:10.9070741Z #8 0.425 Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.11/site-packages (from google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (5.5.2) 2025-11-22T11:52:10.9072988Z #8 0.426 Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.11/site-packages (from google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.4.2) 2025-11-22T11:52:10.9075159Z #8 0.427 Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.11/site-packages (from google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.7.2) 2025-11-22T11:52:10.9077527Z #8 0.434 Requirement already satisfied: pymemcache<5.0.0dev,>=2.1.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-ndb<2.0.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.0.0) 2025-11-22T11:52:10.9079881Z #8 0.435 Requirement already satisfied: redis<5.0.0dev,>=3.0.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-ndb<2.0.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.6.0) 2025-11-22T11:52:10.9082412Z #8 0.439 Requirement already satisfied: typing-extensions~=4.12 in /usr/local/lib/python3.11/site-packages (from grpcio->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.15.0) 2025-11-22T11:52:10.9084733Z #8 0.448 Requirement already satisfied: pyasn1>=0.1.3 in /usr/local/lib/python3.11/site-packages (from rsa<5,>=3.1.4->google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.6.1) 2025-11-22T11:52:10.9121893Z #8 0.458 Requirement already satisfied: boto>=2.29.1 in /usr/local/lib/python3.11/site-packages (from gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.49.0) 2025-11-22T11:52:10.9124294Z #8 0.459 Requirement already satisfied: google-auth-httplib2>=0.2.0 in /usr/local/lib/python3.11/site-packages (from gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.2.1) 2025-11-22T11:52:10.9126658Z #8 0.466 Requirement already satisfied: aiohttp<4.0.0,>=3.6.2 in /usr/local/lib/python3.11/site-packages (from google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.13.2) 2025-11-22T11:52:10.9129206Z #8 0.468 Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.6.1) 2025-11-22T11:52:10.9131931Z #8 0.469 Requirement already satisfied: aiosignal>=1.4.0 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.4.0) 2025-11-22T11:52:10.9134464Z #8 0.470 Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (25.4.0) 2025-11-22T11:52:10.9136849Z #8 0.470 Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.8.0) 2025-11-22T11:52:10.9139422Z #8 0.471 Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (6.7.0) 2025-11-22T11:52:10.9141790Z #8 0.471 Requirement already satisfied: propcache>=0.2.0 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.4.1) 2025-11-22T11:52:10.9144173Z #8 0.472 Requirement already satisfied: yarl<2.0,>=1.17.0 in /usr/local/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.22.0) 2025-11-22T11:52:10.9146403Z #8 0.483 Requirement already satisfied: pyu2f in /usr/local/lib/python3.11/site-packages (from google-reauth>=0.1.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.1.5) 2025-11-22T11:52:10.9148656Z #8 0.493 Requirement already satisfied: cryptography<47,>=45.0.7 in /usr/local/lib/python3.11/site-packages (from pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (46.0.3) 2025-11-22T11:52:11.0330407Z #8 0.498 Requirement already satisfied: cffi>=2.0.0 in /usr/local/lib/python3.11/site-packages (from cryptography<47,>=45.0.7->pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.0.0) 2025-11-22T11:52:11.0332247Z #8 0.504 Requirement already satisfied: pycparser in /usr/local/lib/python3.11/site-packages (from cffi>=2.0.0->cryptography<47,>=45.0.7->pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.23) 2025-11-22T11:52:11.0334061Z #8 0.513 Requirement already satisfied: uritemplate<5,>=3.0.1 in /usr/local/lib/python3.11/site-packages (from google-api-python-client->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.2.0) 2025-11-22T11:52:11.0335556Z #8 0.517 Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.11/site-packages (from google-auth-oauthlib->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.0.0) 2025-11-22T11:52:11.0337102Z #8 0.520 Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.11/site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.3.1) 2025-11-22T11:52:11.0338990Z #8 0.526 Requirement already satisfied: google-cloud-appengine-logging<2.0.0dev,>=0.1.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.7.0) 2025-11-22T11:52:11.0340604Z #8 0.527 Requirement already satisfied: google-cloud-audit-log<1.0.0dev,>=0.1.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.4.0) 2025-11-22T11:52:11.0342228Z #8 0.528 Requirement already satisfied: grpc-google-iam-v1<1.0.0dev,>=0.12.4 in /usr/local/lib/python3.11/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.14.3) 2025-11-22T11:52:11.0343738Z #8 0.528 Requirement already satisfied: proto-plus<2.0.0dev,>=1.15.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.26.1) 2025-11-22T11:52:11.0345056Z #8 0.570 Requirement already satisfied: google-resumable-media<0.6dev,>=0.5.0 in /usr/local/lib/python3.11/site-packages (from google-cloud-storage->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.5.1) 2025-11-22T11:52:11.0346297Z #8 0.575 Requirement already satisfied: mozinfo in /usr/local/lib/python3.11/site-packages (from mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.2.3) 2025-11-22T11:52:11.0347689Z #8 0.577 Requirement already satisfied: distro>=1.4.0 in /usr/local/lib/python3.11/site-packages (from mozinfo->mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.9.0) 2025-11-22T11:52:11.0348873Z #8 0.577 Requirement already satisfied: mozfile>=0.12 in /usr/local/lib/python3.11/site-packages (from mozinfo->mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.0.0) 2025-11-22T11:52:11.0350571Z #8 0.628 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning. 2025-11-22T11:52:11.2428389Z #8 DONE 0.7s 2025-11-22T11:52:11.2429138Z 2025-11-22T11:52:11.2431740Z #9 exporting to image 2025-11-22T11:52:11.2432129Z #9 exporting layers 2025-11-22T11:52:14.6399559Z #9 exporting layers 3.5s done 2025-11-22T11:52:14.6597794Z #9 writing image sha256:fbe000347c0a6266262f93bddebc13c35f29ce718ee30f79f45f400c3b4deca6 done 2025-11-22T11:52:14.6598724Z #9 naming to docker.io/library/207f17:c56653790b0c4db091b6057e2ac6f688 done 2025-11-22T11:52:14.6599283Z #9 DONE 3.6s 2025-11-22T11:52:14.6661168Z ##[endgroup] 2025-11-22T11:52:14.6690270Z ##[group]Build container for action use: '/home/runner/work/_actions/google/oss-fuzz/master/infra/cifuzz/actions/run_fuzzers/../../../run_fuzzers.Dockerfile'. 2025-11-22T11:52:14.6692167Z ##[command]/usr/bin/docker build -t 207f17:6a803eb2beb646bb83690ddfe4785996 -f "/home/runner/work/_actions/google/oss-fuzz/master/infra/cifuzz/actions/run_fuzzers/../../../run_fuzzers.Dockerfile" "/home/runner/work/_actions/google/oss-fuzz/master/infra" 2025-11-22T11:52:14.7886282Z #0 building with "default" instance using docker driver 2025-11-22T11:52:14.7886705Z 2025-11-22T11:52:14.7887097Z #1 [internal] load build definition from run_fuzzers.Dockerfile 2025-11-22T11:52:14.9451307Z #1 transferring dockerfile: 1.27kB done 2025-11-22T11:52:14.9451738Z #1 DONE 0.0s 2025-11-22T11:52:14.9451905Z 2025-11-22T11:52:15.4440595Z #2 [internal] load metadata for gcr.io/oss-fuzz-base/cifuzz-base:metzman-test 2025-11-22T11:52:15.4441224Z #2 DONE 0.6s 2025-11-22T11:52:15.5606773Z 2025-11-22T11:52:15.5607273Z #3 [internal] load .dockerignore 2025-11-22T11:52:15.5607934Z #3 transferring context: 125B done 2025-11-22T11:52:15.5608315Z #3 DONE 0.0s 2025-11-22T11:52:15.5608471Z 2025-11-22T11:52:15.5608571Z #4 [internal] load build context 2025-11-22T11:52:15.5608843Z #4 transferring context: 28.12kB 0.0s done 2025-11-22T11:52:15.5609106Z #4 DONE 0.0s 2025-11-22T11:52:15.5609208Z 2025-11-22T11:52:15.5609617Z #5 [1/4] FROM gcr.io/oss-fuzz-base/cifuzz-base:metzman-test@sha256:289cbb5f251608f37e52d5a7fb0af23021a9c3667a670e26372682d9f8b1aa3c 2025-11-22T11:52:15.5610544Z #5 resolve gcr.io/oss-fuzz-base/cifuzz-base:metzman-test@sha256:289cbb5f251608f37e52d5a7fb0af23021a9c3667a670e26372682d9f8b1aa3c done 2025-11-22T11:52:15.5611338Z #5 sha256:6075d866060a9fd559e0a9fc08e95277516eb8341ebb7f36ce9698ca1661540f 0B / 174B 0.1s 2025-11-22T11:52:15.5611986Z #5 sha256:7007b98020afac40c05fdb1b9ea0706d4621002d26da538b13b724dc0b9c0497 17.61kB / 17.61kB done 2025-11-22T11:52:15.5612637Z #5 sha256:5235348439713679faa4c801ccf7f724c62590d184683e62f34e2eac3c4cbe5f 0B / 2.33MB 0.1s 2025-11-22T11:52:15.5613258Z #5 sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 0B / 82.09MB 0.1s 2025-11-22T11:52:15.7600998Z #5 sha256:6075d866060a9fd559e0a9fc08e95277516eb8341ebb7f36ce9698ca1661540f 174B / 174B 0.2s done 2025-11-22T11:52:15.7602055Z #5 sha256:5235348439713679faa4c801ccf7f724c62590d184683e62f34e2eac3c4cbe5f 2.33MB / 2.33MB 0.2s done 2025-11-22T11:52:15.7605327Z #5 sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 12.18MB / 82.09MB 0.3s 2025-11-22T11:52:15.7606395Z #5 sha256:289cbb5f251608f37e52d5a7fb0af23021a9c3667a670e26372682d9f8b1aa3c 6.83kB / 6.83kB done 2025-11-22T11:52:15.7606978Z #5 sha256:b1cc8b776b4eea1d4dd9df12c8c7033a470912d57abf4141505db9117cd63c2d 0B / 7.04MB 0.3s 2025-11-22T11:52:15.7607677Z #5 sha256:e9a61ad392c2038dd201b45e90cfb6c78d03c31be0fa41eda451172858d8ccca 0B / 3.31kB 0.3s 2025-11-22T11:52:15.8608092Z #5 sha256:b1cc8b776b4eea1d4dd9df12c8c7033a470912d57abf4141505db9117cd63c2d 7.04MB / 7.04MB 0.4s done 2025-11-22T11:52:15.8609234Z #5 sha256:f9c1fbf5f0bcc09a098aa672831713b9a7d5a5cf0c22b51825b22e2b8325687d 0B / 5.15MB 0.4s 2025-11-22T11:52:16.0474077Z #5 sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 67.11MB / 82.09MB 0.5s 2025-11-22T11:52:16.0475421Z #5 sha256:e9a61ad392c2038dd201b45e90cfb6c78d03c31be0fa41eda451172858d8ccca 3.31kB / 3.31kB 0.4s done 2025-11-22T11:52:16.0476552Z #5 sha256:ddbfd59897331a649ad1898ff9a0f303d914c4eb8936c6a70962ddea006e1427 0B / 187.97kB 0.5s 2025-11-22T11:52:16.0478071Z #5 extracting sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 2025-11-22T11:52:16.1600061Z #5 sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 82.09MB / 82.09MB 0.6s done 2025-11-22T11:52:16.1601279Z #5 sha256:f9c1fbf5f0bcc09a098aa672831713b9a7d5a5cf0c22b51825b22e2b8325687d 5.15MB / 5.15MB 0.7s done 2025-11-22T11:52:16.1603148Z #5 sha256:ddbfd59897331a649ad1898ff9a0f303d914c4eb8936c6a70962ddea006e1427 187.97kB / 187.97kB 0.6s done 2025-11-22T11:52:16.1604278Z #5 sha256:e007cf47cd0e6015fc81a477530b53d1c4d04599ca328bd5057baf0401175000 0B / 31.90MB 0.6s 2025-11-22T11:52:16.1605321Z #5 sha256:8054a9e298e365ab110c2039f08534ecc1e53d0f329c099b2c0d4d8dd9b2785a 0B / 332B 0.7s 2025-11-22T11:52:16.1606600Z #5 sha256:3e0890435dafb834ea8a3d69cada5e13f7a0b2b1e25e852b377540e21b80803e 0B / 3.35kB 0.7s 2025-11-22T11:52:16.3125921Z #5 sha256:e007cf47cd0e6015fc81a477530b53d1c4d04599ca328bd5057baf0401175000 20.97MB / 31.90MB 0.8s 2025-11-22T11:52:16.4598573Z #5 sha256:e007cf47cd0e6015fc81a477530b53d1c4d04599ca328bd5057baf0401175000 31.90MB / 31.90MB 0.9s done 2025-11-22T11:52:16.4599768Z #5 sha256:8054a9e298e365ab110c2039f08534ecc1e53d0f329c099b2c0d4d8dd9b2785a 332B / 332B 0.8s done 2025-11-22T11:52:16.4600577Z #5 sha256:3e0890435dafb834ea8a3d69cada5e13f7a0b2b1e25e852b377540e21b80803e 3.35kB / 3.35kB 0.8s done 2025-11-22T11:52:16.4601414Z #5 sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 0B / 46.65MB 0.9s 2025-11-22T11:52:16.4602225Z #5 sha256:d9f6e9b55a732e118385b347cab7dc59f6de868f39d7e6824adcc5df803f27dd 0B / 2.77MB 1.0s 2025-11-22T11:52:16.4602877Z #5 sha256:477402658b0ba7badf264d0f90aa03cfa129aeb7c8ecf174344f03e6a5c6d581 701B / 701B 1.0s done 2025-11-22T11:52:16.4603528Z #5 sha256:f0fc9faee82bb0165557f141c61716604133fb8f6d0c7d1b668c07d0e81e3f20 0B / 11.04kB 1.0s 2025-11-22T11:52:16.6285502Z #5 sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 18.87MB / 46.65MB 1.1s 2025-11-22T11:52:16.6288942Z #5 sha256:d9f6e9b55a732e118385b347cab7dc59f6de868f39d7e6824adcc5df803f27dd 2.77MB / 2.77MB 1.1s done 2025-11-22T11:52:16.6290003Z #5 sha256:f63da361bb5d53a2696ff94aff4e06d9df120c55566f1618a1d8ddad74d3bad6 0B / 864B 1.1s 2025-11-22T11:52:16.7330386Z #5 sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 44.99MB / 46.65MB 1.2s 2025-11-22T11:52:16.7331510Z #5 sha256:f0fc9faee82bb0165557f141c61716604133fb8f6d0c7d1b668c07d0e81e3f20 11.04kB / 11.04kB 1.2s done 2025-11-22T11:52:16.7332584Z #5 sha256:c6e3f6e0d4ddda12b4407a7c36a063e5f6327ef7bb5af5b2fab5033ab6da8d0d 0B / 19.02MB 1.2s 2025-11-22T11:52:16.8442012Z #5 sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 46.65MB / 46.65MB 1.2s done 2025-11-22T11:52:16.8443195Z #5 sha256:f63da361bb5d53a2696ff94aff4e06d9df120c55566f1618a1d8ddad74d3bad6 864B / 864B 1.2s done 2025-11-22T11:52:16.8444222Z #5 sha256:bb3ae3d4a376b0e79f69af0718e1b8d437ae23ad2316cc0aa55ee8f021dcf8a3 0B / 940B 1.3s 2025-11-22T11:52:16.8446296Z #5 sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 0B / 137.28MB 1.3s 2025-11-22T11:52:16.9529888Z #5 sha256:c6e3f6e0d4ddda12b4407a7c36a063e5f6327ef7bb5af5b2fab5033ab6da8d0d 15.73MB / 19.02MB 1.4s 2025-11-22T11:52:17.0594187Z #5 sha256:c6e3f6e0d4ddda12b4407a7c36a063e5f6327ef7bb5af5b2fab5033ab6da8d0d 19.02MB / 19.02MB 1.4s done 2025-11-22T11:52:17.0595334Z #5 sha256:bb3ae3d4a376b0e79f69af0718e1b8d437ae23ad2316cc0aa55ee8f021dcf8a3 940B / 940B 1.4s done 2025-11-22T11:52:17.0596995Z #5 sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 36.54MB / 137.28MB 1.5s 2025-11-22T11:52:17.0598310Z #5 sha256:fb2d2d681e4973bf545d043b2aad748582104e364b3c2062d5c5fedeb476369f 0B / 785B 1.5s 2025-11-22T11:52:17.0599182Z #5 sha256:bef6828467940f4d5d924c93488b6cbeb5e6416525c36193bc84b0915cfddf94 0B / 759.28kB 1.5s 2025-11-22T11:52:17.1600707Z #5 sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 113.25MB / 137.28MB 1.7s 2025-11-22T11:52:17.1601842Z #5 sha256:fb2d2d681e4973bf545d043b2aad748582104e364b3c2062d5c5fedeb476369f 785B / 785B 1.6s done 2025-11-22T11:52:17.1603820Z #5 sha256:bef6828467940f4d5d924c93488b6cbeb5e6416525c36193bc84b0915cfddf94 759.28kB / 759.28kB 1.6s done 2025-11-22T11:52:17.1604915Z #5 sha256:327d6b8a4ce5ad5e4c01b1ff207cfda37b18ba3fa944ed978f5e28d0ed46dce9 0B / 72.37MB 1.7s 2025-11-22T11:52:17.1605932Z #5 sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 0B / 80.57MB 1.7s 2025-11-22T11:52:17.2606598Z #5 sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 137.28MB / 137.28MB 1.8s 2025-11-22T11:52:17.4599957Z #5 sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 137.28MB / 137.28MB 1.8s done 2025-11-22T11:52:17.4601937Z #5 sha256:327d6b8a4ce5ad5e4c01b1ff207cfda37b18ba3fa944ed978f5e28d0ed46dce9 57.67MB / 72.37MB 2.0s 2025-11-22T11:52:17.4603411Z #5 sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 52.98MB / 80.57MB 2.0s 2025-11-22T11:52:17.4604737Z #5 sha256:7f93d64bbd0e838ff84ba782eb9e9d22ab1e727dcb28f4e768409bbc6a294914 3.15MB / 11.37MB 2.0s 2025-11-22T11:52:17.6602591Z #5 sha256:327d6b8a4ce5ad5e4c01b1ff207cfda37b18ba3fa944ed978f5e28d0ed46dce9 72.37MB / 72.37MB 2.1s done 2025-11-22T11:52:17.6603995Z #5 sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 80.57MB / 80.57MB 2.2s 2025-11-22T11:52:17.6605284Z #5 sha256:7f93d64bbd0e838ff84ba782eb9e9d22ab1e727dcb28f4e768409bbc6a294914 11.37MB / 11.37MB 2.0s done 2025-11-22T11:52:17.6606526Z #5 sha256:06de2273f4ba4048ad71d278117d9c3b54eea3bc161bc61cf9b6b65f23afa5cf 657B / 657B 2.2s 2025-11-22T11:52:17.6607919Z #5 sha256:7f1c557318f1cbdf6de4e4906208ba3e71349377088358dd6a681fe1399668d1 0B / 24.28kB 2.2s 2025-11-22T11:52:17.8063908Z #5 sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 80.57MB / 80.57MB 2.2s done 2025-11-22T11:52:17.8064988Z #5 sha256:06de2273f4ba4048ad71d278117d9c3b54eea3bc161bc61cf9b6b65f23afa5cf 657B / 657B 2.2s done 2025-11-22T11:52:17.8066235Z #5 sha256:7f1c557318f1cbdf6de4e4906208ba3e71349377088358dd6a681fe1399668d1 24.28kB / 24.28kB 2.2s done 2025-11-22T11:52:17.8067239Z #5 sha256:0e55ef266d087e0a70a54633283435acac0ddab481d1f83b915876ed1751deda 0B / 198B 2.3s 2025-11-22T11:52:17.8068307Z #5 sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 0B / 89.16MB 2.3s 2025-11-22T11:52:17.8069018Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 0B / 92.69MB 2.3s 2025-11-22T11:52:17.9602557Z #5 sha256:0e55ef266d087e0a70a54633283435acac0ddab481d1f83b915876ed1751deda 198B / 198B 2.4s done 2025-11-22T11:52:17.9603680Z #5 sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 44.04MB / 89.16MB 2.5s 2025-11-22T11:52:17.9604751Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 25.17MB / 92.69MB 2.5s 2025-11-22T11:52:17.9605769Z #5 sha256:56d49c27f82eba9eee88477b7bc956130fcf9691fff6bf002e09a0100f63fe51 0B / 3.25MB 2.5s 2025-11-22T11:52:18.0610561Z #5 sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 80.74MB / 89.16MB 2.6s 2025-11-22T11:52:18.0612009Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 54.53MB / 92.69MB 2.6s 2025-11-22T11:52:18.0613411Z #5 sha256:56d49c27f82eba9eee88477b7bc956130fcf9691fff6bf002e09a0100f63fe51 1.05MB / 3.25MB 2.6s 2025-11-22T11:52:18.2285013Z #5 sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 89.16MB / 89.16MB 2.6s done 2025-11-22T11:52:18.2286542Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 83.89MB / 92.69MB 2.7s 2025-11-22T11:52:18.2288306Z #5 sha256:56d49c27f82eba9eee88477b7bc956130fcf9691fff6bf002e09a0100f63fe51 3.25MB / 3.25MB 2.6s done 2025-11-22T11:52:18.2289429Z #5 sha256:dfde0212bc57fb3ad4e393f72e0e8ff8913d13f2ce52115c01534875af498533 0B / 37.51MB 2.7s 2025-11-22T11:52:18.3318967Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 92.69MB / 92.69MB 2.8s 2025-11-22T11:52:18.4336666Z #5 sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 92.69MB / 92.69MB 2.8s done 2025-11-22T11:52:18.4337969Z #5 sha256:dfde0212bc57fb3ad4e393f72e0e8ff8913d13f2ce52115c01534875af498533 32.51MB / 37.51MB 2.9s 2025-11-22T11:52:18.5387042Z #5 sha256:dfde0212bc57fb3ad4e393f72e0e8ff8913d13f2ce52115c01534875af498533 37.51MB / 37.51MB 2.9s done 2025-11-22T11:52:18.8371043Z #5 extracting sha256:1088de4dab11f86e6b9307ad4382a90b72ee708615e9711d6b2cccb41f495cfb 2.6s done 2025-11-22T11:52:18.9538941Z #5 extracting sha256:6075d866060a9fd559e0a9fc08e95277516eb8341ebb7f36ce9698ca1661540f 2025-11-22T11:52:19.0978733Z #5 extracting sha256:6075d866060a9fd559e0a9fc08e95277516eb8341ebb7f36ce9698ca1661540f done 2025-11-22T11:52:19.0981734Z #5 extracting sha256:5235348439713679faa4c801ccf7f724c62590d184683e62f34e2eac3c4cbe5f 0.0s done 2025-11-22T11:52:19.0983122Z #5 extracting sha256:b1cc8b776b4eea1d4dd9df12c8c7033a470912d57abf4141505db9117cd63c2d 0.1s done 2025-11-22T11:52:19.1992672Z #5 extracting sha256:e9a61ad392c2038dd201b45e90cfb6c78d03c31be0fa41eda451172858d8ccca done 2025-11-22T11:52:19.1993760Z #5 extracting sha256:f9c1fbf5f0bcc09a098aa672831713b9a7d5a5cf0c22b51825b22e2b8325687d 0.1s done 2025-11-22T11:52:19.1994802Z #5 extracting sha256:ddbfd59897331a649ad1898ff9a0f303d914c4eb8936c6a70962ddea006e1427 0.0s done 2025-11-22T11:52:19.3584371Z #5 extracting sha256:e007cf47cd0e6015fc81a477530b53d1c4d04599ca328bd5057baf0401175000 0.1s 2025-11-22T11:52:20.8739870Z #5 extracting sha256:e007cf47cd0e6015fc81a477530b53d1c4d04599ca328bd5057baf0401175000 1.5s done 2025-11-22T11:52:20.8740961Z #5 extracting sha256:8054a9e298e365ab110c2039f08534ecc1e53d0f329c099b2c0d4d8dd9b2785a 2025-11-22T11:52:21.0626594Z #5 extracting sha256:8054a9e298e365ab110c2039f08534ecc1e53d0f329c099b2c0d4d8dd9b2785a done 2025-11-22T11:52:21.0628212Z #5 extracting sha256:3e0890435dafb834ea8a3d69cada5e13f7a0b2b1e25e852b377540e21b80803e done 2025-11-22T11:52:21.0629941Z #5 extracting sha256:477402658b0ba7badf264d0f90aa03cfa129aeb7c8ecf174344f03e6a5c6d581 done 2025-11-22T11:52:21.0630982Z #5 extracting sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 0.1s 2025-11-22T11:52:22.1229584Z #5 extracting sha256:7412fb83206cedc8bca84bd2c14e5811922eba0cdd9d9680c20312a9118dc8aa 1.2s done 2025-11-22T11:52:22.3005441Z #5 extracting sha256:d9f6e9b55a732e118385b347cab7dc59f6de868f39d7e6824adcc5df803f27dd 0.1s done 2025-11-22T11:52:22.4624637Z #5 extracting sha256:f0fc9faee82bb0165557f141c61716604133fb8f6d0c7d1b668c07d0e81e3f20 done 2025-11-22T11:52:22.4625736Z #5 extracting sha256:f63da361bb5d53a2696ff94aff4e06d9df120c55566f1618a1d8ddad74d3bad6 done 2025-11-22T11:52:22.4626744Z #5 extracting sha256:c6e3f6e0d4ddda12b4407a7c36a063e5f6327ef7bb5af5b2fab5033ab6da8d0d 0.1s 2025-11-22T11:52:23.0626432Z #5 extracting sha256:c6e3f6e0d4ddda12b4407a7c36a063e5f6327ef7bb5af5b2fab5033ab6da8d0d 0.7s done 2025-11-22T11:52:23.2230138Z #5 extracting sha256:bb3ae3d4a376b0e79f69af0718e1b8d437ae23ad2316cc0aa55ee8f021dcf8a3 done 2025-11-22T11:52:23.2231145Z #5 extracting sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 2025-11-22T11:52:24.9742443Z #5 extracting sha256:da1ece868e3e9561c55554becb544cb94fa72b392596fc93e7a3b3468b19f45d 1.6s done 2025-11-22T11:52:25.5295991Z #5 extracting sha256:bef6828467940f4d5d924c93488b6cbeb5e6416525c36193bc84b0915cfddf94 2025-11-22T11:52:25.6962204Z #5 extracting sha256:bef6828467940f4d5d924c93488b6cbeb5e6416525c36193bc84b0915cfddf94 done 2025-11-22T11:52:25.6963285Z #5 extracting sha256:fb2d2d681e4973bf545d043b2aad748582104e364b3c2062d5c5fedeb476369f done 2025-11-22T11:52:25.6964253Z #5 extracting sha256:327d6b8a4ce5ad5e4c01b1ff207cfda37b18ba3fa944ed978f5e28d0ed46dce9 0.1s 2025-11-22T11:52:28.7084083Z #5 extracting sha256:327d6b8a4ce5ad5e4c01b1ff207cfda37b18ba3fa944ed978f5e28d0ed46dce9 3.1s done 2025-11-22T11:52:29.1811149Z #5 extracting sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 2025-11-22T11:52:31.5049401Z #5 extracting sha256:0ba9bb96bbba8671ad1d04ae8d13fe8884ff7c9b5715d160c350d302cacc677b 2.3s done 2025-11-22T11:52:31.7138220Z #5 extracting sha256:7f93d64bbd0e838ff84ba782eb9e9d22ab1e727dcb28f4e768409bbc6a294914 2025-11-22T11:52:36.9109041Z #5 extracting sha256:7f93d64bbd0e838ff84ba782eb9e9d22ab1e727dcb28f4e768409bbc6a294914 5.2s 2025-11-22T11:52:37.9528344Z #5 extracting sha256:7f93d64bbd0e838ff84ba782eb9e9d22ab1e727dcb28f4e768409bbc6a294914 6.1s done 2025-11-22T11:52:38.2713097Z #5 extracting sha256:06de2273f4ba4048ad71d278117d9c3b54eea3bc161bc61cf9b6b65f23afa5cf 2025-11-22T11:52:38.4397724Z #5 extracting sha256:06de2273f4ba4048ad71d278117d9c3b54eea3bc161bc61cf9b6b65f23afa5cf done 2025-11-22T11:52:38.4398824Z #5 extracting sha256:7f1c557318f1cbdf6de4e4906208ba3e71349377088358dd6a681fe1399668d1 done 2025-11-22T11:52:38.4400039Z #5 extracting sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 0.1s 2025-11-22T11:52:39.8595799Z #5 extracting sha256:8a62f121e23e1182e8b1a37ab07e02933401a7314dfdf2d3c404720eb7293937 1.4s done 2025-11-22T11:52:40.0697772Z #5 extracting sha256:0e55ef266d087e0a70a54633283435acac0ddab481d1f83b915876ed1751deda 2025-11-22T11:52:40.2208441Z #5 extracting sha256:0e55ef266d087e0a70a54633283435acac0ddab481d1f83b915876ed1751deda done 2025-11-22T11:52:40.2209871Z #5 extracting sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 0.1s 2025-11-22T11:52:43.6940573Z #5 extracting sha256:6e6199b11263a6f48297defda0e093f4691e4e0213f2b1d2c11da4bdd823444b 3.4s done 2025-11-22T11:52:44.0335286Z #5 extracting sha256:56d49c27f82eba9eee88477b7bc956130fcf9691fff6bf002e09a0100f63fe51 2025-11-22T11:52:45.6078976Z #5 extracting sha256:56d49c27f82eba9eee88477b7bc956130fcf9691fff6bf002e09a0100f63fe51 1.6s done 2025-11-22T11:52:45.8605896Z #5 extracting sha256:dfde0212bc57fb3ad4e393f72e0e8ff8913d13f2ce52115c01534875af498533 2025-11-22T11:52:47.8980633Z #5 extracting sha256:dfde0212bc57fb3ad4e393f72e0e8ff8913d13f2ce52115c01534875af498533 1.9s done 2025-11-22T11:52:48.1897086Z #5 DONE 32.7s 2025-11-22T11:52:48.3061533Z 2025-11-22T11:52:48.3063507Z #6 [2/4] WORKDIR /opt/oss-fuzz/infra 2025-11-22T11:52:48.3064916Z #6 DONE 0.0s 2025-11-22T11:52:48.3065355Z 2025-11-22T11:52:48.3065591Z #7 [3/4] ADD . /opt/oss-fuzz/infra 2025-11-22T11:52:48.3066840Z #7 DONE 0.1s 2025-11-22T11:52:48.4575318Z 2025-11-22T11:52:48.4576038Z #8 [4/4] RUN python3 -m pip install -r /opt/oss-fuzz/infra/cifuzz/requirements.txt 2025-11-22T11:52:48.7983007Z #8 0.491 Requirement already satisfied: clusterfuzz==2.5.9 in /usr/local/lib/python3.10/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.5.9) 2025-11-22T11:52:48.9036958Z #8 0.492 Requirement already satisfied: requests==2.28.0 in /usr/local/lib/python3.10/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2.28.0) 2025-11-22T11:52:48.9040001Z #8 0.493 Requirement already satisfied: protobuf==3.20.2 in /usr/local/lib/python3.10/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 3)) (3.20.2) 2025-11-22T11:52:48.9042290Z #8 0.493 Requirement already satisfied: gsutil==5.20 in /usr/local/lib/python3.10/site-packages (from -r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (5.20) 2025-11-22T11:52:48.9044339Z #8 0.497 Requirement already satisfied: google-api-python-client in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.162.0) 2025-11-22T11:52:48.9046496Z #8 0.497 Requirement already satisfied: google-auth>=1.22.1 in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.17.0) 2025-11-22T11:52:48.9048843Z #8 0.498 Requirement already satisfied: google-auth-oauthlib in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.2.1) 2025-11-22T11:52:48.9050986Z #8 0.498 Requirement already satisfied: google-cloud-core in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.5.0) 2025-11-22T11:52:48.9053163Z #8 0.499 Requirement already satisfied: google-cloud-datastore==1.12.0 in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.12.0) 2025-11-22T11:52:48.9055114Z #8 0.500 Requirement already satisfied: google-cloud-logging in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.1.2) 2025-11-22T11:52:48.9057491Z #8 0.500 Requirement already satisfied: google-cloud-monitoring in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.27.0) 2025-11-22T11:52:48.9059452Z #8 0.501 Requirement already satisfied: google-cloud-ndb<2.0.0 in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.12.0) 2025-11-22T11:52:48.9061550Z #8 0.501 Requirement already satisfied: google-cloud-storage in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.23.0) 2025-11-22T11:52:48.9065242Z #8 0.502 Requirement already satisfied: grpcio in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.70.0) 2025-11-22T11:52:48.9067034Z #8 0.502 Requirement already satisfied: httplib2 in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.20.4) 2025-11-22T11:52:48.9068840Z #8 0.503 Requirement already satisfied: mozprocess in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.4.0) 2025-11-22T11:52:48.9070466Z #8 0.503 Requirement already satisfied: oauth2client in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.1.3) 2025-11-22T11:52:48.9072064Z #8 0.504 Requirement already satisfied: psutil in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (7.0.0) 2025-11-22T11:52:48.9073574Z #8 0.504 Requirement already satisfied: pytz in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2025.1) 2025-11-22T11:52:48.9074852Z #8 0.505 Requirement already satisfied: PyYAML in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (6.0.2) 2025-11-22T11:52:48.9076128Z #8 0.505 Requirement already satisfied: six in /usr/local/lib/python3.10/site-packages (from clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.15.0) 2025-11-22T11:52:48.9077659Z #8 0.509 Requirement already satisfied: charset-normalizer~=2.0.0 in /usr/local/lib/python3.10/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2.0.12) 2025-11-22T11:52:48.9078993Z #8 0.509 Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (3.10) 2025-11-22T11:52:48.9080291Z #8 0.510 Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.10/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (1.26.20) 2025-11-22T11:52:48.9081631Z #8 0.510 Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/site-packages (from requests==2.28.0->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 2)) (2025.1.31) 2025-11-22T11:52:48.9082978Z #8 0.515 Requirement already satisfied: argcomplete>=1.9.4 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.5.3) 2025-11-22T11:52:48.9084266Z #8 0.516 Requirement already satisfied: crcmod>=1.7 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.7) 2025-11-22T11:52:48.9085532Z #8 0.517 Requirement already satisfied: fasteners>=0.14.1 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.19) 2025-11-22T11:52:48.9086862Z #8 0.517 Requirement already satisfied: gcs-oauth2-boto-plugin>=3.0 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.2) 2025-11-22T11:52:48.9088535Z #8 0.518 Requirement already satisfied: google-apitools>=0.5.32 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.5.32) 2025-11-22T11:52:48.9089875Z #8 0.519 Requirement already satisfied: google-reauth>=0.1.0 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.1.1) 2025-11-22T11:52:48.9091280Z #8 0.519 Requirement already satisfied: monotonic>=1.4 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.6) 2025-11-22T11:52:48.9092583Z #8 0.520 Requirement already satisfied: pyOpenSSL>=0.13 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (25.0.0) 2025-11-22T11:52:48.9093854Z #8 0.520 Requirement already satisfied: retry-decorator>=1.0.0 in /usr/local/lib/python3.10/site-packages (from gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.1.1) 2025-11-22T11:52:48.9095377Z #8 0.523 Requirement already satisfied: google-api-core<2.0.0dev,>=1.14.0 in /usr/local/lib/python3.10/site-packages (from google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.34.1) 2025-11-22T11:52:48.9097031Z #8 0.527 Requirement already satisfied: pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2 in /usr/local/lib/python3.10/site-packages (from httplib2->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.2.1) 2025-11-22T11:52:48.9098568Z #8 0.536 Requirement already satisfied: rsa==4.7.2 in /usr/local/lib/python3.10/site-packages (from gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (4.7.2) 2025-11-22T11:52:48.9099910Z #8 0.537 Requirement already satisfied: boto>=2.29.1 in /usr/local/lib/python3.10/site-packages (from gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.49.0) 2025-11-22T11:52:48.9101336Z #8 0.538 Requirement already satisfied: google-auth-httplib2>=0.2.0 in /usr/local/lib/python3.10/site-packages (from gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.2.0) 2025-11-22T11:52:48.9102773Z #8 0.544 Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.10/site-packages (from google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (5.5.2) 2025-11-22T11:52:48.9104089Z #8 0.545 Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.10/site-packages (from google-auth>=1.22.1->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.4.1) 2025-11-22T11:52:48.9105404Z #8 0.547 Requirement already satisfied: pyasn1>=0.1.3 in /usr/local/lib/python3.10/site-packages (from rsa==4.7.2->gcs-oauth2-boto-plugin>=3.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.6.1) 2025-11-22T11:52:48.9106732Z #8 0.553 Requirement already satisfied: aiohttp<4.0.0dev,>=3.6.2 in /usr/local/lib/python3.10/site-packages (from google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (3.11.13) 2025-11-22T11:52:48.9108148Z #8 0.559 Requirement already satisfied: pymemcache<5.0.0dev,>=2.1.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-ndb<2.0.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.0.0) 2025-11-22T11:52:48.9109468Z #8 0.569 Requirement already satisfied: redis<5.0.0dev,>=3.0.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-ndb<2.0.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.6.0) 2025-11-22T11:52:48.9110709Z #8 0.571 Requirement already satisfied: pyu2f in /usr/local/lib/python3.10/site-packages (from google-reauth>=0.1.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.1.5) 2025-11-22T11:52:48.9112055Z #8 0.581 Requirement already satisfied: cryptography<45,>=41.0.5 in /usr/local/lib/python3.10/site-packages (from pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (44.0.1) 2025-11-22T11:52:48.9113546Z #8 0.582 Requirement already satisfied: typing-extensions>=4.9 in /usr/local/lib/python3.10/site-packages (from pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (4.12.2) 2025-11-22T11:52:48.9114853Z #8 0.597 Requirement already satisfied: uritemplate<5,>=3.0.1 in /usr/local/lib/python3.10/site-packages (from google-api-python-client->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (4.1.1) 2025-11-22T11:52:49.0193694Z #8 0.600 Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.10/site-packages (from google-auth-oauthlib->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (2.0.0) 2025-11-22T11:52:49.0197205Z #8 0.606 Requirement already satisfied: google-cloud-appengine-logging<2.0.0dev,>=0.1.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.6.0) 2025-11-22T11:52:49.0201255Z #8 0.607 Requirement already satisfied: google-cloud-audit-log<1.0.0dev,>=0.1.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.3.0) 2025-11-22T11:52:49.0204790Z #8 0.608 Requirement already satisfied: grpc-google-iam-v1<1.0.0dev,>=0.12.4 in /usr/local/lib/python3.10/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.14.0) 2025-11-22T11:52:49.0208204Z #8 0.608 Requirement already satisfied: proto-plus<2.0.0dev,>=1.15.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-logging->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.26.0) 2025-11-22T11:52:49.0211073Z #8 0.622 Requirement already satisfied: google-resumable-media<0.6dev,>=0.5.0 in /usr/local/lib/python3.10/site-packages (from google-cloud-storage->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (0.5.1) 2025-11-22T11:52:49.0213729Z #8 0.626 Requirement already satisfied: mozinfo in /usr/local/lib/python3.10/site-packages (from mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.2.3) 2025-11-22T11:52:49.0216276Z #8 0.637 Requirement already satisfied: aiohappyeyeballs>=2.3.0 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.4.6) 2025-11-22T11:52:49.0218972Z #8 0.638 Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.3.2) 2025-11-22T11:52:49.0221459Z #8 0.638 Requirement already satisfied: async-timeout<6.0,>=4.0 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (5.0.1) 2025-11-22T11:52:49.0223874Z #8 0.639 Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (25.1.0) 2025-11-22T11:52:49.0226270Z #8 0.640 Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.5.0) 2025-11-22T11:52:49.0228963Z #8 0.640 Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (6.1.0) 2025-11-22T11:52:49.0231649Z #8 0.641 Requirement already satisfied: propcache>=0.2.0 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (0.3.0) 2025-11-22T11:52:49.0234188Z #8 0.642 Requirement already satisfied: yarl<2.0,>=1.17.0 in /usr/local/lib/python3.10/site-packages (from aiohttp<4.0.0dev,>=3.6.2->google-auth[aiohttp]>=2.5.0->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.18.3) 2025-11-22T11:52:49.0236452Z #8 0.654 Requirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.10/site-packages (from cryptography<45,>=41.0.5->pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (1.17.1) 2025-11-22T11:52:49.0239392Z #8 0.659 Requirement already satisfied: googleapis-common-protos<2.0dev,>=1.56.2 in /usr/local/lib/python3.10/site-packages (from google-api-core<2.0.0dev,>=1.14.0->google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.68.0) 2025-11-22T11:52:49.0242402Z #8 0.667 Requirement already satisfied: grpcio-status<2.0dev,>=1.33.2 in /usr/local/lib/python3.10/site-packages (from google-api-core[grpc]<2.0.0dev,>=1.14.0->google-cloud-datastore==1.12.0->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.49.0rc1) 2025-11-22T11:52:49.0245067Z #8 0.713 Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.10/site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.2.2) 2025-11-22T11:52:49.1197299Z #8 0.721 Requirement already satisfied: distro>=1.4.0 in /usr/local/lib/python3.10/site-packages (from mozinfo->mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (1.9.0) 2025-11-22T11:52:49.1200013Z #8 0.722 Requirement already satisfied: mozfile>=0.12 in /usr/local/lib/python3.10/site-packages (from mozinfo->mozprocess->clusterfuzz==2.5.9->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 1)) (3.0.0) 2025-11-22T11:52:49.1202892Z #8 0.746 Requirement already satisfied: pycparser in /usr/local/lib/python3.10/site-packages (from cffi>=1.12->cryptography<45,>=41.0.5->pyOpenSSL>=0.13->gsutil==5.20->-r /opt/oss-fuzz/infra/cifuzz/requirements.txt (line 4)) (2.22) 2025-11-22T11:52:49.1207050Z #8 0.813 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning. 2025-11-22T11:52:49.2490769Z #8 0.884 2025-11-22T11:52:49.2491328Z #8 0.884 [notice] A new release of pip is available: 25.0.1 -> 25.3 2025-11-22T11:52:49.2492037Z #8 0.884 [notice] To update, run: pip3 install --upgrade pip 2025-11-22T11:52:49.2492607Z #8 DONE 0.9s 2025-11-22T11:52:49.4007566Z 2025-11-22T11:52:49.4007987Z #9 exporting to image 2025-11-22T11:52:49.4008432Z #9 exporting layers 2025-11-22T11:52:54.0793856Z #9 exporting layers 4.8s done 2025-11-22T11:52:54.0978483Z #9 writing image sha256:d9694df2528b59e32194f54f9a30bef2358ee99e5c8546b73ef7ca3cae74ef44 done 2025-11-22T11:52:54.0980266Z #9 naming to docker.io/library/207f17:6a803eb2beb646bb83690ddfe4785996 done 2025-11-22T11:52:54.0981058Z #9 DONE 4.8s 2025-11-22T11:52:54.1022702Z ##[endgroup] 2025-11-22T11:52:54.1257184Z ##[group]Run google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 2025-11-22T11:52:54.1258166Z with: 2025-11-22T11:52:54.1258374Z oss-fuzz-project-name: icalendar 2025-11-22T11:52:54.1258637Z language: python 2025-11-22T11:52:54.1258825Z dry-run: false 2025-11-22T11:52:54.1259016Z sanitizer: address 2025-11-22T11:52:54.1259214Z architecture: x86_64 2025-11-22T11:52:54.1259424Z bad-build-check: true 2025-11-22T11:52:54.1259655Z keep-unaffected-fuzz-targets: false 2025-11-22T11:52:54.1259927Z output-sarif: false 2025-11-22T11:52:54.1260759Z ##[endgroup] 2025-11-22T11:52:54.1412863Z ##[command]/usr/bin/docker run --name f17c56653790b0c4db091b6057e2ac6f688_a30638 --label 207f17 --workdir /github/workspace --rm -e "INPUT_OSS-FUZZ-PROJECT-NAME" -e "INPUT_LANGUAGE" -e "INPUT_DRY-RUN" -e "INPUT_ALLOWED-BROKEN-TARGETS-PERCENTAGE" -e "INPUT_SANITIZER" -e "INPUT_ARCHITECTURE" -e "INPUT_PROJECT-SRC-PATH" -e "INPUT_BAD-BUILD-CHECK" -e "INPUT_KEEP-UNAFFECTED-FUZZ-TARGETS" -e "INPUT_OUTPUT-SARIF" -e "OSS_FUZZ_PROJECT_NAME" -e "LANGUAGE" -e "DRY_RUN" -e "ALLOWED_BROKEN_TARGETS_PERCENTAGE" -e "SANITIZER" -e "ARCHITECTURE" -e "PROJECT_SRC_PATH" -e "LOW_DISK_SPACE" -e "BAD_BUILD_CHECK" -e "CIFUZZ_DEBUG" -e "CFL_PLATFORM" -e "KEEP_UNAFFECTED_FUZZ_TARGETS" -e "OUTPUT_SARIF" -e "HOME" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_REPOSITORY_OWNER_ID" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR_ID" -e "GITHUB_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKFLOW_REF" -e "GITHUB_WORKFLOW_SHA" -e "GITHUB_REPOSITORY_ID" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "GITHUB_STATE" -e "GITHUB_OUTPUT" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_ENVIRONMENT" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e "ACTIONS_RESULTS_URL" -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "/home/runner/work/_temp/_github_home":"/github/home" -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" -v "/home/runner/work/icalendar/icalendar":"/github/workspace" 207f17:c56653790b0c4db091b6057e2ac6f688 2025-11-22T11:52:54.5776189Z 2025-11-22 11:52:54,576 - root - DEBUG - Is github: True. 2025-11-22T11:52:54.5782871Z 2025-11-22 11:52:54,577 - root - DEBUG - base_commit: None 2025-11-22T11:52:54.5783594Z 2025-11-22 11:52:54,578 - root - DEBUG - pr_ref: refs/pull/979/merge 2025-11-22T11:52:54.5785058Z 2025-11-22 11:52:54,578 - root - DEBUG - No PROJECT_SRC_PATH. 2025-11-22T11:52:54.5788726Z 2025-11-22 11:52:54,578 - root - INFO - ci_system: . 2025-11-22T11:52:54.5791710Z 2025-11-22 11:52:54,578 - root - INFO - ClusterFuzzDeployment: . 2025-11-22T11:52:54.5792738Z 2025-11-22 11:52:54,578 - root - INFO - InternalGithub: preparing for fuzzer build. 2025-11-22T11:52:54.5794141Z 2025-11-22 11:52:54,579 - helper - INFO - Running: docker build -t gcr.io/oss-fuzz/icalendar --file /opt/oss-fuzz/projects/icalendar/Dockerfile /opt/oss-fuzz/projects/icalendar. 2025-11-22T11:52:54.7564104Z #1 [internal] load build definition from Dockerfile 2025-11-22T11:52:54.7565005Z #1 sha256:e391930fba05eb68e054dfded8948a73860aa3e954c250ed795a1d22a3f24975 2025-11-22T11:52:54.7565795Z #1 transferring dockerfile: 998B done 2025-11-22T11:52:54.7566291Z #1 DONE 0.0s 2025-11-22T11:52:54.7566493Z 2025-11-22T11:52:54.7567678Z #2 [internal] load metadata for gcr.io/oss-fuzz-base/base-builder-python:latest 2025-11-22T11:52:54.7568726Z #2 sha256:84f30eced7823eef020fd84d905a66647e32a084fef851c0e56bfbe804b9c2fa 2025-11-22T11:52:55.4954309Z #2 DONE 0.9s 2025-11-22T11:52:55.6086315Z 2025-11-22T11:52:55.6086896Z #3 [internal] load .dockerignore 2025-11-22T11:52:55.6088324Z #3 sha256:0f7e8869653577e61241ba9a0b18900d0bf00d6792b038ddafb8e56e855f9152 2025-11-22T11:52:55.6089088Z #3 transferring context: 2B done 2025-11-22T11:52:55.6089519Z #3 DONE 0.0s 2025-11-22T11:52:55.6089721Z 2025-11-22T11:52:55.6090543Z #8 [1/5] FROM gcr.io/oss-fuzz-base/base-builder-python:latest@sha256:d4ed838cbf9ffb9835dfa97204b1214c7f7e5cfdee1ca177ce216e051705ee1a 2025-11-22T11:52:55.6092181Z #8 sha256:bff5b21366ceba95e53b89a08fe005ba03e659fc4089c8476d86cdb88b4dc07b 2025-11-22T11:52:55.6093606Z #8 resolve gcr.io/oss-fuzz-base/base-builder-python:latest@sha256:d4ed838cbf9ffb9835dfa97204b1214c7f7e5cfdee1ca177ce216e051705ee1a done 2025-11-22T11:52:55.6095196Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 0B / 338.23MB 0.1s 2025-11-22T11:52:55.7606552Z #8 sha256:d4ed838cbf9ffb9835dfa97204b1214c7f7e5cfdee1ca177ce216e051705ee1a 8.51kB / 8.51kB done 2025-11-22T11:52:55.7609126Z #8 sha256:ed93ccfc6fe70818aa50c2926776917d96ec481fc3f1b44d5afdbec8f1001687 0B / 34.58MB 0.2s 2025-11-22T11:52:55.7611382Z #8 sha256:8491c5900be6227e4d5c1e36e3c5959ea013de24a40367b42d191839d06743d3 27.42kB / 27.42kB done 2025-11-22T11:52:55.7613656Z #8 sha256:2618b58e4f479294ff8b6701829ac440c04cc4bf1ae319013c1643a92cba0392 0B / 3.52kB 0.2s 2025-11-22T11:52:55.9084845Z #8 sha256:ed93ccfc6fe70818aa50c2926776917d96ec481fc3f1b44d5afdbec8f1001687 34.58MB / 34.58MB 0.3s done 2025-11-22T11:52:55.9086299Z #8 sha256:2618b58e4f479294ff8b6701829ac440c04cc4bf1ae319013c1643a92cba0392 3.52kB / 3.52kB 0.2s done 2025-11-22T11:52:55.9087841Z #8 sha256:a5eaf5b768676fc52ad21d4632217c16edd1db840e58227a4ccdb0f752fc02df 0B / 148B 0.4s 2025-11-22T11:52:55.9089275Z #8 sha256:27981f76713a3cf20ff7d8fa2a197868bd93beaeaf1912919e764b14c351e281 0B / 688B 0.4s 2025-11-22T11:52:56.0198053Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 84.93MB / 338.23MB 0.5s 2025-11-22T11:52:56.0201759Z #8 sha256:a5eaf5b768676fc52ad21d4632217c16edd1db840e58227a4ccdb0f752fc02df 148B / 148B 0.4s done 2025-11-22T11:52:56.0202782Z #8 extracting sha256:ed93ccfc6fe70818aa50c2926776917d96ec481fc3f1b44d5afdbec8f1001687 0.1s 2025-11-22T11:52:56.0205211Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 0B / 139.15MB 0.5s 2025-11-22T11:52:56.1219744Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 121.63MB / 338.23MB 0.6s 2025-11-22T11:52:56.1223810Z #8 sha256:27981f76713a3cf20ff7d8fa2a197868bd93beaeaf1912919e764b14c351e281 688B / 688B 0.5s done 2025-11-22T11:52:56.1224959Z #8 sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 0B / 54.66MB 0.6s 2025-11-22T11:52:56.2241106Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 157.29MB / 338.23MB 0.7s 2025-11-22T11:52:56.2246398Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 22.96MB / 139.15MB 0.7s 2025-11-22T11:52:56.3373582Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 191.89MB / 338.23MB 0.8s 2025-11-22T11:52:56.3398928Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 59.77MB / 139.15MB 0.8s 2025-11-22T11:52:56.3400150Z #8 sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 11.53MB / 54.66MB 0.8s 2025-11-22T11:52:56.4508743Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 225.44MB / 338.23MB 0.9s 2025-11-22T11:52:56.4510207Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 92.27MB / 139.15MB 0.9s 2025-11-22T11:52:56.4511623Z #8 sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 39.85MB / 54.66MB 0.9s 2025-11-22T11:52:56.5545361Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 259.67MB / 338.23MB 1.0s 2025-11-22T11:52:56.5546591Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 127.93MB / 139.15MB 1.0s 2025-11-22T11:52:56.5553254Z #8 sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 54.66MB / 54.66MB 1.0s done 2025-11-22T11:52:56.5554388Z #8 sha256:4b84efc607fed0905bb7ba200a91c4a70d69f294685a65002b47417fd3240357 0B / 2.93MB 1.0s 2025-11-22T11:52:56.7049252Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 295.95MB / 338.23MB 1.1s 2025-11-22T11:52:56.7050818Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 139.15MB / 139.15MB 1.1s 2025-11-22T11:52:56.8082108Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 338.23MB / 338.23MB 1.3s 2025-11-22T11:52:56.8083804Z #8 sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 139.15MB / 139.15MB 1.2s done 2025-11-22T11:52:56.8085088Z #8 sha256:4b84efc607fed0905bb7ba200a91c4a70d69f294685a65002b47417fd3240357 2.93MB / 2.93MB 1.2s done 2025-11-22T11:52:56.8085982Z #8 sha256:bf1b8840fcd7975fe8a1f3a2dfa1d67f282fd4a8e01a67a71c28ad117a858e55 0B / 862.71kB 1.3s 2025-11-22T11:52:56.8087261Z #8 sha256:15d5d2f7ad2b6a5dbbe60081fdec0cc717c3c1372b8d909fa40836e882b91245 0B / 2.09MB 1.3s 2025-11-22T11:52:56.9082755Z #8 sha256:bf1b8840fcd7975fe8a1f3a2dfa1d67f282fd4a8e01a67a71c28ad117a858e55 862.71kB / 862.71kB 1.4s 2025-11-22T11:52:56.9084541Z #8 sha256:15d5d2f7ad2b6a5dbbe60081fdec0cc717c3c1372b8d909fa40836e882b91245 2.09MB / 2.09MB 1.4s 2025-11-22T11:52:57.6597210Z #8 sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 338.23MB / 338.23MB 2.0s done 2025-11-22T11:52:57.6598557Z #8 extracting sha256:ed93ccfc6fe70818aa50c2926776917d96ec481fc3f1b44d5afdbec8f1001687 1.7s done 2025-11-22T11:52:57.6599559Z #8 sha256:bf1b8840fcd7975fe8a1f3a2dfa1d67f282fd4a8e01a67a71c28ad117a858e55 862.71kB / 862.71kB 2.0s done 2025-11-22T11:52:57.6600580Z #8 sha256:15d5d2f7ad2b6a5dbbe60081fdec0cc717c3c1372b8d909fa40836e882b91245 2.09MB / 2.09MB 2.0s done 2025-11-22T11:52:57.6601495Z #8 sha256:0c5bc5d6cf42d1cb2f72c8198aa648651c5daf7d78560b709695ab3b5e72fc29 0B / 788B 2.1s 2025-11-22T11:52:57.6602332Z #8 sha256:208e3cf3b62e31c650066547054bda553b5daa8ac4d4f472142f0d16ad965107 0B / 677B 2.1s 2025-11-22T11:52:57.6603169Z #8 sha256:32427479b376647221e3c34c0b3c231383c8161492273a9e7cc025ecba82b383 0B / 3.06MB 2.1s 2025-11-22T11:52:57.7717985Z #8 extracting sha256:2618b58e4f479294ff8b6701829ac440c04cc4bf1ae319013c1643a92cba0392 2025-11-22T11:52:57.9083278Z #8 sha256:0c5bc5d6cf42d1cb2f72c8198aa648651c5daf7d78560b709695ab3b5e72fc29 788B / 788B 2.2s done 2025-11-22T11:52:57.9084527Z #8 sha256:208e3cf3b62e31c650066547054bda553b5daa8ac4d4f472142f0d16ad965107 677B / 677B 2.2s done 2025-11-22T11:52:57.9085922Z #8 sha256:32427479b376647221e3c34c0b3c231383c8161492273a9e7cc025ecba82b383 3.06MB / 3.06MB 2.2s done 2025-11-22T11:52:57.9087171Z #8 extracting sha256:2618b58e4f479294ff8b6701829ac440c04cc4bf1ae319013c1643a92cba0392 done 2025-11-22T11:52:57.9088333Z #8 extracting sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 2025-11-22T11:52:57.9089703Z #8 sha256:4e627b7fa6d4ae7c03ea161256e2747998cf0ce42049ecca20a31a900c35c999 0B / 1.06kB 2.3s 2025-11-22T11:52:57.9090533Z #8 sha256:4a56eda510791ba53daa38416468f1545b3540b62d829c45e7d515cce4e6f45a 0B / 1.22MB 2.3s 2025-11-22T11:52:57.9091333Z #8 sha256:8b6b78f7077baf4e9ca6e7844fd5a9a6b3949113a96e8ea2aea63b0c0d012a51 0B / 5.24MB 2.4s 2025-11-22T11:52:57.9092137Z #8 sha256:594de18a26a8638c6c24fab92472e927d5bcbe9a63062f9d2e4e50b9947eac88 0B / 1.73MB 2.4s 2025-11-22T11:52:58.0083111Z #8 sha256:4e627b7fa6d4ae7c03ea161256e2747998cf0ce42049ecca20a31a900c35c999 1.06kB / 1.06kB 2.4s done 2025-11-22T11:52:58.0084358Z #8 sha256:4a56eda510791ba53daa38416468f1545b3540b62d829c45e7d515cce4e6f45a 1.22MB / 1.22MB 2.4s done 2025-11-22T11:52:58.0085585Z #8 sha256:8b6b78f7077baf4e9ca6e7844fd5a9a6b3949113a96e8ea2aea63b0c0d012a51 5.24MB / 5.24MB 2.4s done 2025-11-22T11:52:58.0087015Z #8 sha256:4738071a5dd33cce857ad4660b2c0e79f579d42b6935d55c8d1b6e35176bc2c1 0B / 1.23kB 2.5s 2025-11-22T11:52:58.0088261Z #8 sha256:861aab9592ff123031ac56938f141e0cc49ea15f7832e8fb886d208c7bdf7dc9 0B / 526.34kB 2.5s 2025-11-22T11:52:58.1593483Z #8 sha256:594de18a26a8638c6c24fab92472e927d5bcbe9a63062f9d2e4e50b9947eac88 1.73MB / 1.73MB 2.6s done 2025-11-22T11:52:58.1595165Z #8 sha256:af7d40390f18f8b9c4b7b0c191e384a520286127d5a3f89dc82189a72d736cce 0B / 3.65MB 2.6s 2025-11-22T11:52:58.3083181Z #8 sha256:4738071a5dd33cce857ad4660b2c0e79f579d42b6935d55c8d1b6e35176bc2c1 1.23kB / 1.23kB 2.6s done 2025-11-22T11:52:58.3085016Z #8 sha256:861aab9592ff123031ac56938f141e0cc49ea15f7832e8fb886d208c7bdf7dc9 526.34kB / 526.34kB 2.6s done 2025-11-22T11:52:58.3087305Z #8 sha256:751e818c97901e6ed60b715ac99e4b74eb7cb7412a76a2f498c1258dbec9e696 0B / 8.88kB 2.8s 2025-11-22T11:52:58.3089301Z #8 sha256:1f15f12faaa5f951d598528575c00467d7cd7cd7587dd728f6f5910a9d80c977 0B / 19.19kB 2.8s 2025-11-22T11:52:58.3090643Z #8 sha256:34316d17cd12abca26ae6178362dca190986a80fde6090682eef931defdab02a 0B / 1.66MB 2.8s 2025-11-22T11:52:58.4083393Z #8 sha256:af7d40390f18f8b9c4b7b0c191e384a520286127d5a3f89dc82189a72d736cce 3.65MB / 3.65MB 2.8s done 2025-11-22T11:52:58.4085555Z #8 sha256:751e818c97901e6ed60b715ac99e4b74eb7cb7412a76a2f498c1258dbec9e696 8.88kB / 8.88kB 2.8s done 2025-11-22T11:52:58.4087743Z #8 sha256:1f15f12faaa5f951d598528575c00467d7cd7cd7587dd728f6f5910a9d80c977 19.19kB / 19.19kB 2.8s done 2025-11-22T11:52:58.4089113Z #8 sha256:ad82a3bcd70fd696f849b9c89b4a2be2561302e6699e3c7aff0859dff05e8706 0B / 1.66MB 2.9s 2025-11-22T11:52:58.5592628Z #8 sha256:34316d17cd12abca26ae6178362dca190986a80fde6090682eef931defdab02a 1.66MB / 1.66MB 2.9s done 2025-11-22T11:52:58.5594151Z #8 sha256:ad82a3bcd70fd696f849b9c89b4a2be2561302e6699e3c7aff0859dff05e8706 1.66MB / 1.66MB 3.0s done 2025-11-22T11:52:58.5595666Z #8 sha256:c0a0b133f474b6036e929d7830fe1399c81f28769a9d87b69d14fa1ef5e10cae 0B / 1.99MB 3.0s 2025-11-22T11:52:58.5597094Z #8 sha256:b29290d1dc948a70cb1c55a8771f3c65d106903cb7000f44c72736a4bcd97210 0B / 1.99MB 3.0s 2025-11-22T11:52:58.5598674Z #8 sha256:ccb4adca7c4bac2fd082e81948128bfc1ae96b5c7bc6625df13910b75d1a609f 0B / 7.31MB 3.0s 2025-11-22T11:52:58.7018429Z #8 sha256:c0a0b133f474b6036e929d7830fe1399c81f28769a9d87b69d14fa1ef5e10cae 1.99MB / 1.99MB 3.0s done 2025-11-22T11:52:58.7019625Z #8 sha256:e549e0f4d76efaa1a5ddf07b60ff55cb9fb282f2b20e619c46e44a694f90628b 0B / 790B 3.1s 2025-11-22T11:52:58.8057603Z #8 sha256:b29290d1dc948a70cb1c55a8771f3c65d106903cb7000f44c72736a4bcd97210 1.99MB / 1.99MB 3.1s done 2025-11-22T11:52:58.8059827Z #8 sha256:ccb4adca7c4bac2fd082e81948128bfc1ae96b5c7bc6625df13910b75d1a609f 7.31MB / 7.31MB 3.2s done 2025-11-22T11:52:58.8061387Z #8 sha256:fc502402e0d3cb2c118b040a70ff1be4a0b2a7ef4af6795cc4510b93b9f0da2d 0B / 2.34kB 3.2s 2025-11-22T11:52:58.8062834Z #8 sha256:70d121973b4398290de7affa47a97beeb271f7badf77dd3daba1e6f5b0fa81fd 0B / 574B 3.2s 2025-11-22T11:52:58.9082783Z #8 sha256:e549e0f4d76efaa1a5ddf07b60ff55cb9fb282f2b20e619c46e44a694f90628b 790B / 790B 3.2s done 2025-11-22T11:52:58.9084729Z #8 sha256:fc502402e0d3cb2c118b040a70ff1be4a0b2a7ef4af6795cc4510b93b9f0da2d 2.34kB / 2.34kB 3.3s done 2025-11-22T11:52:58.9086723Z #8 sha256:70d121973b4398290de7affa47a97beeb271f7badf77dd3daba1e6f5b0fa81fd 574B / 574B 3.4s done 2025-11-22T11:52:58.9088759Z #8 sha256:5b708852848e819085b4a1126bd4409055aab85b33a34bfb232a794796a04f92 0B / 240B 3.4s 2025-11-22T11:52:58.9089995Z #8 sha256:4f4b7363db7496a0a1a4abf8ab57527b1f387e9c197f1b763c91d22abb391484 0B / 1.40MB 3.4s 2025-11-22T11:52:58.9091208Z #8 sha256:cb8ca2bbde9af7abcda4569e66f04e9a1cf7054535eb16ced51b0aec7373f10b 0B / 29.45kB 3.4s 2025-11-22T11:52:59.0154728Z #8 sha256:5b708852848e819085b4a1126bd4409055aab85b33a34bfb232a794796a04f92 240B / 240B 3.4s done 2025-11-22T11:52:59.0156050Z #8 sha256:4f4b7363db7496a0a1a4abf8ab57527b1f387e9c197f1b763c91d22abb391484 1.40MB / 1.40MB 3.5s done 2025-11-22T11:52:59.0157275Z #8 sha256:7b87fea31e4490f904c5448432e3c7ec241070134f202cc16f6dd341c71246b1 0B / 23.75MB 3.5s 2025-11-22T11:52:59.0160018Z #8 sha256:28de8c26b302e0755da6faf0d22ee0bda166159c3c32c2b34936a78d4c0522df 0B / 23.76MB 3.5s 2025-11-22T11:52:59.1196192Z #8 sha256:cb8ca2bbde9af7abcda4569e66f04e9a1cf7054535eb16ced51b0aec7373f10b 29.45kB / 29.45kB 3.6s done 2025-11-22T11:52:59.1197672Z #8 sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 0B / 39.36MB 3.6s 2025-11-22T11:52:59.2285733Z #8 sha256:7b87fea31e4490f904c5448432e3c7ec241070134f202cc16f6dd341c71246b1 23.07MB / 23.75MB 3.7s 2025-11-22T11:52:59.2287211Z #8 sha256:28de8c26b302e0755da6faf0d22ee0bda166159c3c32c2b34936a78d4c0522df 9.44MB / 23.76MB 3.7s 2025-11-22T11:52:59.3325176Z #8 sha256:7b87fea31e4490f904c5448432e3c7ec241070134f202cc16f6dd341c71246b1 23.75MB / 23.75MB 3.7s done 2025-11-22T11:52:59.3326987Z #8 sha256:28de8c26b302e0755da6faf0d22ee0bda166159c3c32c2b34936a78d4c0522df 23.76MB / 23.76MB 3.7s done 2025-11-22T11:52:59.3328428Z #8 sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 4.46MB / 39.36MB 3.8s 2025-11-22T11:52:59.4385824Z #8 sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 12.98MB / 39.36MB 3.9s 2025-11-22T11:52:59.5447878Z #8 sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 39.36MB / 39.36MB 4.0s done 2025-11-22T11:53:02.8952152Z #8 extracting sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 5.1s 2025-11-22T11:53:04.6047148Z #8 extracting sha256:830f20adda997b1fe416e1ae776d7bcba7d7ba33c4f4e7df8badac34270e391d 6.7s done 2025-11-22T11:53:06.8136382Z #8 extracting sha256:a5eaf5b768676fc52ad21d4632217c16edd1db840e58227a4ccdb0f752fc02df 2025-11-22T11:53:06.9626187Z #8 extracting sha256:a5eaf5b768676fc52ad21d4632217c16edd1db840e58227a4ccdb0f752fc02df done 2025-11-22T11:53:06.9627968Z #8 extracting sha256:27981f76713a3cf20ff7d8fa2a197868bd93beaeaf1912919e764b14c351e281 done 2025-11-22T11:53:06.9628987Z #8 extracting sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 0.1s 2025-11-22T11:53:09.7160520Z #8 extracting sha256:2086450adfb708b7e3e572ab856923c34da13d0424f76fab438ea0d644ce66fd 2.7s done 2025-11-22T11:53:10.4543585Z #8 extracting sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 0.1s 2025-11-22T11:53:12.4016386Z #8 extracting sha256:c7082b32c5b327aa62cc7f81cab8340b08e5b78ecd3904536446c0e0e8a756af 1.9s done 2025-11-22T11:53:12.5272873Z #8 extracting sha256:4b84efc607fed0905bb7ba200a91c4a70d69f294685a65002b47417fd3240357 0.0s done 2025-11-22T11:53:12.5274326Z #8 extracting sha256:bf1b8840fcd7975fe8a1f3a2dfa1d67f282fd4a8e01a67a71c28ad117a858e55 0.1s done 2025-11-22T11:53:12.6782396Z #8 extracting sha256:15d5d2f7ad2b6a5dbbe60081fdec0cc717c3c1372b8d909fa40836e882b91245 0.0s done 2025-11-22T11:53:12.6783719Z #8 extracting sha256:208e3cf3b62e31c650066547054bda553b5daa8ac4d4f472142f0d16ad965107 done 2025-11-22T11:53:12.6784883Z #8 extracting sha256:32427479b376647221e3c34c0b3c231383c8161492273a9e7cc025ecba82b383 2025-11-22T11:53:12.9511697Z #8 extracting sha256:32427479b376647221e3c34c0b3c231383c8161492273a9e7cc025ecba82b383 0.2s done 2025-11-22T11:53:12.9512821Z #8 extracting sha256:0c5bc5d6cf42d1cb2f72c8198aa648651c5daf7d78560b709695ab3b5e72fc29 done 2025-11-22T11:53:12.9514022Z #8 extracting sha256:8b6b78f7077baf4e9ca6e7844fd5a9a6b3949113a96e8ea2aea63b0c0d012a51 2025-11-22T11:53:13.1017690Z #8 extracting sha256:8b6b78f7077baf4e9ca6e7844fd5a9a6b3949113a96e8ea2aea63b0c0d012a51 0.1s done 2025-11-22T11:53:13.1018555Z #8 extracting sha256:4a56eda510791ba53daa38416468f1545b3540b62d829c45e7d515cce4e6f45a 0.0s done 2025-11-22T11:53:13.1019335Z #8 extracting sha256:4e627b7fa6d4ae7c03ea161256e2747998cf0ce42049ecca20a31a900c35c999 done 2025-11-22T11:53:13.1020430Z #8 extracting sha256:594de18a26a8638c6c24fab92472e927d5bcbe9a63062f9d2e4e50b9947eac88 2025-11-22T11:53:13.3868463Z #8 extracting sha256:594de18a26a8638c6c24fab92472e927d5bcbe9a63062f9d2e4e50b9947eac88 0.2s done 2025-11-22T11:53:13.3869989Z #8 extracting sha256:861aab9592ff123031ac56938f141e0cc49ea15f7832e8fb886d208c7bdf7dc9 0.1s done 2025-11-22T11:53:13.5239696Z #8 extracting sha256:4738071a5dd33cce857ad4660b2c0e79f579d42b6935d55c8d1b6e35176bc2c1 done 2025-11-22T11:53:13.5240930Z #8 extracting sha256:af7d40390f18f8b9c4b7b0c191e384a520286127d5a3f89dc82189a72d736cce 0.1s done 2025-11-22T11:53:13.6245377Z #8 extracting sha256:751e818c97901e6ed60b715ac99e4b74eb7cb7412a76a2f498c1258dbec9e696 done 2025-11-22T11:53:13.6246273Z #8 extracting sha256:1f15f12faaa5f951d598528575c00467d7cd7cd7587dd728f6f5910a9d80c977 done 2025-11-22T11:53:13.6247135Z #8 extracting sha256:34316d17cd12abca26ae6178362dca190986a80fde6090682eef931defdab02a 0.0s done 2025-11-22T11:53:13.6248429Z #8 extracting sha256:ad82a3bcd70fd696f849b9c89b4a2be2561302e6699e3c7aff0859dff05e8706 0.0s done 2025-11-22T11:53:13.6249556Z #8 extracting sha256:c0a0b133f474b6036e929d7830fe1399c81f28769a9d87b69d14fa1ef5e10cae 2025-11-22T11:53:13.7274757Z #8 extracting sha256:c0a0b133f474b6036e929d7830fe1399c81f28769a9d87b69d14fa1ef5e10cae 0.0s done 2025-11-22T11:53:13.7276677Z #8 extracting sha256:b29290d1dc948a70cb1c55a8771f3c65d106903cb7000f44c72736a4bcd97210 0.0s done 2025-11-22T11:53:13.7278133Z #8 extracting sha256:ccb4adca7c4bac2fd082e81948128bfc1ae96b5c7bc6625df13910b75d1a609f 0.1s done 2025-11-22T11:53:13.8322418Z #8 extracting sha256:e549e0f4d76efaa1a5ddf07b60ff55cb9fb282f2b20e619c46e44a694f90628b done 2025-11-22T11:53:13.8324425Z #8 extracting sha256:fc502402e0d3cb2c118b040a70ff1be4a0b2a7ef4af6795cc4510b93b9f0da2d done 2025-11-22T11:53:13.8326371Z #8 extracting sha256:70d121973b4398290de7affa47a97beeb271f7badf77dd3daba1e6f5b0fa81fd done 2025-11-22T11:53:13.8328578Z #8 extracting sha256:5b708852848e819085b4a1126bd4409055aab85b33a34bfb232a794796a04f92 done 2025-11-22T11:53:13.8329807Z #8 extracting sha256:4f4b7363db7496a0a1a4abf8ab57527b1f387e9c197f1b763c91d22abb391484 0.0s done 2025-11-22T11:53:13.8330895Z #8 extracting sha256:cb8ca2bbde9af7abcda4569e66f04e9a1cf7054535eb16ced51b0aec7373f10b 2025-11-22T11:53:13.9514335Z #8 extracting sha256:cb8ca2bbde9af7abcda4569e66f04e9a1cf7054535eb16ced51b0aec7373f10b done 2025-11-22T11:53:13.9515662Z #8 extracting sha256:7b87fea31e4490f904c5448432e3c7ec241070134f202cc16f6dd341c71246b1 0.1s 2025-11-22T11:53:14.1586534Z #8 extracting sha256:7b87fea31e4490f904c5448432e3c7ec241070134f202cc16f6dd341c71246b1 0.3s done 2025-11-22T11:53:14.1587877Z #8 extracting sha256:28de8c26b302e0755da6faf0d22ee0bda166159c3c32c2b34936a78d4c0522df 2025-11-22T11:53:14.4732875Z #8 extracting sha256:28de8c26b302e0755da6faf0d22ee0bda166159c3c32c2b34936a78d4c0522df 0.3s done 2025-11-22T11:53:14.4733914Z #8 extracting sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 2025-11-22T11:53:15.9861028Z #8 extracting sha256:ec7339b77527803997c122ee47cbd39a538ec41890100cc725fa3afbe675f551 1.4s done 2025-11-22T11:53:16.1212575Z #8 DONE 20.6s 2025-11-22T11:53:16.2715789Z 2025-11-22T11:53:16.2716038Z #7 [2/5] RUN pip3 install --upgrade pip 2025-11-22T11:53:16.2716834Z #7 sha256:bcb4b11e1547e1ada18d8369efb3c150a842049e63a3b712f7affbae3bcd697e 2025-11-22T11:53:16.5285351Z #7 0.406 Requirement already satisfied: pip in /usr/local/lib/python3.11/site-packages (25.3) 2025-11-22T11:53:16.6792365Z #7 0.483 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning. 2025-11-22T11:53:16.8295291Z #7 DONE 0.6s 2025-11-22T11:53:16.8295592Z 2025-11-22T11:53:16.8296610Z #6 [3/5] RUN git clone --depth 1 https://github.com/collective/icalendar.git icalendar && cp icalendar/src/icalendar/fuzzing/build.sh /src/ 2025-11-22T11:53:16.8298488Z #6 sha256:1ce645bade339311a700240bf8dceae9d2f49a643956634f6414e199f4d024a8 2025-11-22T11:53:16.9801175Z #6 0.120 Cloning into 'icalendar'... 2025-11-22T11:53:17.1037107Z #6 DONE 0.4s 2025-11-22T11:53:17.2388620Z 2025-11-22T11:53:17.2389808Z #5 [4/5] RUN mv icalendar/src/icalendar/fuzzing/corpus /src/corpus 2025-11-22T11:53:17.2391065Z #5 sha256:93607237f38ba5d50ac7f98c0b1d2c4074948c64dcc2ca189a63eb03f697214b 2025-11-22T11:53:17.2391910Z #5 DONE 0.1s 2025-11-22T11:53:17.3896485Z 2025-11-22T11:53:17.3897626Z #4 [5/5] WORKDIR /src/icalendar 2025-11-22T11:53:17.3900254Z #4 sha256:511646d0d7d2e50ff58073d222cbba61f7601365bcfe4255ab9c5a5b33fffed4 2025-11-22T11:53:17.3901874Z #4 DONE 0.0s 2025-11-22T11:53:17.3902482Z 2025-11-22T11:53:17.3902745Z #9 exporting to image 2025-11-22T11:53:17.3903446Z #9 sha256:322e45a2f4eb92d0406ba417bcb30c8c631a5bbcab51092fe476c36f52116862 2025-11-22T11:53:17.3904281Z #9 exporting layers 2025-11-22T11:53:19.5649360Z #9 exporting layers 2.3s done 2025-11-22T11:53:19.5651049Z #9 writing image sha256:91a561d6b8050e3cc2e7ab0083b8a4e06c929648581ef421ade07fdfd552eba3 done 2025-11-22T11:53:19.5652762Z #9 naming to gcr.io/oss-fuzz/icalendar:latest done 2025-11-22T11:53:19.5653387Z #9 DONE 2.3s 2025-11-22T11:53:19.8012211Z 2025-11-22 11:53:19,800 - root - INFO - Docker container: 58b4c2d3cbfb. 2025-11-22T11:53:19.8014747Z 2025-11-22 11:53:19,800 - helper - INFO - Running: docker run --privileged --shm-size=2g --platform linux/amd64 --rm -e FUZZING_ENGINE=libfuzzer -e CIFUZZ=True -e SANITIZER=address -e ARCHITECTURE=x86_64 -e FUZZING_LANGUAGE=python -e OUT=/github/workspace/build-out --volumes-from 58b4c2d3cbfb gcr.io/oss-fuzz/icalendar /bin/bash -c 'cp -r /src/icalendar /github/workspace/storage/icalendar'. 2025-11-22T11:53:20.7534525Z 2025-11-22 11:53:20,752 - root - DEBUG - Stderr of command "git fetch origin refs/pull/979/merge" is: From https://github.com/collective/icalendar 2025-11-22T11:53:20.7535823Z * branch refs/pull/979/merge -> FETCH_HEAD 2025-11-22T11:53:20.7536407Z . 2025-11-22T11:53:20.7881380Z 2025-11-22 11:53:20,787 - root - DEBUG - Stderr of command "git checkout -f FETCH_HEAD" is: Note: switching to 'FETCH_HEAD'. 2025-11-22T11:53:20.7882923Z 2025-11-22T11:53:20.7883672Z You are in 'detached HEAD' state. You can look around, make experimental 2025-11-22T11:53:20.7905421Z changes and commit them, and you can discard any commits you make in this 2025-11-22T11:53:20.7906675Z state without impacting any branches by switching back to a branch. 2025-11-22T11:53:20.7907528Z 2025-11-22T11:53:20.7907995Z If you want to create a new branch to retain commits you create, you may 2025-11-22T11:53:20.7909051Z do so (now or later) by using -c with the switch command. Example: 2025-11-22T11:53:20.7909670Z 2025-11-22T11:53:20.7909897Z git switch -c 2025-11-22T11:53:20.7910307Z 2025-11-22T11:53:20.7910486Z Or undo this operation with: 2025-11-22T11:53:20.7910784Z 2025-11-22T11:53:20.7910919Z git switch - 2025-11-22T11:53:20.7911152Z 2025-11-22T11:53:20.7911544Z Turn off this advice by setting config variable advice.detachedHead to false 2025-11-22T11:53:20.7912113Z 2025-11-22T11:53:20.7912784Z HEAD is now at f6063f9 Merge ca5e623a119c6510d438300a667482c5ab9ce6e2 into bb4b4677ba34e477b05a91fed0ceda91e67a72d8 2025-11-22T11:53:20.7913826Z . 2025-11-22T11:53:20.8159508Z 2025-11-22 11:53:20,815 - root - INFO - repo_dir: /github/workspace/storage/icalendar. 2025-11-22T11:53:20.8202586Z 2025-11-22 11:53:20,819 - root - INFO - Docker container: 58b4c2d3cbfb. 2025-11-22T11:53:20.8203820Z 2025-11-22 11:53:20,820 - root - INFO - Building with address sanitizer. 2025-11-22T11:53:20.8206705Z 2025-11-22 11:53:20,820 - helper - INFO - Running: docker run --privileged --shm-size=2g --platform linux/amd64 --rm -e FUZZING_ENGINE=libfuzzer -e CIFUZZ=True -e SANITIZER=address -e ARCHITECTURE=x86_64 -e FUZZING_LANGUAGE=python -e OUT=/github/workspace/build-out --volumes-from 58b4c2d3cbfb gcr.io/oss-fuzz/icalendar /bin/bash -c 'cd / && rm -rf /src/icalendar/* && cp -r /github/workspace/storage/icalendar /src && cd - && compile'. 2025-11-22T11:53:21.0722757Z /src/icalendar 2025-11-22T11:53:21.0738013Z --------------------------------------------------------------- 2025-11-22T11:53:21.0746575Z vm.mmap_rnd_bits = 28 2025-11-22T11:53:21.0804122Z Compiling libFuzzer to /usr/lib/libFuzzingEngine.a... done. 2025-11-22T11:53:21.1529479Z --------------------------------------------------------------- 2025-11-22T11:53:21.1530508Z CC=clang 2025-11-22T11:53:21.1531039Z CXX=clang++ 2025-11-22T11:53:21.1536247Z CFLAGS=-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=unknown-warning-option -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -fno-sanitize=function,leak,vptr, 2025-11-22T11:53:21.1541841Z CXXFLAGS=-O1 -fno-omit-frame-pointer -gline-tables-only -Wno-error=incompatible-function-pointer-types -Wno-error=int-conversion -Wno-error=deprecated-declarations -Wno-error=implicit-function-declaration -Wno-error=implicit-int -Wno-error=unknown-warning-option -Wno-error=vla-cxx-extension -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=fuzzer-no-link -stdlib=libc++ -fno-sanitize=function,leak,vptr 2025-11-22T11:53:21.1544512Z RUSTFLAGS=--cfg fuzzing -Zsanitizer=address -Cdebuginfo=1 -Cforce-frame-pointers 2025-11-22T11:53:21.1545092Z --------------------------------------------------------------- 2025-11-22T11:53:21.1553889Z + cd /src/icalendar 2025-11-22T11:53:21.1554148Z + pip3 install . 2025-11-22T11:53:21.4051039Z Processing /src/icalendar 2025-11-22T11:53:21.4083080Z Installing build dependencies: started 2025-11-22T11:53:22.3513688Z Installing build dependencies: finished with status 'done' 2025-11-22T11:53:22.3521030Z Getting requirements to build wheel: started 2025-11-22T11:53:22.4952282Z Getting requirements to build wheel: finished with status 'done' 2025-11-22T11:53:22.4962761Z Preparing metadata (pyproject.toml): started 2025-11-22T11:53:22.9177985Z Preparing metadata (pyproject.toml): finished with status 'done' 2025-11-22T11:53:22.9647685Z Collecting python-dateutil (from icalendar==0.1.dev2670) 2025-11-22T11:53:22.9917938Z Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB) 2025-11-22T11:53:23.0171404Z Collecting typing-extensions~=4.10 (from icalendar==0.1.dev2670) 2025-11-22T11:53:23.0243124Z Downloading typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB) 2025-11-22T11:53:23.0420397Z Collecting tzdata (from icalendar==0.1.dev2670) 2025-11-22T11:53:23.0493044Z Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB) 2025-11-22T11:53:23.0538068Z Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/site-packages (from python-dateutil->icalendar==0.1.dev2670) (1.15.0) 2025-11-22T11:53:23.0620895Z Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB) 2025-11-22T11:53:23.0763412Z Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB) 2025-11-22T11:53:23.0936361Z Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB) 2025-11-22T11:53:23.1067565Z Building wheels for collected packages: icalendar 2025-11-22T11:53:23.1076730Z Building wheel for icalendar (pyproject.toml): started 2025-11-22T11:53:23.1376619Z Building wheel for icalendar (pyproject.toml): finished with status 'done' 2025-11-22T11:53:23.1388302Z Created wheel for icalendar: filename=icalendar-0.1.dev2670-py3-none-any.whl size=352088 sha256=e68f5c6660e5ffd38ecfb78a8967363fd76ac03ede3823a4d14b8033dda10de1 2025-11-22T11:53:23.1391993Z Stored in directory: /tmp/pip-ephem-wheel-cache-9y6lpaoq/wheels/d0/38/ec/b33610d595a711fdafc0fc2702f814198f783546386dd78ffc 2025-11-22T11:53:23.1424264Z Successfully built icalendar 2025-11-22T11:53:23.1616461Z Installing collected packages: tzdata, typing-extensions, python-dateutil, icalendar 2025-11-22T11:53:23.5393194Z 2025-11-22T11:53:23.5409443Z Successfully installed icalendar-0.1.dev2670 python-dateutil-2.9.0.post0 typing-extensions-4.15.0 tzdata-2025.2 2025-11-22T11:53:23.5414226Z WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning. 2025-11-22T11:53:23.6169416Z ++ find src/icalendar/fuzzing -name '*_fuzzer.py' 2025-11-22T11:53:23.6184880Z + for fuzzer in $(find src/icalendar/fuzzing -name '*_fuzzer.py') 2025-11-22T11:53:23.6185770Z + compile_python_fuzzer src/icalendar/fuzzing/ical_fuzzer.py 2025-11-22T11:53:23.6200534Z + fuzzer_path=src/icalendar/fuzzing/ical_fuzzer.py 2025-11-22T11:53:23.6201376Z + shift 1 2025-11-22T11:53:23.6204464Z ++ basename -s .py src/icalendar/fuzzing/ical_fuzzer.py 2025-11-22T11:53:23.6214797Z + fuzzer_basename=ical_fuzzer 2025-11-22T11:53:23.6215841Z + fuzzer_package=ical_fuzzer.pkg 2025-11-22T11:53:23.6216432Z + PYFUZZ_WORKPATH=/src/pyfuzzworkdir/ 2025-11-22T11:53:23.6217098Z + FUZZ_WORKPATH=/src/pyfuzzworkdir//ical_fuzzer 2025-11-22T11:53:23.6217946Z + [[ address = *introspector* ]] 2025-11-22T11:53:23.6218477Z + [[ address = *coverage* ]] 2025-11-22T11:53:23.6218946Z + [[ 0 != \0 ]] 2025-11-22T11:53:23.6220532Z + rm -rf /src/pyfuzzworkdir/ 2025-11-22T11:53:23.6228286Z + mkdir /src/pyfuzzworkdir/ /src/pyfuzzworkdir//ical_fuzzer 2025-11-22T11:53:23.6243084Z + pyinstaller --distpath /github/workspace/build-out --workpath=/src/pyfuzzworkdir//ical_fuzzer --onefile --name ical_fuzzer.pkg src/icalendar/fuzzing/ical_fuzzer.py 2025-11-22T11:53:23.7286487Z 44 INFO: PyInstaller: 6.10.0, contrib hooks: 2025.9 2025-11-22T11:53:23.7287313Z 44 INFO: Python: 3.11.13 2025-11-22T11:53:23.7295649Z 45 INFO: Platform: Linux-6.11.0-1018-azure-x86_64-with-glibc2.31 2025-11-22T11:53:23.7296648Z 45 INFO: Python environment: /usr/local 2025-11-22T11:53:23.7299572Z 46 INFO: wrote /src/icalendar/ical_fuzzer.pkg.spec 2025-11-22T11:53:23.7311580Z 47 INFO: Module search paths (PYTHONPATH): 2025-11-22T11:53:23.7312301Z ['/usr/local/lib/python311.zip', 2025-11-22T11:53:23.7312717Z '/usr/local/lib/python3.11', 2025-11-22T11:53:23.7313044Z '/usr/local/lib/python3.11/lib-dynload', 2025-11-22T11:53:23.7313416Z '/usr/local/lib/python3.11/site-packages', 2025-11-22T11:53:23.7313775Z '/src/icalendar/src/icalendar/fuzzing'] 2025-11-22T11:53:23.8119992Z 127 INFO: checking Analysis 2025-11-22T11:53:23.8121915Z 128 INFO: Building Analysis because Analysis-00.toc is non existent 2025-11-22T11:53:23.8123090Z 128 INFO: Running Analysis Analysis-00.toc 2025-11-22T11:53:23.8123717Z 128 INFO: Target bytecode optimization level: 0 2025-11-22T11:53:23.8124381Z 128 INFO: Initializing module dependency graph... 2025-11-22T11:53:23.8127742Z 128 INFO: Caching module graph hooks... 2025-11-22T11:53:23.8216598Z 137 INFO: Analyzing base_library.zip ... 2025-11-22T11:53:24.4146332Z 730 INFO: Processing standard module hook 'hook-encodings.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:25.7195274Z 2035 INFO: Processing standard module hook 'hook-pickle.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:26.8175728Z 3133 INFO: Processing standard module hook 'hook-heapq.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:27.0997732Z 3415 INFO: Caching module dependency graph... 2025-11-22T11:53:27.1645533Z 3480 INFO: Looking for Python shared library... 2025-11-22T11:53:27.1723967Z 3488 INFO: Using Python shared library: /usr/local/lib/libpython3.11.so.1.0 2025-11-22T11:53:27.1724952Z 3488 INFO: Analyzing /src/icalendar/src/icalendar/fuzzing/ical_fuzzer.py 2025-11-22T11:53:27.1749175Z 3491 INFO: Processing standard module hook 'hook-atheris.py' from '/usr/local/lib/python3.11/site-packages/atheris' 2025-11-22T11:53:27.3618555Z 3677 INFO: Processing standard module hook 'hook-dateutil.py' from '/usr/local/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/stdhooks' 2025-11-22T11:53:27.4214031Z 3737 INFO: Processing pre-safe-import-module hook 'hook-six.moves.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks/pre_safe_import_module' 2025-11-22T11:53:27.7191729Z 4034 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks/pre_safe_import_module' 2025-11-22T11:53:27.7194125Z 4035 INFO: SetuptoolsInfo: initializing cached setuptools info... 2025-11-22T11:53:31.1194343Z 7435 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:31.2454481Z 7561 INFO: Processing standard module hook 'hook-xml.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:31.7271260Z 8042 INFO: Processing standard module hook 'hook-zoneinfo.py' from '/usr/local/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/stdhooks' 2025-11-22T11:53:31.7661171Z 8081 INFO: Processing standard module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:31.8588499Z 8174 INFO: Processing standard module hook 'hook-platform.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks' 2025-11-22T11:53:31.9138032Z 8229 INFO: Processing module hooks (post-graph stage)... 2025-11-22T11:53:32.1570241Z 8472 INFO: Performing binary vs. data reclassification (3 entries) 2025-11-22T11:53:32.1594318Z 8475 INFO: Looking for ctypes DLLs 2025-11-22T11:53:32.2057942Z 8521 WARNING: Library user32 required via ctypes not found 2025-11-22T11:53:32.2190452Z 8535 INFO: Analyzing run-time hooks ... 2025-11-22T11:53:32.2212295Z 8537 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks/rthooks' 2025-11-22T11:53:32.2226140Z 8538 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks/rthooks' 2025-11-22T11:53:32.2240313Z 8540 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/usr/local/lib/python3.11/site-packages/PyInstaller/hooks/rthooks' 2025-11-22T11:53:32.2293156Z 8545 INFO: Looking for dynamic libraries 2025-11-22T11:53:32.6596941Z 8975 INFO: Warnings written to /src/pyfuzzworkdir/ical_fuzzer/ical_fuzzer.pkg/warn-ical_fuzzer.pkg.txt 2025-11-22T11:53:32.6741155Z 8990 INFO: Graph cross-reference written to /src/pyfuzzworkdir/ical_fuzzer/ical_fuzzer.pkg/xref-ical_fuzzer.pkg.html 2025-11-22T11:53:32.6844874Z 9000 INFO: checking PYZ 2025-11-22T11:53:32.6845682Z 9000 INFO: Building PYZ because PYZ-00.toc is non existent 2025-11-22T11:53:32.6846632Z 9000 INFO: Building PYZ (ZlibArchive) /src/pyfuzzworkdir/ical_fuzzer/ical_fuzzer.pkg/PYZ-00.pyz 2025-11-22T11:53:33.0125160Z 9328 INFO: Building PYZ (ZlibArchive) /src/pyfuzzworkdir/ical_fuzzer/ical_fuzzer.pkg/PYZ-00.pyz completed successfully. 2025-11-22T11:53:33.0231379Z 9339 INFO: checking PKG 2025-11-22T11:53:33.0232222Z 9339 INFO: Building PKG because PKG-00.toc is non existent 2025-11-22T11:53:33.0233603Z 9339 INFO: Building PKG (CArchive) ical_fuzzer.pkg.pkg 2025-11-22T11:53:44.9051338Z 21220 INFO: Building PKG (CArchive) ical_fuzzer.pkg.pkg completed successfully. 2025-11-22T11:53:44.9065009Z 21222 INFO: Bootloader /usr/local/lib/python3.11/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run 2025-11-22T11:53:44.9066828Z 21222 INFO: checking EXE 2025-11-22T11:53:44.9067990Z 21222 INFO: Building EXE because EXE-00.toc is non existent 2025-11-22T11:53:44.9069195Z 21222 INFO: Building EXE from EXE-00.toc 2025-11-22T11:53:44.9070602Z 21222 INFO: Copying bootloader EXE to /github/workspace/build-out/ical_fuzzer.pkg 2025-11-22T11:53:44.9071715Z 21223 INFO: Appending PKG archive to custom ELF section in EXE 2025-11-22T11:53:44.9564638Z 21272 INFO: Building EXE from EXE-00.toc completed successfully. 2025-11-22T11:53:45.0059162Z + chmod -x /github/workspace/build-out/ical_fuzzer.pkg 2025-11-22T11:53:45.0072092Z + [[ address = *coverage* ]] 2025-11-22T11:53:45.0072439Z + echo '#!/bin/sh 2025-11-22T11:53:45.0073097Z # LLVMFuzzerTestOneInput for fuzzer detection. 2025-11-22T11:53:45.0073709Z this_dir=$(dirname "$0") 2025-11-22T11:53:45.0074150Z chmod +x $this_dir/ical_fuzzer.pkg 2025-11-22T11:53:45.0075617Z LD_PRELOAD=$this_dir/sanitizer_with_fuzzer.so ASAN_OPTIONS=$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=$this_dir/llvm-symbolizer:detect_leaks=0 $this_dir/ical_fuzzer.pkg $@' 2025-11-22T11:53:45.0077202Z + chmod +x /github/workspace/build-out/ical_fuzzer 2025-11-22T11:53:45.0091364Z + zip -q /github/workspace/build-out/ical_fuzzer_seed_corpus.zip /src/corpus/Index_Error.ics /src/corpus/Type_Error.ics /src/corpus/america_new_york.ics /src/corpus/calendar_with_unicode.ics /src/corpus/event_with_escaped_character1.ics /src/corpus/event_with_escaped_character2.ics /src/corpus/event_with_escaped_character3.ics /src/corpus/event_with_escaped_character4.ics /src/corpus/event_with_escaped_characters.ics /src/corpus/event_with_recurrence.ics /src/corpus/event_with_recurrence_exdates_on_different_lines.ics /src/corpus/event_with_rsvp.ics /src/corpus/event_with_unicode_fields.ics /src/corpus/event_with_unicode_organizer.ics /src/corpus/issue_100_transformed_doctests_into_unittests.ics /src/corpus/issue_101_icalendar_chokes_on_umlauts_in_organizer.ics /src/corpus/issue_104_mark_events_broken.ics /src/corpus/issue_112_missing_tzinfo_on_exdate.ics /src/corpus/issue_156_RDATE_with_PERIOD.ics /src/corpus/issue_156_RDATE_with_PERIOD_list.ics /src/corpus/issue_157_removes_trailing_semicolon.ics /src/corpus/issue_184_broken_representation_of_period.ics /src/corpus/issue_464_invalid_rdate.ics /src/corpus/issue_53_description_parsed_properly.ics /src/corpus/issue_64_event_with_ascii_summary.ics /src/corpus/issue_64_event_with_non_ascii_summary.ics /src/corpus/issue_70_rrule_causes_attribute_error.ics /src/corpus/issue_82_expected_output.ics /src/corpus/pacific_fiji.ics /src/corpus/time.ics /src/corpus/timezone_rdate.ics /src/corpus/timezone_same_start.ics /src/corpus/timezone_same_start_and_offset.ics /src/corpus/timezoned.ics 2025-11-22T11:53:45.1604227Z 2025-11-22 11:53:45,160 - root - INFO - Removing unaffected fuzz targets. 2025-11-22T11:53:45.2952968Z 2025-11-22 11:53:45,294 - root - DEBUG - Stderr of command "git fetch origin main:main" is: From https://github.com/collective/icalendar 2025-11-22T11:53:45.2954841Z * [new tag] 2.2 -> 2.2 2025-11-22T11:53:45.2955718Z * [new tag] 3.0.1b1 -> 3.0.1b1 2025-11-22T11:53:45.2956609Z * [new tag] 3.0.1b2 -> 3.0.1b2 2025-11-22T11:53:45.2957724Z * [new tag] 3.1 -> 3.1 2025-11-22T11:53:45.2958596Z * [new tag] 3.10 -> 3.10 2025-11-22T11:53:45.2959447Z * [new tag] 3.11 -> 3.11 2025-11-22T11:53:45.2960036Z * [new tag] 3.11.1 -> 3.11.1 2025-11-22T11:53:45.2960655Z * [new tag] 3.11.2 -> 3.11.2 2025-11-22T11:53:45.2961224Z * [new tag] 3.11.3 -> 3.11.3 2025-11-22T11:53:45.2961835Z * [new tag] 3.11.4 -> 3.11.4 2025-11-22T11:53:45.2962410Z * [new tag] 3.11.5 -> 3.11.5 2025-11-22T11:53:45.2963004Z * [new tag] 3.11.6 -> 3.11.6 2025-11-22T11:53:45.2963581Z * [new tag] 3.11.7 -> 3.11.7 2025-11-22T11:53:45.2964140Z * [new tag] 3.12 -> 3.12 2025-11-22T11:53:45.2964726Z * [new tag] 3.1htug1 -> 3.1htug1 2025-11-22T11:53:45.2965330Z * [new tag] 3.1htug2 -> 3.1htug2 2025-11-22T11:53:45.2965933Z * [new tag] 3.2 -> 3.2 2025-11-22T11:53:45.2966517Z * [new tag] 3.3 -> 3.3 2025-11-22T11:53:45.2967068Z * [new tag] 3.4 -> 3.4 2025-11-22T11:53:45.2967818Z * [new tag] 3.4htug1 -> 3.4htug1 2025-11-22T11:53:45.2968654Z * [new tag] 3.4htug2 -> 3.4htug2 2025-11-22T11:53:45.2969214Z * [new tag] 3.4htug3 -> 3.4htug3 2025-11-22T11:53:45.2969826Z * [new tag] 3.5 -> 3.5 2025-11-22T11:53:45.2970500Z * [new tag] 3.6 -> 3.6 2025-11-22T11:53:45.2971133Z * [new tag] 3.6.1 -> 3.6.1 2025-11-22T11:53:45.2971771Z * [new tag] 3.6.2 -> 3.6.2 2025-11-22T11:53:45.2972347Z * [new tag] 3.7 -> 3.7 2025-11-22T11:53:45.2972705Z * [new tag] 3.8 -> 3.8 2025-11-22T11:53:45.2973038Z * [new tag] 3.8.1 -> 3.8.1 2025-11-22T11:53:45.2973700Z * [new tag] 3.8.2 -> 3.8.2 2025-11-22T11:53:45.2974241Z * [new tag] 3.8.3 -> 3.8.3 2025-11-22T11:53:45.2974799Z * [new tag] 3.8.4 -> 3.8.4 2025-11-22T11:53:45.2975352Z * [new tag] 3.9.0 -> 3.9.0 2025-11-22T11:53:45.2975839Z * [new tag] 3.9.1 -> 3.9.1 2025-11-22T11:53:45.2976288Z * [new tag] 3.9.2 -> 3.9.2 2025-11-22T11:53:45.2976730Z * [new tag] 4.0.0 -> 4.0.0 2025-11-22T11:53:45.2977082Z * [new tag] 4.0.1 -> 4.0.1 2025-11-22T11:53:45.2977568Z * [new tag] 4.0.2 -> 4.0.2 2025-11-22T11:53:45.2977879Z * [new tag] 4.0.3 -> 4.0.3 2025-11-22T11:53:45.2978172Z * [new tag] 4.0.4 -> 4.0.4 2025-11-22T11:53:45.2978461Z * [new tag] 4.0.5 -> 4.0.5 2025-11-22T11:53:45.2978747Z * [new tag] 4.0.6 -> 4.0.6 2025-11-22T11:53:45.2979033Z * [new tag] 4.0.7 -> 4.0.7 2025-11-22T11:53:45.2979314Z * [new tag] 4.0.8 -> 4.0.8 2025-11-22T11:53:45.2979602Z * [new tag] 4.0.9 -> 4.0.9 2025-11-22T11:53:45.2979891Z * [new tag] 5.0.0a1 -> 5.0.0a1 2025-11-22T11:53:45.2980300Z * [new tag] before-timezone-refactoring -> before-timezone-refactoring 2025-11-22T11:53:45.2980695Z * [new tag] v5.0.0 -> v5.0.0 2025-11-22T11:53:45.2980997Z * [new tag] v5.0.1 -> v5.0.1 2025-11-22T11:53:45.2981287Z * [new tag] v5.0.10 -> v5.0.10 2025-11-22T11:53:45.2981573Z * [new tag] v5.0.11 -> v5.0.11 2025-11-22T11:53:45.2981857Z * [new tag] v5.0.13 -> v5.0.13 2025-11-22T11:53:45.2982132Z * [new tag] v5.0.2 -> v5.0.2 2025-11-22T11:53:45.2982407Z * [new tag] v5.0.3 -> v5.0.3 2025-11-22T11:53:45.2982680Z * [new tag] v5.0.4 -> v5.0.4 2025-11-22T11:53:45.2982959Z * [new tag] v5.0.5 -> v5.0.5 2025-11-22T11:53:45.2983232Z * [new tag] v5.0.6 -> v5.0.6 2025-11-22T11:53:45.2983502Z * [new tag] v5.0.7 -> v5.0.7 2025-11-22T11:53:45.2983777Z * [new tag] v5.0.8 -> v5.0.8 2025-11-22T11:53:45.2984049Z * [new tag] v5.0.9 -> v5.0.9 2025-11-22T11:53:45.2984324Z * [new tag] v6.0.0 -> v6.0.0 2025-11-22T11:53:45.2984610Z * [new tag] v6.0.0a0 -> v6.0.0a0 2025-11-22T11:53:45.2984894Z * [new tag] v6.0.1 -> v6.0.1 2025-11-22T11:53:45.2985167Z * [new tag] v6.1.0 -> v6.1.0 2025-11-22T11:53:45.2985445Z * [new tag] v6.1.1 -> v6.1.1 2025-11-22T11:53:45.2985724Z * [new tag] v6.1.2 -> v6.1.2 2025-11-22T11:53:45.2985993Z * [new tag] v6.1.3 -> v6.1.3 2025-11-22T11:53:45.2986430Z * [new tag] v6.2.0 -> v6.2.0 2025-11-22T11:53:45.2986707Z * [new tag] v6.3.0 -> v6.3.0 2025-11-22T11:53:45.2986980Z * [new tag] v6.3.1 -> v6.3.1 2025-11-22T11:53:45.2987265Z * [new tag] v7.0.0a1 -> v7.0.0a1 2025-11-22T11:53:45.2987718Z . 2025-11-22T11:53:45.2987977Z 2025-11-22 11:53:45,297 - root - DEBUG - Diffing against base_ref: main. 2025-11-22T11:53:45.2988349Z 2025-11-22 11:53:45,297 - root - INFO - Diffing against main. 2025-11-22T11:53:45.3022440Z 2025-11-22 11:53:45,300 - root - INFO - Files changed in PR: ['CHANGES.rst', 'docs/styles/config/vocabularies/icalendar/accept.txt', 'pyproject.toml', 'src/icalendar/__init__.py', 'src/icalendar/cal/availability.py', 'src/icalendar/cal/available.py', 'src/icalendar/cal/component.py', 'src/icalendar/cal/component_factory.py', 'src/icalendar/error.py', 'src/icalendar/fuzzing/ical_fuzzer.py', 'src/icalendar/param.py', 'src/icalendar/parser.py', 'src/icalendar/prop/__init__.py', 'src/icalendar/tests/calendars/fuzz_testcase_0_char_in_component_name.ics', 'src/icalendar/tests/calendars/fuzz_testcase_invalid_month.ics', 'src/icalendar/tests/calendars/issue_82_expected_output.ics', 'src/icalendar/tests/calendars/rfc_7256_multi_value_parameters.ics', 'src/icalendar/tests/calendars/rfc_7265_example_1.ics', 'src/icalendar/tests/calendars/rfc_7265_example_2.jcal', 'src/icalendar/tests/calendars/rfc_7265_example_3.jcal', 'src/icalendar/tests/conftest.py', 'src/icalendar/tests/data.py', 'src/icalendar/tests/events/rfc_7265_example_4.ics', 'src/icalendar/tests/events/rfc_7265_request_status.ics', 'src/icalendar/tests/fuzzed/__init__.py', 'src/icalendar/tests/fuzzed/test_fuzzed_calendars.py', 'src/icalendar/tests/prop/test_common_functionality.py', 'src/icalendar/tests/prop/test_date_and_time.py', 'src/icalendar/tests/rfc_7265_jcal/__init__.py', 'src/icalendar/tests/rfc_7265_jcal/test_additional_considerations.py', 'src/icalendar/tests/rfc_7265_jcal/test_categories.py', 'src/icalendar/tests/rfc_7265_jcal/test_examples.py', 'src/icalendar/tests/rfc_7265_jcal/test_invalid_jcal.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_3_3_components.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_3_4_properties.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_3_5_parameters.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_3_6_values.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_4_ical_parsing.py', 'src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py', 'src/icalendar/tests/test_issue_798_property_parameters.py', 'src/icalendar/tests/test_parameter_access.py', 'src/icalendar/tests/test_parsing.py', 'src/icalendar/tests/test_timezone_identification.py', 'src/icalendar/timezone/__init__.py', 'src/icalendar/timezone/tzid.py'] 2025-11-22T11:53:45.3071183Z 2025-11-22 11:53:45,306 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): storage.googleapis.com:443 2025-11-22T11:53:45.4239731Z 2025-11-22 11:53:45,423 - urllib3.connectionpool - DEBUG - https://storage.googleapis.com:443 "GET /oss-fuzz-coverage/latest_report_info/icalendar.json HTTP/1.1" 200 315 2025-11-22T11:53:45.4267285Z 2025-11-22 11:53:45,426 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): storage.googleapis.com:443 2025-11-22T11:53:45.6030768Z 2025-11-22 11:53:45,602 - urllib3.connectionpool - DEBUG - https://storage.googleapis.com:443 "GET /oss-fuzz-coverage/icalendar/fuzzer_stats/20251122/ical_fuzzer.json HTTP/1.1" 200 3 2025-11-22T11:53:45.6042275Z 2025-11-22 11:53:45,603 - root - INFO - No coverage available for ical_fuzzer. 2025-11-22T11:53:45.6043687Z 2025-11-22 11:53:45,604 - root - INFO - Could not get coverage for ical_fuzzer. Treating as affected. 2025-11-22T11:53:45.6046864Z 2025-11-22 11:53:45,604 - root - INFO - Using affected fuzz targets: {'/github/workspace/build-out/ical_fuzzer'}. 2025-11-22T11:53:45.6048212Z 2025-11-22 11:53:45,604 - root - INFO - Removing unaffected fuzz targets: set(). 2025-11-22T11:53:56.3265538Z 2025-11-22 11:53:56,326 - root - INFO - Build check passed. 2025-11-22T11:53:56.3266343Z Build check: stdout: INFO: performing bad build checks for /tmp/not-out/tmp13c437a9/ical_fuzzer 2025-11-22T11:53:56.3266892Z 2025-11-22T11:53:56.3267000Z stderr: 2025-11-22T11:53:56.4660821Z ##[group]Run google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 2025-11-22T11:53:56.4661197Z with: 2025-11-22T11:53:56.4661405Z oss-fuzz-project-name: icalendar 2025-11-22T11:53:56.4661665Z language: python 2025-11-22T11:53:56.4661860Z fuzz-seconds: 600 2025-11-22T11:53:56.4662057Z output-sarif: true 2025-11-22T11:53:56.4662253Z dry-run: false 2025-11-22T11:53:56.4662444Z sanitizer: address 2025-11-22T11:53:56.4662835Z mode: code-change 2025-11-22T11:53:56.4663060Z report-unreproducible-crashes: false 2025-11-22T11:53:56.4663332Z minimize-crashes: false 2025-11-22T11:53:56.4663558Z parallel-fuzzing: false 2025-11-22T11:53:56.4663770Z report-timeouts: true 2025-11-22T11:53:56.4663976Z report-ooms: true 2025-11-22T11:53:56.4664174Z ##[endgroup] 2025-11-22T11:53:56.4688773Z ##[command]/usr/bin/docker run --name f176a803eb2beb646bb83690ddfe4785996_35647d --label 207f17 --workdir /github/workspace --rm -e "INPUT_OSS-FUZZ-PROJECT-NAME" -e "INPUT_LANGUAGE" -e "INPUT_FUZZ-SECONDS" -e "INPUT_OUTPUT-SARIF" -e "INPUT_DRY-RUN" -e "INPUT_SANITIZER" -e "INPUT_MODE" -e "INPUT_GITHUB-TOKEN" -e "INPUT_REPORT-UNREPRODUCIBLE-CRASHES" -e "INPUT_MINIMIZE-CRASHES" -e "INPUT_PARALLEL-FUZZING" -e "INPUT_REPORT-TIMEOUTS" -e "INPUT_REPORT-OOMS" -e "OSS_FUZZ_PROJECT_NAME" -e "LANGUAGE" -e "FUZZ_SECONDS" -e "DRY_RUN" -e "SANITIZER" -e "MODE" -e "GITHUB_TOKEN" -e "LOW_DISK_SPACE" -e "REPORT_UNREPRODUCIBLE_CRASHES" -e "MINIMIZE_CRASHES" -e "CIFUZZ_DEBUG" -e "CFL_PLATFORM" -e "PARALLEL_FUZZING" -e "OUTPUT_SARIF" -e "REPORT_TIMEOUTS" -e "REPORT_OOMS" -e "HOME" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_REPOSITORY_OWNER_ID" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR_ID" -e "GITHUB_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKFLOW_REF" -e "GITHUB_WORKFLOW_SHA" -e "GITHUB_REPOSITORY_ID" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "GITHUB_STATE" -e "GITHUB_OUTPUT" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_ENVIRONMENT" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e "ACTIONS_RESULTS_URL" -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "/home/runner/work/_temp/_github_home":"/github/home" -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" -v "/home/runner/work/icalendar/icalendar":"/github/workspace" 207f17:6a803eb2beb646bb83690ddfe4785996 2025-11-22T11:53:56.9975806Z 2025-11-22 11:53:56,997 - root - DEBUG - Is github: True. 2025-11-22T11:53:56.9991891Z 2025-11-22 11:53:56,998 - root - DEBUG - base_commit: None 2025-11-22T11:53:56.9993179Z 2025-11-22 11:53:56,998 - root - DEBUG - pr_ref: refs/pull/979/merge 2025-11-22T11:53:56.9994490Z 2025-11-22 11:53:56,999 - root - DEBUG - No PROJECT_SRC_PATH. 2025-11-22T11:53:56.9998332Z 2025-11-22 11:53:56,999 - root - INFO - Deleting builder docker images to save disk space. 2025-11-22T11:53:57.0322263Z 2025-11-22 11:53:57,031 - root - DEBUG - Stderr of command "docker rmi -f gcr.io/oss-fuzz/icalendar gcr.io/oss-fuzz-base/base-builder gcr.io/oss-fuzz-base/base-builder-go gcr.io/oss-fuzz-base/base-builder-javascript gcr.io/oss-fuzz-base/base-builder-jvm gcr.io/oss-fuzz-base/base-builder-python gcr.io/oss-fuzz-base/base-builder-rust gcr.io/oss-fuzz-base/base-builder-ruby gcr.io/oss-fuzz-base/base-builder-swift" is: Error: No such image: gcr.io/oss-fuzz-base/base-builder 2025-11-22T11:53:57.0325127Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-go 2025-11-22T11:53:57.0325624Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-javascript 2025-11-22T11:53:57.0326109Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-jvm 2025-11-22T11:53:57.0326585Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-python 2025-11-22T11:53:57.0327061Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-rust 2025-11-22T11:53:57.0327731Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-ruby 2025-11-22T11:53:57.0328372Z Error: No such image: gcr.io/oss-fuzz-base/base-builder-swift 2025-11-22T11:53:57.0328777Z . 2025-11-22T11:53:58.4083657Z 2025-11-22 11:53:58,407 - root - INFO - ClusterFuzzDeployment: . 2025-11-22T11:53:58.4085654Z 2025-11-22 11:53:58,408 - root - INFO - run fuzzers MODE is: code-change. Runner: . 2025-11-22T11:53:58.4087055Z 2025-11-22 11:53:58,408 - root - INFO - Using address sanitizer. 2025-11-22T11:53:58.4128823Z 2025-11-22 11:53:58,412 - root - INFO - Fuzz targets: ['/github/workspace/build-out/ical_fuzzer'] 2025-11-22T11:53:58.4132279Z 2025-11-22 11:53:58,412 - root - INFO - Running fuzzer: ical_fuzzer. 2025-11-22T11:53:58.4135079Z 2025-11-22 11:53:58,413 - root - INFO - Downloading corpus from OSS-Fuzz: https://storage.googleapis.com/icalendar-backup.clusterfuzz-external.appspot.com/corpus/libFuzzer/icalendar_ical_fuzzer/public.zip 2025-11-22T11:53:58.4266937Z 2025-11-22 11:53:58,426 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): storage.googleapis.com:443 2025-11-22T11:53:58.6130065Z 2025-11-22 11:53:58,612 - urllib3.connectionpool - DEBUG - https://storage.googleapis.com:443 "GET /icalendar-backup.clusterfuzz-external.appspot.com/corpus/libFuzzer/icalendar_ical_fuzzer/public.zip HTTP/1.1" 200 468762 2025-11-22T11:53:58.8810519Z 2025-11-22 11:53:58,880 - root - INFO - Starting fuzzing 2025-11-22T11:54:04.7696184Z 2025-11-22 11:54:04,769 - root - INFO - Fuzzer: ical_fuzzer. Detected bug. 2025-11-22T11:54:04.7697294Z Fuzzing logs: 2025-11-22T11:54:04.7698087Z INFO: Instrumenting icalendar 2025-11-22T11:54:04.7702373Z WARNING: It looks like this module is imported by a custom loader. Atheris has experimental support for this. However, it may be incompatible with certain libraries. If you experience unusual errors or poor coverage collection, try atheris.instrument_all() instead, add enable_loader_override=False to instrument_imports(), or file an issue on GitHub. 2025-11-22T11:54:04.7706130Z INFO: Instrumenting icalendar.alarms 2025-11-22T11:54:04.7706651Z INFO: Instrumenting __future__ 2025-11-22T11:54:04.7707099Z INFO: Instrumenting datetime 2025-11-22T11:54:04.7707684Z INFO: Instrumenting icalendar.cal 2025-11-22T11:54:04.7708182Z INFO: Instrumenting icalendar.cal.alarm 2025-11-22T11:54:04.7708678Z INFO: Instrumenting icalendar.attr 2025-11-22T11:54:04.7709174Z INFO: Instrumenting icalendar.enums 2025-11-22T11:54:04.7709666Z INFO: Instrumenting icalendar.error 2025-11-22T11:54:04.7710175Z INFO: Instrumenting icalendar.parser_tools 2025-11-22T11:54:04.7710705Z INFO: Instrumenting icalendar.prop 2025-11-22T11:54:04.7711217Z INFO: Instrumenting icalendar.caselessdict 2025-11-22T11:54:04.7711766Z INFO: Instrumenting icalendar.parser 2025-11-22T11:54:04.7712269Z INFO: Instrumenting icalendar.timezone 2025-11-22T11:54:04.7712813Z INFO: Instrumenting icalendar.timezone.tzid 2025-11-22T11:54:04.7713341Z INFO: Instrumenting dateutil 2025-11-22T11:54:04.7713816Z INFO: Instrumenting dateutil._version 2025-11-22T11:54:04.7714314Z INFO: Instrumenting dateutil.tz 2025-11-22T11:54:04.7714769Z INFO: Instrumenting dateutil.tz.tz 2025-11-22T11:54:04.7715165Z INFO: Instrumenting six 2025-11-22T11:54:04.7715552Z INFO: Instrumenting dateutil.tz._common 2025-11-22T11:54:04.7716753Z 2025-11-22 11:54:04,769 - root - INFO - Trying to reproduce crash using: /tmp/tmpp8_6szi9/crash-8b27cc3e60b1a6a95cf42ad106ed19b30a848197. 2025-11-22T11:54:04.7717830Z INFO: Instrumenting dateutil.tz._factories 2025-11-22T11:54:04.7718463Z INFO: Instrumenting dateutil.tz.win 2025-11-22T11:54:04.7718910Z INFO: Instrumenting icalendar.timezone.equivalent_timezone_ids_result 2025-11-22T11:54:04.7719396Z INFO: Instrumenting icalendar.tools 2025-11-22T11:54:04.7719667Z INFO: Instrumenting typing_extensions 2025-11-22T11:54:04.7719948Z INFO: Instrumenting icalendar.timezone.tzp 2025-11-22T11:54:04.7720373Z INFO: Instrumenting icalendar.timezone.windows_to_olson 2025-11-22T11:54:04.7720956Z INFO: Instrumenting icalendar.timezone.zoneinfo 2025-11-22T11:54:04.7721286Z INFO: Instrumenting copy 2025-11-22T11:54:04.7721518Z INFO: Instrumenting dateutil.rrule 2025-11-22T11:54:04.7721869Z INFO: Instrumenting calendar 2025-11-22T11:54:04.7722105Z INFO: Instrumenting heapq 2025-11-22T11:54:04.7722343Z INFO: Instrumenting dateutil._common 2025-11-22T11:54:04.7722714Z INFO: Instrumenting icalendar.compatibility 2025-11-22T11:54:04.7723002Z INFO: Instrumenting zoneinfo 2025-11-22T11:54:04.7723247Z INFO: Instrumenting zoneinfo._tzpath 2025-11-22T11:54:04.7723605Z INFO: Instrumenting sysconfig 2025-11-22T11:54:04.7723908Z INFO: Instrumenting _sysconfigdata__linux_x86_64-linux-gnu 2025-11-22T11:54:04.7724319Z INFO: Instrumenting zoneinfo._common 2025-11-22T11:54:04.7724661Z INFO: Instrumenting icalendar.timezone.provider 2025-11-22T11:54:04.7725122Z INFO: Instrumenting icalendar.param 2025-11-22T11:54:04.7725401Z INFO: Instrumenting icalendar.prop.conference 2025-11-22T11:54:04.7725669Z INFO: Instrumenting dataclasses 2025-11-22T11:54:04.7726097Z INFO: Instrumenting icalendar.prop.image 2025-11-22T11:54:04.7726578Z INFO: Instrumenting icalendar.cal.component 2025-11-22T11:54:04.7727008Z INFO: Instrumenting json 2025-11-22T11:54:04.7727513Z INFO: Instrumenting json.decoder 2025-11-22T11:54:04.7727847Z INFO: Instrumenting json.scanner 2025-11-22T11:54:04.7728069Z INFO: Instrumenting json.encoder 2025-11-22T11:54:04.7728319Z INFO: Instrumenting icalendar.cal.component_factory 2025-11-22T11:54:04.7728730Z INFO: Instrumenting icalendar.cal.availability 2025-11-22T11:54:04.7728995Z INFO: Instrumenting uuid 2025-11-22T11:54:04.7729246Z INFO: Instrumenting platform 2025-11-22T11:54:04.7729572Z INFO: Instrumenting icalendar.cal.examples 2025-11-22T11:54:04.7729838Z INFO: Instrumenting icalendar.cal.available 2025-11-22T11:54:04.7730194Z INFO: Instrumenting icalendar.cal.calendar 2025-11-22T11:54:04.7730456Z INFO: Instrumenting icalendar.cal.timezone 2025-11-22T11:54:04.7730719Z INFO: Instrumenting icalendar.version 2025-11-22T11:54:04.7731058Z INFO: Instrumenting icalendar._version 2025-11-22T11:54:04.7731312Z INFO: Instrumenting icalendar.cal.event 2025-11-22T11:54:04.7731570Z INFO: Instrumenting icalendar.cal.free_busy 2025-11-22T11:54:04.7731923Z INFO: Instrumenting icalendar.cal.journal 2025-11-22T11:54:04.7732180Z INFO: Instrumenting icalendar.cal.todo 2025-11-22T11:54:04.7732465Z INFO: Instrumenting icalendar.tests 2025-11-22T11:54:04.7732763Z INFO: Instrumenting icalendar.tests.fuzzed 2025-11-22T11:54:04.7733007Z INFO: Using preloaded libfuzzer 2025-11-22T11:54:04.7733359Z INFO: Running with entropic power schedule (0xFF, 100). 2025-11-22T11:54:04.7733634Z INFO: Seed: 1337 2025-11-22T11:54:04.7733898Z INFO: 1918 files found in /github/workspace/cifuzz-corpus/ical_fuzzer 2025-11-22T11:54:04.7734472Z INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 1048576 bytes 2025-11-22T11:54:04.7735052Z INFO: seed corpus: files: 1918 min: 1b max: 2491770b total: 8047833b rss: 266Mb 2025-11-22T11:54:04.7735544Z --- start calendar --- 2025-11-22T11:54:04.7735767Z 2025-11-22T11:54:04.7735888Z --- end calendar --- 2025-11-22T11:54:04.7736207Z --- start calendar --- 2025-11-22T11:54:04.7736407Z 2025-11-22T11:54:04.7736531Z --- end calendar --- 2025-11-22T11:54:04.7736802Z --- start calendar --- 2025-11-22T11:54:04.7736919Z 2025-11-22T11:54:04.7736984Z --- end calendar --- 2025-11-22T11:54:04.7737159Z --- start calendar --- 2025-11-22T11:54:04.7737325Z Ig== 2025-11-22T11:54:04.7737731Z --- end calendar --- 2025-11-22T11:54:04.7738140Z --- start calendar --- 2025-11-22T11:54:04.7738454Z Og== 2025-11-22T11:54:04.7738709Z --- end calendar --- 2025-11-22T11:54:04.7739019Z --- start calendar --- 2025-11-22T11:54:04.7739329Z TQ== 2025-11-22T11:54:04.7739573Z --- end calendar --- 2025-11-22T11:54:04.7739884Z --- start calendar --- 2025-11-22T11:54:04.7740212Z Cg== 2025-11-22T11:54:04.7740472Z --- end calendar --- 2025-11-22T11:54:04.7740779Z --- start calendar --- 2025-11-22T11:54:04.7741074Z Qjo= 2025-11-22T11:54:04.7741554Z --- end calendar --- 2025-11-22T11:54:04.7741882Z --- start calendar --- 2025-11-22T11:54:04.7742176Z Ojo= 2025-11-22T11:54:04.7742434Z --- end calendar --- 2025-11-22T11:54:04.7742742Z --- start calendar --- 2025-11-22T11:54:04.7743040Z IiI= 2025-11-22T11:54:04.7743296Z --- end calendar --- 2025-11-22T11:54:04.7743600Z --- start calendar --- 2025-11-22T11:54:04.7743902Z Il8= 2025-11-22T11:54:04.7744145Z --- end calendar --- 2025-11-22T11:54:04.7744451Z --- start calendar --- 2025-11-22T11:54:04.7744738Z Il8= 2025-11-22T11:54:04.7745001Z --- end calendar --- 2025-11-22T11:54:04.7745305Z --- start calendar --- 2025-11-22T11:54:04.7745603Z Qjs= 2025-11-22T11:54:04.7745851Z --- end calendar --- 2025-11-22T11:54:04.7746158Z --- start calendar --- 2025-11-22T11:54:04.7746451Z XFw= 2025-11-22T11:54:04.7746690Z --- end calendar --- 2025-11-22T11:54:04.7747004Z --- start calendar --- 2025-11-22T11:54:04.7747305Z --- end calendar --- 2025-11-22T11:54:04.7747793Z --- start calendar --- 2025-11-22T11:54:04.7748064Z 74is 2025-11-22T11:54:04.7748286Z --- end calendar --- 2025-11-22T11:54:04.7748550Z --- start calendar --- 2025-11-22T11:54:04.7748826Z Zzsi 2025-11-22T11:54:04.7749067Z --- end calendar --- 2025-11-22T11:54:04.7749350Z --- start calendar --- 2025-11-22T11:54:04.7749625Z Qjs9 2025-11-22T11:54:04.7749848Z --- end calendar --- 2025-11-22T11:54:04.7750134Z --- start calendar --- 2025-11-22T11:54:04.7750417Z NABa 2025-11-22T11:54:04.7750657Z --- end calendar --- 2025-11-22T11:54:04.7751058Z --- start calendar --- 2025-11-22T11:54:04.7751330Z TDs7 2025-11-22T11:54:04.7751573Z --- end calendar --- 2025-11-22T11:54:04.7751861Z --- start calendar --- 2025-11-22T11:54:04.7752136Z YgpF 2025-11-22T11:54:04.7752363Z --- end calendar --- 2025-11-22T11:54:04.7752646Z --- start calendar --- 2025-11-22T11:54:04.7752920Z CgoK 2025-11-22T11:54:04.7753147Z --- end calendar --- 2025-11-22T11:54:04.7753432Z --- start calendar --- 2025-11-22T11:54:04.7753714Z Ojo6 2025-11-22T11:54:04.7753954Z --- end calendar --- 2025-11-22T11:54:04.7754247Z --- start calendar --- 2025-11-22T11:54:04.7754502Z YTtp 2025-11-22T11:54:04.7754723Z --- end calendar --- 2025-11-22T11:54:04.7755004Z --- start calendar --- 2025-11-22T11:54:04.7755275Z TjE6 2025-11-22T11:54:04.7755511Z --- end calendar --- 2025-11-22T11:54:04.7755789Z --- start calendar --- 2025-11-22T11:54:04.7756071Z TkQ6 2025-11-22T11:54:04.7756310Z --- end calendar --- 2025-11-22T11:54:04.7756615Z --- start calendar --- 2025-11-22T11:54:04.7756948Z IiIi 2025-11-22T11:54:04.7757176Z --- end calendar --- 2025-11-22T11:54:04.7757643Z --- start calendar --- 2025-11-22T11:54:04.7757940Z elxc 2025-11-22T11:54:04.7758197Z --- end calendar --- 2025-11-22T11:54:04.7758501Z --- start calendar --- 2025-11-22T11:54:04.7758807Z RU5VOg== 2025-11-22T11:54:04.7759065Z --- end calendar --- 2025-11-22T11:54:04.7759362Z --- start calendar --- 2025-11-22T11:54:04.7759649Z RjEtOg== 2025-11-22T11:54:04.7759901Z --- end calendar --- 2025-11-22T11:54:04.7760191Z --- start calendar --- 2025-11-22T11:54:04.7760498Z RU4zOg== 2025-11-22T11:54:04.7760745Z --- end calendar --- 2025-11-22T11:54:04.7761047Z --- start calendar --- 2025-11-22T11:54:04.7761337Z R0lOOg== 2025-11-22T11:54:04.7761588Z --- end calendar --- 2025-11-22T11:54:04.7761882Z --- start calendar --- 2025-11-22T11:54:04.7762179Z RU5kOg== 2025-11-22T11:54:04.7762428Z --- end calendar --- 2025-11-22T11:54:04.7762718Z --- start calendar --- 2025-11-22T11:54:04.7763008Z dztcXA== 2025-11-22T11:54:04.7763252Z --- end calendar --- 2025-11-22T11:54:04.7763553Z --- start calendar --- 2025-11-22T11:54:04.7764055Z RU4xOg== 2025-11-22T11:54:04.7764326Z --- end calendar --- 2025-11-22T11:54:04.7764630Z --- start calendar --- 2025-11-22T11:54:04.7764932Z RU5UOg== 2025-11-22T11:54:04.7765194Z --- end calendar --- 2025-11-22T11:54:04.7765494Z --- start calendar --- 2025-11-22T11:54:04.7765797Z RTFEOg== 2025-11-22T11:54:04.7766047Z --- end calendar --- 2025-11-22T11:54:04.7766353Z --- start calendar --- 2025-11-22T11:54:04.7766646Z RTs7Ow== 2025-11-22T11:54:04.7767078Z --- end calendar --- 2025-11-22T11:54:04.7767539Z --- start calendar --- 2025-11-22T11:54:04.7767843Z 4b+TOg== 2025-11-22T11:54:04.7768099Z --- end calendar --- 2025-11-22T11:54:04.7768405Z --- start calendar --- 2025-11-22T11:54:04.7768700Z TjtPPQ== 2025-11-22T11:54:04.7768959Z --- end calendar --- 2025-11-22T11:54:04.7769263Z --- start calendar --- 2025-11-22T11:54:04.7769558Z OG5oOg== 2025-11-22T11:54:04.7769817Z --- end calendar --- 2025-11-22T11:54:04.7770112Z --- start calendar --- 2025-11-22T11:54:04.7770409Z RUUwOg== 2025-11-22T11:54:04.7770663Z --- end calendar --- 2025-11-22T11:54:04.7770968Z --- start calendar --- 2025-11-22T11:54:04.7771265Z Ojo6Og== 2025-11-22T11:54:04.7771525Z --- end calendar --- 2025-11-22T11:54:04.7771816Z --- start calendar --- 2025-11-22T11:54:04.7772114Z M0VkOg== 2025-11-22T11:54:04.7772363Z --- end calendar --- 2025-11-22T11:54:04.7772660Z --- start calendar --- 2025-11-22T11:54:04.7772957Z RUdGOg== 2025-11-22T11:54:04.7773221Z --- end calendar --- 2025-11-22T11:54:04.7773531Z --- start calendar --- 2025-11-22T11:54:04.7773827Z 4buTOg== 2025-11-22T11:54:04.7774083Z --- end calendar --- 2025-11-22T11:54:04.7774382Z --- start calendar --- 2025-11-22T11:54:04.7774680Z QkVkOg== 2025-11-22T11:54:04.7774919Z --- end calendar --- 2025-11-22T11:54:04.7775221Z --- start calendar --- 2025-11-22T11:54:04.7775513Z IiIiIg== 2025-11-22T11:54:04.7775774Z --- end calendar --- 2025-11-22T11:54:04.7776068Z --- start calendar --- 2025-11-22T11:54:04.7776358Z RVlPOg== 2025-11-22T11:54:04.7776588Z --- end calendar --- 2025-11-22T11:54:04.7776891Z --- start calendar --- 2025-11-22T11:54:04.7777171Z RTAtOg== 2025-11-22T11:54:04.7777589Z --- end calendar --- 2025-11-22T11:54:04.7777894Z --- start calendar --- 2025-11-22T11:54:04.7778198Z NEVkOg== 2025-11-22T11:54:04.7778457Z --- end calendar --- 2025-11-22T11:54:04.7778761Z --- start calendar --- 2025-11-22T11:54:04.7779057Z dztdXA== 2025-11-22T11:54:04.7779308Z --- end calendar --- 2025-11-22T11:54:04.7779617Z --- start calendar --- 2025-11-22T11:54:04.7779922Z RU9POg== 2025-11-22T11:54:04.7780178Z --- end calendar --- 2025-11-22T11:54:04.7780470Z --- start calendar --- 2025-11-22T11:54:04.7780779Z RVFHOg== 2025-11-22T11:54:04.7781026Z --- end calendar --- 2025-11-22T11:54:04.7781319Z --- start calendar --- 2025-11-22T11:54:04.7781602Z UjsiIg== 2025-11-22T11:54:04.7781866Z --- end calendar --- 2025-11-22T11:54:04.7782163Z --- start calendar --- 2025-11-22T11:54:04.7782450Z RTtFOw== 2025-11-22T11:54:04.7782707Z --- end calendar --- 2025-11-22T11:54:04.7782997Z --- start calendar --- 2025-11-22T11:54:04.7783289Z RU5POg== 2025-11-22T11:54:04.7783536Z --- end calendar --- 2025-11-22T11:54:04.7783840Z --- start calendar --- 2025-11-22T11:54:04.7784132Z Mjs7Ig== 2025-11-22T11:54:04.7784391Z --- end calendar --- 2025-11-22T11:54:04.7784694Z --- start calendar --- 2025-11-22T11:54:04.7784993Z RU4wOg== 2025-11-22T11:54:04.7785238Z --- end calendar --- 2025-11-22T11:54:04.7785542Z --- start calendar --- 2025-11-22T11:54:04.7785848Z YTtyPT4= 2025-11-22T11:54:04.7786133Z --- end calendar --- 2025-11-22T11:54:04.7786450Z --- start calendar --- 2025-11-22T11:54:04.7786759Z SDs7Ozs= 2025-11-22T11:54:04.7787030Z --- end calendar --- 2025-11-22T11:54:04.7787505Z --- start calendar --- 2025-11-22T11:54:04.7787829Z WDtbXFw= 2025-11-22T11:54:04.7788077Z --- end calendar --- 2025-11-22T11:54:04.7788383Z --- start calendar --- 2025-11-22T11:54:04.7788673Z WjsyPSw= 2025-11-22T11:54:04.7788924Z --- end calendar --- 2025-11-22T11:54:04.7789220Z --- start calendar --- 2025-11-22T11:54:04.7789510Z MTtCPSI= 2025-11-22T11:54:04.7789939Z --- end calendar --- 2025-11-22T11:54:04.7790257Z --- start calendar --- 2025-11-22T11:54:04.7790547Z STtrPTs= 2025-11-22T11:54:04.7790800Z --- end calendar --- 2025-11-22T11:54:04.7791101Z --- start calendar --- 2025-11-22T11:54:04.7791387Z QgoxCns= 2025-11-22T11:54:04.7791634Z --- end calendar --- 2025-11-22T11:54:04.7791924Z --- start calendar --- 2025-11-22T11:54:04.7792227Z YTtyPQg= 2025-11-22T11:54:04.7792480Z --- end calendar --- 2025-11-22T11:54:04.7793008Z --- start calendar --- 2025-11-22T11:54:04.7793321Z QkVHSUo6 2025-11-22T11:54:04.7793596Z --- end calendar --- 2025-11-22T11:54:04.7793904Z --- start calendar --- 2025-11-22T11:54:04.7794210Z 47my4b+TOg== 2025-11-22T11:54:04.7794481Z --- end calendar --- 2025-11-22T11:54:04.7794792Z --- start calendar --- 2025-11-22T11:54:04.7795086Z QkVHNmQ6 2025-11-22T11:54:04.7795325Z --- end calendar --- 2025-11-22T11:54:04.7795628Z --- start calendar --- 2025-11-22T11:54:04.7795922Z QkVHSTY6 2025-11-22T11:54:04.7796183Z --- end calendar --- 2025-11-22T11:54:04.7796490Z --- start calendar --- 2025-11-22T11:54:04.7796792Z LTtUPV5e 2025-11-22T11:54:04.7797050Z --- end calendar --- 2025-11-22T11:54:04.7797537Z --- start calendar --- 2025-11-22T11:54:04.7797832Z MTs7IiIi 2025-11-22T11:54:04.7798088Z --- end calendar --- 2025-11-22T11:54:04.7798395Z --- start calendar --- 2025-11-22T11:54:04.7798693Z Ri02OC06 2025-11-22T11:54:04.7798946Z --- end calendar --- 2025-11-22T11:54:04.7799257Z --- start calendar --- 2025-11-22T11:54:04.7799567Z MDtDPSws 2025-11-22T11:54:04.7799819Z --- end calendar --- 2025-11-22T11:54:04.7800130Z --- start calendar --- 2025-11-22T11:54:04.7800418Z eDtcXFxc 2025-11-22T11:54:04.7800675Z --- end calendar --- 2025-11-22T11:54:04.7800956Z --- start calendar --- 2025-11-22T11:54:04.7801225Z 5IW74b+TOg== 2025-11-22T11:54:04.7801486Z --- end calendar --- 2025-11-22T11:54:04.7801777Z --- start calendar --- 2025-11-22T11:54:04.7802068Z QkVHaUU6 2025-11-22T11:54:04.7802340Z --- end calendar --- 2025-11-22T11:54:04.7802645Z --- start calendar --- 2025-11-22T11:54:04.7802961Z QmVHSTE6 2025-11-22T11:54:04.7803222Z --- end calendar --- 2025-11-22T11:54:04.7803534Z --- start calendar --- 2025-11-22T11:54:04.7803822Z QkVHek46 2025-11-22T11:54:04.7804064Z --- end calendar --- 2025-11-22T11:54:04.7804369Z --- start calendar --- 2025-11-22T11:54:04.7804672Z QkVHSk46 2025-11-22T11:54:04.7804928Z --- end calendar --- 2025-11-22T11:54:04.7805223Z --- start calendar --- 2025-11-22T11:54:04.7805536Z QkVHSTk6 2025-11-22T11:54:04.7823528Z --- end calendar --- 2025-11-22T11:54:04.7823881Z --- start calendar --- 2025-11-22T11:54:04.7824186Z QkVHSVE6 2025-11-22T11:54:04.7824451Z --- end calendar --- 2025-11-22T11:54:04.7824756Z --- start calendar --- 2025-11-22T11:54:04.7825056Z QkVHQW46 2025-11-22T11:54:04.7825316Z --- end calendar --- 2025-11-22T11:54:04.7825623Z --- start calendar --- 2025-11-22T11:54:04.7825913Z QkRHSTA6 2025-11-22T11:54:04.7826176Z --- end calendar --- 2025-11-22T11:54:04.7826470Z --- start calendar --- 2025-11-22T11:54:04.7826767Z QkVHSWQ6 2025-11-22T11:54:04.7827038Z --- end calendar --- 2025-11-22T11:54:04.7827523Z --- start calendar --- 2025-11-22T11:54:04.7827837Z TztsPTs9 2025-11-22T11:54:04.7828086Z --- end calendar --- 2025-11-22T11:54:04.7828421Z --- start calendar --- 2025-11-22T11:54:04.7828718Z UjsiIiIi 2025-11-22T11:54:04.7828974Z --- end calendar --- 2025-11-22T11:54:04.7829282Z --- start calendar --- 2025-11-22T11:54:04.7829567Z QkVHN046 2025-11-22T11:54:04.7829826Z --- end calendar --- 2025-11-22T11:54:04.7830127Z --- start calendar --- 2025-11-22T11:54:04.7830422Z VDs7Ozs7 2025-11-22T11:54:04.7830669Z --- end calendar --- 2025-11-22T11:54:04.7830970Z --- start calendar --- 2025-11-22T11:54:04.7831258Z 56me4b+TOg== 2025-11-22T11:54:04.7831521Z --- end calendar --- 2025-11-22T11:54:04.7831816Z --- start calendar --- 2025-11-22T11:54:04.7832105Z QkVnSU46 2025-11-22T11:54:04.7832357Z --- end calendar --- 2025-11-22T11:54:04.7832656Z --- start calendar --- 2025-11-22T11:54:04.7832942Z QkVnSU46 2025-11-22T11:54:04.7833368Z --- end calendar --- 2025-11-22T11:54:04.7833672Z --- start calendar --- 2025-11-22T11:54:04.7833958Z QkVHNWQ6 2025-11-22T11:54:04.7834206Z --- end calendar --- 2025-11-22T11:54:04.7834500Z --- start calendar --- 2025-11-22T11:54:04.7834787Z QkVnSU46 2025-11-22T11:54:04.7835035Z --- end calendar --- 2025-11-22T11:54:04.7835336Z --- start calendar --- 2025-11-22T11:54:04.7835617Z QkVHMTE6 2025-11-22T11:54:04.7835875Z --- end calendar --- 2025-11-22T11:54:04.7836162Z --- start calendar --- 2025-11-22T11:54:04.7836615Z QkU4LmQ6 2025-11-22T11:54:04.7836856Z --- end calendar --- 2025-11-22T11:54:04.7837151Z --- start calendar --- 2025-11-22T11:54:04.7837604Z QkVmSU46 2025-11-22T11:54:04.7837850Z --- end calendar --- 2025-11-22T11:54:04.7838147Z --- start calendar --- 2025-11-22T11:54:04.7838447Z RDtHPSwsLA== 2025-11-22T11:54:04.7838720Z --- end calendar --- 2025-11-22T11:54:04.7839012Z --- start calendar --- 2025-11-22T11:54:04.7839312Z MDswPTswPQ== 2025-11-22T11:54:04.7839577Z --- end calendar --- 2025-11-22T11:54:04.7839885Z --- start calendar --- 2025-11-22T11:54:04.7840173Z YztHPWM7ZA== 2025-11-22T11:54:04.7840445Z --- end calendar --- 2025-11-22T11:54:04.7840733Z --- start calendar --- 2025-11-22T11:54:04.7841028Z QgoxCjEKew== 2025-11-22T11:54:04.7841289Z --- end calendar --- 2025-11-22T11:54:04.7841582Z --- start calendar --- 2025-11-22T11:54:04.7841883Z STtCPTswPQ== 2025-11-22T11:54:04.7842142Z --- end calendar --- 2025-11-22T11:54:04.7842438Z --- start calendar --- 2025-11-22T11:54:04.7842738Z eDtcXFxcXFw= 2025-11-22T11:54:04.7843003Z --- end calendar --- 2025-11-22T11:54:04.7843294Z --- start calendar --- 2025-11-22T11:54:04.7843656Z aTtLPTtGPTs= 2025-11-22T11:54:04.7843927Z --- end calendar --- 2025-11-22T11:54:04.7844223Z --- start calendar --- 2025-11-22T11:54:04.7844509Z MDtDPSwsLCw= 2025-11-22T11:54:04.7844780Z --- end calendar --- 2025-11-22T11:54:04.7845066Z --- start calendar --- 2025-11-22T11:54:04.7845366Z QkVHSU46CkI= 2025-11-22T11:54:04.7845628Z --- end calendar --- 2025-11-22T11:54:04.7845925Z --- start calendar --- 2025-11-22T11:54:04.7846229Z MDstPSIiLCI= 2025-11-22T11:54:04.7846487Z --- end calendar --- 2025-11-22T11:54:04.7846788Z --- start calendar --- 2025-11-22T11:54:04.7847077Z Ojo6Ojo6Ojo= 2025-11-22T11:54:04.7847487Z --- end calendar --- 2025-11-22T11:54:04.7847784Z --- start calendar --- 2025-11-22T11:54:04.7848080Z TjtVPV5eXl4= 2025-11-22T11:54:04.7848341Z --- end calendar --- 2025-11-22T11:54:04.7848641Z --- start calendar --- 2025-11-22T11:54:04.7848939Z IiIiIiIiIiI= 2025-11-22T11:54:04.7849212Z --- end calendar --- 2025-11-22T11:54:04.7849503Z --- start calendar --- 2025-11-22T11:54:04.7849804Z QkVHSU46VnM= 2025-11-22T11:54:04.7850079Z --- end calendar --- 2025-11-22T11:54:04.7850374Z --- start calendar --- 2025-11-22T11:54:04.7850680Z QkVHSU46VnM= 2025-11-22T11:54:04.7850942Z --- end calendar --- 2025-11-22T11:54:04.7851239Z --- start calendar --- 2025-11-22T11:54:04.7851530Z QkVHSW46Cjo= 2025-11-22T11:54:04.7851799Z --- end calendar --- 2025-11-22T11:54:04.7852086Z --- start calendar --- 2025-11-22T11:54:04.7852388Z QkVHaU46Ck46 2025-11-22T11:54:04.7852650Z --- end calendar --- 2025-11-22T11:54:04.7852960Z --- start calendar --- 2025-11-22T11:54:04.7853253Z WjtBPSw7MT0s 2025-11-22T11:54:04.7853522Z --- end calendar --- 2025-11-22T11:54:04.7853823Z --- start calendar --- 2025-11-22T11:54:04.7854112Z YkVHaU46Cjo6 2025-11-22T11:54:04.7854383Z --- end calendar --- 2025-11-22T11:54:04.7854671Z --- start calendar --- 2025-11-22T11:54:04.7854970Z TjtOPU87Tj04 2025-11-22T11:54:04.7855245Z --- end calendar --- 2025-11-22T11:54:04.7855541Z --- start calendar --- 2025-11-22T11:54:04.7855836Z MTtEPSwsLCws 2025-11-22T11:54:04.7856106Z --- end calendar --- 2025-11-22T11:54:04.7856393Z --- start calendar --- 2025-11-22T11:54:04.7856688Z NTttPTs2PTs9 2025-11-22T11:54:04.7856952Z --- end calendar --- 2025-11-22T11:54:04.7857248Z --- start calendar --- 2025-11-22T11:54:04.7857688Z WC1DT00yTUVYOg== 2025-11-22T11:54:04.7857985Z --- end calendar --- 2025-11-22T11:54:04.7858281Z --- start calendar --- 2025-11-22T11:54:04.7858714Z WC1DT01NTlRHOg== 2025-11-22T11:54:04.7859002Z --- end calendar --- 2025-11-22T11:54:04.7859293Z --- start calendar --- 2025-11-22T11:54:04.7859595Z WC1DT010RU5UOg== 2025-11-22T11:54:04.7859872Z --- end calendar --- 2025-11-22T11:54:04.7860165Z --- start calendar --- 2025-11-22T11:54:04.7860461Z WC1DT01NRTRYOg== 2025-11-22T11:54:04.7860740Z --- end calendar --- 2025-11-22T11:54:04.7861030Z --- start calendar --- 2025-11-22T11:54:04.7861330Z WC1DT21NRXdMOg== 2025-11-22T11:54:04.7861754Z --- end calendar --- 2025-11-22T11:54:04.7862054Z --- start calendar --- 2025-11-22T11:54:04.7862354Z WC1DT01zSFVDOg== 2025-11-22T11:54:04.7862625Z --- end calendar --- 2025-11-22T11:54:04.7862922Z --- start calendar --- 2025-11-22T11:54:04.7863213Z WC1DT01NRU4zOg== 2025-11-22T11:54:04.7863493Z --- end calendar --- 2025-11-22T11:54:04.7863787Z --- start calendar --- 2025-11-22T11:54:04.7864093Z WC1DT01NVE1VOg== 2025-11-22T11:54:04.7864375Z --- end calendar --- 2025-11-22T11:54:04.7864680Z --- start calendar --- 2025-11-22T11:54:04.7864984Z WC1DT01NNFVDOg== 2025-11-22T11:54:04.7865263Z --- end calendar --- 2025-11-22T11:54:04.7865559Z --- start calendar --- 2025-11-22T11:54:04.7865861Z WC1DT00xTUVYOg== 2025-11-22T11:54:04.7866145Z --- end calendar --- 2025-11-22T11:54:04.7866437Z --- start calendar --- 2025-11-22T11:54:04.7866741Z WC1DTzNPQU5TOg== 2025-11-22T11:54:04.7867021Z --- end calendar --- 2025-11-22T11:54:04.7867317Z --- start calendar --- 2025-11-22T11:54:04.7867776Z WC1DT00zTUVYOg== 2025-11-22T11:54:04.7868063Z --- end calendar --- 2025-11-22T11:54:04.7868354Z --- start calendar --- 2025-11-22T11:54:04.7868666Z LTtUPV5eXl5eXg== 2025-11-22T11:54:04.7868945Z --- end calendar --- 2025-11-22T11:54:04.7869246Z --- start calendar --- 2025-11-22T11:54:04.7869545Z YztsPTs2PTtPPQ== 2025-11-22T11:54:04.7869831Z --- end calendar --- 2025-11-22T11:54:04.7870121Z --- start calendar --- 2025-11-22T11:54:04.7870428Z WC1DT01NRU4xOg== 2025-11-22T11:54:04.7870707Z --- end calendar --- 2025-11-22T11:54:04.7870998Z --- start calendar --- 2025-11-22T11:54:04.7871307Z WC1DT01NVU1VOg== 2025-11-22T11:54:04.7871579Z --- end calendar --- 2025-11-22T11:54:04.7871874Z --- start calendar --- 2025-11-22T11:54:04.7872172Z WC04QVBDUE5POg== 2025-11-22T11:54:04.7872455Z --- end calendar --- 2025-11-22T11:54:04.7872742Z --- start calendar --- 2025-11-22T11:54:04.7873045Z WC0wQVBDUE5POg== 2025-11-22T11:54:04.7873322Z --- end calendar --- 2025-11-22T11:54:04.7873623Z --- start calendar --- 2025-11-22T11:54:04.7873928Z WC1DT01NRVNVOg== 2025-11-22T11:54:04.7874205Z --- end calendar --- 2025-11-22T11:54:04.7874508Z --- start calendar --- 2025-11-22T11:54:04.7874802Z WC1DT01NRTVYOg== 2025-11-22T11:54:04.7875086Z --- end calendar --- 2025-11-22T11:54:04.7875377Z --- start calendar --- 2025-11-22T11:54:04.7875679Z WFJBMzhFR0lOOg== 2025-11-22T11:54:04.7875958Z --- end calendar --- 2025-11-22T11:54:04.7876252Z --- start calendar --- 2025-11-22T11:54:04.7876547Z ODs7Ozs7Ozs7Ow== 2025-11-22T11:54:04.7876824Z --- end calendar --- 2025-11-22T11:54:04.7877120Z --- start calendar --- 2025-11-22T11:54:04.7877557Z WC1DT01NRU5UOg== 2025-11-22T11:54:04.7877831Z --- end calendar --- 2025-11-22T11:54:04.7878131Z --- start calendar --- 2025-11-22T11:54:04.7878431Z WC1DT01NRU5aOg== 2025-11-22T11:54:04.7878703Z --- end calendar --- 2025-11-22T11:54:04.7879003Z --- start calendar --- 2025-11-22T11:54:04.7879294Z WC1DT01hRTJYOg== 2025-11-22T11:54:04.7879571Z --- end calendar --- 2025-11-22T11:54:04.7879863Z --- start calendar --- 2025-11-22T11:54:04.7880172Z WC1DT21NRUdWOg== 2025-11-22T11:54:04.7880450Z --- end calendar --- 2025-11-22T11:54:04.7880740Z --- start calendar --- 2025-11-22T11:54:04.7881035Z WC1DT01NRU5VOg== 2025-11-22T11:54:04.7881311Z --- end calendar --- 2025-11-22T11:54:04.7881603Z --- start calendar --- 2025-11-22T11:54:04.7881902Z WC1DMEVNT01TOg== 2025-11-22T11:54:04.7882176Z --- end calendar --- 2025-11-22T11:54:04.7882474Z --- start calendar --- 2025-11-22T11:54:04.7882772Z YkVHSW47RT0KXw== 2025-11-22T11:54:04.7883045Z --- end calendar --- 2025-11-22T11:54:04.7883489Z --- start calendar --- 2025-11-22T11:54:04.7883794Z WC1DUE1NMFVROg== 2025-11-22T11:54:04.7884079Z --- end calendar --- 2025-11-22T11:54:04.7884372Z --- start calendar --- 2025-11-22T11:54:04.7884670Z WC1DT01NRU40Og== 2025-11-22T11:54:04.7884946Z --- end calendar --- 2025-11-22T11:54:04.7885240Z --- start calendar --- 2025-11-22T11:54:04.7885538Z WC1DTzJPQU5TOg== 2025-11-22T11:54:04.7885822Z --- end calendar --- 2025-11-22T11:54:04.7886114Z --- start calendar --- 2025-11-22T11:54:04.7886567Z UjsiIiIiIiIiIg== 2025-11-22T11:54:04.7886850Z --- end calendar --- 2025-11-22T11:54:04.7887147Z --- start calendar --- 2025-11-22T11:54:04.7887598Z WFNFcDJNcDAwOg== 2025-11-22T11:54:04.7887885Z --- end calendar --- 2025-11-22T11:54:04.7888182Z --- start calendar --- 2025-11-22T11:54:04.7888481Z WC1DT21NRU9WOg== 2025-11-22T11:54:04.7888757Z --- end calendar --- 2025-11-22T11:54:04.7889052Z --- start calendar --- 2025-11-22T11:54:04.7889355Z WC1DT01NMFVROg== 2025-11-22T11:54:04.7889630Z --- end calendar --- 2025-11-22T11:54:04.7889934Z --- start calendar --- 2025-11-22T11:54:04.7890236Z WC1DT01NMlVDOg== 2025-11-22T11:54:04.7890508Z --- end calendar --- 2025-11-22T11:54:04.7890804Z --- start calendar --- 2025-11-22T11:54:04.7891098Z WC1DT01NRTFUOg== 2025-11-22T11:54:04.7891377Z --- end calendar --- 2025-11-22T11:54:04.7891668Z --- start calendar --- 2025-11-22T11:54:04.7891963Z WC1DT01FTXdMOg== 2025-11-22T11:54:04.7892237Z --- end calendar --- 2025-11-22T11:54:04.7892537Z --- start calendar --- 2025-11-22T11:54:04.7892839Z WC1DMTJNN0VNOg== 2025-11-22T11:54:04.7893121Z --- end calendar --- 2025-11-22T11:54:04.7893410Z --- start calendar --- 2025-11-22T11:54:04.7893714Z WC1DTzFPQU5TOg== 2025-11-22T11:54:04.7893988Z --- end calendar --- 2025-11-22T11:54:04.7894286Z --- start calendar --- 2025-11-22T11:54:04.7894586Z QkVHSU46CkdFTzo= 2025-11-22T11:54:04.7894864Z --- end calendar --- 2025-11-22T11:54:04.7895159Z --- start calendar --- 2025-11-22T11:54:04.7895453Z QkVHSU46CkdFTzo= 2025-11-22T11:54:04.7895739Z --- end calendar --- 2025-11-22T11:54:04.7896036Z --- start calendar --- 2025-11-22T11:54:04.7896341Z WDt0PSIiLCIiLCI= 2025-11-22T11:54:04.7896617Z --- end calendar --- 2025-11-22T11:54:04.7896916Z --- start calendar --- 2025-11-22T11:54:04.7897213Z XFxcXHhcXFxcXFw= 2025-11-22T11:54:04.7897634Z --- end calendar --- 2025-11-22T11:54:04.7897927Z --- start calendar --- 2025-11-22T11:54:04.7898236Z RTtHPSwsLCwsLCw= 2025-11-22T11:54:04.7898520Z --- end calendar --- 2025-11-22T11:54:04.7898822Z --- start calendar --- 2025-11-22T11:54:04.7899124Z NTtrPTt5PTttPTs= 2025-11-22T11:54:04.7899408Z --- end calendar --- 2025-11-22T11:54:04.7899708Z --- start calendar --- 2025-11-22T11:54:04.7900000Z QkVHaU46Ck46Ck4= 2025-11-22T11:54:04.7900280Z --- end calendar --- 2025-11-22T11:54:04.7900571Z --- start calendar --- 2025-11-22T11:54:04.7900875Z QkVHSW46CkVOZDo= 2025-11-22T11:54:04.7901149Z --- end calendar --- 2025-11-22T11:54:04.7901446Z --- start calendar --- 2025-11-22T11:54:04.7901736Z QkVnSU46CkVuZDo= 2025-11-22T11:54:04.7902026Z --- end calendar --- 2025-11-22T11:54:04.7902322Z --- start calendar --- 2025-11-22T11:54:04.7902616Z QkVHSU46CkRJUjo= 2025-11-22T11:54:04.7902898Z --- end calendar --- 2025-11-22T11:54:04.7903183Z --- start calendar --- 2025-11-22T11:54:04.7903479Z QkVHSU46CkVuZDo= 2025-11-22T11:54:04.7903749Z --- end calendar --- 2025-11-22T11:54:04.7904043Z --- start calendar --- 2025-11-22T11:54:04.7904334Z QmVHSU46CkRVRTo= 2025-11-22T11:54:04.7904610Z --- end calendar --- 2025-11-22T11:54:04.7904905Z --- start calendar --- 2025-11-22T11:54:04.7905200Z QkVHaU46Ck46Ck46 2025-11-22T11:54:04.7905474Z --- end calendar --- 2025-11-22T11:54:04.7905765Z --- start calendar --- 2025-11-22T11:54:04.7906059Z QkVHSU46CkRVRTox 2025-11-22T11:54:04.7906335Z --- end calendar --- 2025-11-22T11:54:04.7906673Z --- start calendar --- 2025-11-22T11:54:04.7906966Z XztUPV5eXl5eXl5e 2025-11-22T11:54:04.7907246Z --- end calendar --- 2025-11-22T11:54:04.7907670Z --- start calendar --- 2025-11-22T11:54:04.7907970Z QkVHaU46Ck86Ck46 2025-11-22T11:54:04.7908386Z --- end calendar --- 2025-11-22T11:54:04.7908680Z --- start calendar --- 2025-11-22T11:54:04.7908976Z QkVHSU46CkRVRTpQ 2025-11-22T11:54:04.7909258Z --- end calendar --- 2025-11-22T11:54:04.7909553Z --- start calendar --- 2025-11-22T11:54:04.7909852Z QkVHSW46UwpFTmQ6 2025-11-22T11:54:04.7910126Z --- end calendar --- 2025-11-22T11:54:04.7910426Z --- start calendar --- 2025-11-22T11:54:04.7910725Z QkVHSW46UwpFTmQ6 2025-11-22T11:54:04.7911005Z --- end calendar --- 2025-11-22T11:54:04.7911493Z --- start calendar --- 2025-11-22T11:54:04.7911786Z YkVHSU46CkRVRTpw 2025-11-22T11:54:04.7912069Z --- end calendar --- 2025-11-22T11:54:04.7912358Z --- start calendar --- 2025-11-22T11:54:04.7912658Z YkVnSU46CkRVRTov 2025-11-22T11:54:04.7912932Z --- end calendar --- 2025-11-22T11:54:04.7913232Z --- start calendar --- 2025-11-22T11:54:04.7913523Z QkVHaU46ClJzVlA6 2025-11-22T11:54:04.7913802Z --- end calendar --- 2025-11-22T11:54:04.7914100Z --- start calendar --- 2025-11-22T11:54:04.7914412Z --- end calendar --- 2025-11-22T11:54:04.7914717Z --- start calendar --- 2025-11-22T11:54:04.7915014Z QmVHSU46Ck46Cjo6 2025-11-22T11:54:04.7915298Z --- end calendar --- 2025-11-22T11:54:04.7915590Z --- start calendar --- 2025-11-22T11:54:04.7915899Z QkVHSU46ClJSVUxFOg== 2025-11-22T11:54:04.7916190Z --- end calendar --- 2025-11-22T11:54:04.7916488Z --- start calendar --- 2025-11-22T11:54:04.7916789Z QkVHSU46Ci0tUE5kOg== 2025-11-22T11:54:04.7917082Z --- end calendar --- 2025-11-22T11:54:04.7917524Z --- start calendar --- 2025-11-22T11:54:04.7917835Z eTtcXFw7XFxcXEJcXA== 2025-11-22T11:54:04.7918123Z --- end calendar --- 2025-11-22T11:54:04.7918421Z --- start calendar --- 2025-11-22T11:54:04.7918733Z QmVHSW46CkJFR0lOOg== 2025-11-22T11:54:04.7919019Z --- end calendar --- 2025-11-22T11:54:04.7919313Z --- start calendar --- 2025-11-22T11:54:04.7919612Z QkVHSU46CkRVRTotUA== 2025-11-22T11:54:04.7919909Z --- end calendar --- 2025-11-22T11:54:04.7920199Z --- start calendar --- 2025-11-22T11:54:04.7920502Z QkVHaU46ClJEQVRFOg== 2025-11-22T11:54:04.7920793Z --- end calendar --- 2025-11-22T11:54:04.7921084Z --- start calendar --- 2025-11-22T11:54:04.7921385Z TjtVPU47VT1PO1U9Xg== 2025-11-22T11:54:04.7921684Z --- end calendar --- 2025-11-22T11:54:04.7921973Z --- start calendar --- 2025-11-22T11:54:04.7922286Z NTtrPTttPTs2PTtPPQ== 2025-11-22T11:54:04.7922584Z --- end calendar --- 2025-11-22T11:54:04.7922877Z --- start calendar --- 2025-11-22T11:54:04.7923191Z WjtBPSw7MT0sOzI9LA== 2025-11-22T11:54:04.7923483Z --- end calendar --- 2025-11-22T11:54:04.7923788Z --- start calendar --- 2025-11-22T11:54:04.7924094Z eTtcXFw7XFxcO0xcXEI= 2025-11-22T11:54:04.7924389Z --- end calendar --- 2025-11-22T11:54:04.7924678Z --- start calendar --- 2025-11-22T11:54:04.7924983Z QkVHaU46Ck46Ck46Ck8= 2025-11-22T11:54:04.7925274Z --- end calendar --- 2025-11-22T11:54:04.7925579Z --- start calendar --- 2025-11-22T11:54:04.7925880Z QkVHSU46ClJyVUxFOj0= 2025-11-22T11:54:04.7926172Z --- end calendar --- 2025-11-22T11:54:04.7926462Z --- start calendar --- 2025-11-22T11:54:04.7926772Z YkVHaU46ClA6CkVOZDo= 2025-11-22T11:54:04.7927065Z --- end calendar --- 2025-11-22T11:54:04.7927491Z --- start calendar --- 2025-11-22T11:54:04.7927807Z QkVHSU46CkdFTzowOzA= 2025-11-22T11:54:04.7928101Z --- end calendar --- 2025-11-22T11:54:04.7928397Z --- start calendar --- 2025-11-22T11:54:04.7928696Z QkVHaU46ClJEQVRFOlA= 2025-11-22T11:54:04.7928990Z --- end calendar --- 2025-11-22T11:54:04.7929279Z --- start calendar --- 2025-11-22T11:54:04.7929585Z QkVHSU46ClJSVUxFOjs= 2025-11-22T11:54:04.7929882Z --- end calendar --- 2025-11-22T11:54:04.7930177Z --- start calendar --- 2025-11-22T11:54:04.7930496Z 4oii4oii4oii4oii4oii6oSi4oii 2025-11-22T11:54:04.7930845Z --- end calendar --- 2025-11-22T11:54:04.7931145Z --- start calendar --- 2025-11-22T11:54:04.7931443Z QkVHSU46ClJlcEVBVDo= 2025-11-22T11:54:04.7931735Z --- end calendar --- 2025-11-22T11:54:04.7932026Z --- start calendar --- 2025-11-22T11:54:04.7932328Z QkVHSU46VkVWRU5UCgA= 2025-11-22T11:54:04.7932617Z --- end calendar --- 2025-11-22T11:54:04.7933053Z --- start calendar --- 2025-11-22T11:54:04.7933360Z MDstPSIiLCIiLCIiLCI= 2025-11-22T11:54:04.7933658Z --- end calendar --- 2025-11-22T11:54:04.7933948Z --- start calendar --- 2025-11-22T11:54:04.7934253Z YkVHSU47ZT0KTTtlPQpf 2025-11-22T11:54:04.7934547Z --- end calendar --- 2025-11-22T11:54:04.7934842Z --- start calendar --- 2025-11-22T11:54:04.7935150Z VAo9Cj8KPQo9Cj8KPQo/ 2025-11-22T11:54:04.7935445Z --- end calendar --- 2025-11-22T11:54:04.7935743Z --- start calendar --- 2025-11-22T11:54:04.7936191Z QkVHSU46ClJSVUxFOjs7 2025-11-22T11:54:04.7936485Z --- end calendar --- 2025-11-22T11:54:04.7936773Z --- start calendar --- 2025-11-22T11:54:04.7937087Z QkVHSU46ClJlUEVBVDox 2025-11-22T11:54:04.7937513Z --- end calendar --- 2025-11-22T11:54:04.7937815Z --- start calendar --- 2025-11-22T11:54:04.7938115Z QkVHSU46ClJEQVRFOlAs 2025-11-22T11:54:04.7938412Z --- end calendar --- 2025-11-22T11:54:04.7938702Z --- start calendar --- 2025-11-22T11:54:04.7939007Z QkVHSU46ClNFblQtQlk6 2025-11-22T11:54:04.7939302Z --- end calendar --- 2025-11-22T11:54:04.7939605Z --- start calendar --- 2025-11-22T11:54:04.7939906Z QkVHaU46Ck46Ck46Ck46 2025-11-22T11:54:04.7940191Z --- end calendar --- 2025-11-22T11:54:04.7940490Z --- start calendar --- 2025-11-22T11:54:04.7940793Z YztGPWI7Rz1jO2Y9Yztk 2025-11-22T11:54:04.7941092Z --- end calendar --- 2025-11-22T11:54:04.7941381Z --- start calendar --- 2025-11-22T11:54:04.7941703Z RTtHPSwsLCwsLCwsLCwsLA== 2025-11-22T11:54:04.7942028Z --- end calendar --- 2025-11-22T11:54:04.7942339Z --- start calendar --- 2025-11-22T11:54:04.7942657Z QkVHSU46ClJSVUxFOjs7Ow== 2025-11-22T11:54:04.7942992Z --- end calendar --- 2025-11-22T11:54:04.7943419Z --- start calendar --- 2025-11-22T11:54:04.7943738Z QkVHSU46CkZSRUVCVVNZOg== 2025-11-22T11:54:04.7944070Z --- end calendar --- 2025-11-22T11:54:04.7944367Z --- start calendar --- 2025-11-22T11:54:04.7944683Z QkVHSU46CkYtRURuVVNZOg== 2025-11-22T11:54:04.7945040Z --- end calendar --- 2025-11-22T11:54:04.7945339Z --- start calendar --- 2025-11-22T11:54:04.7945658Z QkVHSU46VkVWRU5UCjEKeQ== 2025-11-22T11:54:04.7945982Z --- end calendar --- 2025-11-22T11:54:04.7946274Z --- start calendar --- 2025-11-22T11:54:04.7946590Z Ojo6Ojo6Ojo6Ojo6Ojo6Og== 2025-11-22T11:54:04.7946903Z --- end calendar --- 2025-11-22T11:54:04.7947199Z --- start calendar --- 2025-11-22T11:54:04.7947664Z QkVHSU46ClJSVUxFOj07PQ== 2025-11-22T11:54:04.7948005Z --- end calendar --- 2025-11-22T11:54:04.7948312Z --- start calendar --- 2025-11-22T11:54:04.7948630Z QkVnSU46Cjk2MUVWRU5kOg== 2025-11-22T11:54:04.7948962Z --- end calendar --- 2025-11-22T11:54:04.7949266Z --- start calendar --- 2025-11-22T11:54:04.7949648Z QkVHaU46VkVWRU5UCj8KFw== 2025-11-22T11:54:04.7950003Z --- end calendar --- 2025-11-22T11:54:04.7950301Z --- start calendar --- 2025-11-22T11:54:04.7950620Z QkVHSU46VkVWZU5UCjsKOw== 2025-11-22T11:54:04.7950934Z --- end calendar --- 2025-11-22T11:54:04.7951236Z --- start calendar --- 2025-11-22T11:54:04.7951551Z IiIiIiIiIiIiIiIiIiIiIg== 2025-11-22T11:54:04.7951876Z --- end calendar --- 2025-11-22T11:54:04.7952175Z --- start calendar --- 2025-11-22T11:54:04.7952491Z QkVnSU46ClJEQXRlOlAsLw== 2025-11-22T11:54:04.7952812Z --- end calendar --- 2025-11-22T11:54:04.7953116Z --- start calendar --- 2025-11-22T11:54:04.7953427Z QkVHSW46CkZSekVCVVNZOg== 2025-11-22T11:54:04.7953754Z --- end calendar --- 2025-11-22T11:54:04.7954051Z --- start calendar --- 2025-11-22T11:54:04.7954367Z QkVHSW46CkRJcjoKRU5kOg== 2025-11-22T11:54:04.7954687Z --- end calendar --- 2025-11-22T11:54:04.7954994Z --- start calendar --- 2025-11-22T11:54:04.7955306Z QkVHSW46CnJydUxFOj0sLA== 2025-11-22T11:54:04.7955622Z --- end calendar --- 2025-11-22T11:54:04.7955920Z --- start calendar --- 2025-11-22T11:54:04.7956233Z QkVHSU46Cno7ej0KRU5kOg== 2025-11-22T11:54:04.7956551Z --- end calendar --- 2025-11-22T11:54:04.7956846Z --- start calendar --- 2025-11-22T11:54:04.7957163Z QkVHSU46CkRJUjoKRElSOg== 2025-11-22T11:54:04.7957673Z --- end calendar --- 2025-11-22T11:54:04.7957978Z --- start calendar --- 2025-11-22T11:54:04.7958291Z QkVHSU46CkZSRUVCVS0tOg== 2025-11-22T11:54:04.7958761Z --- end calendar --- 2025-11-22T11:54:04.7959065Z --- start calendar --- 2025-11-22T11:54:04.7959377Z QkVHSU46ClJEQVRFOlAsUA== 2025-11-22T11:54:04.7959700Z --- end calendar --- 2025-11-22T11:54:04.7959989Z --- start calendar --- 2025-11-22T11:54:04.7960307Z QkVHSU46CkVOZDoKRU5kOg== 2025-11-22T11:54:04.7960626Z --- end calendar --- 2025-11-22T11:54:04.7960931Z --- start calendar --- 2025-11-22T11:54:04.7961241Z QkVHSU46CkRVRTowABIWJg== 2025-11-22T11:54:04.7961711Z --- end calendar --- 2025-11-22T11:54:04.7962006Z --- start calendar --- 2025-11-22T11:54:04.7962328Z QkVHSU46VkVWLVJOCkVORDo= 2025-11-22T11:54:04.7962654Z --- end calendar --- 2025-11-22T11:54:04.7962940Z --- start calendar --- 2025-11-22T11:54:04.7963262Z QkVHSU46VkVWLVJOCkVORDo= 2025-11-22T11:54:04.7963575Z --- end calendar --- 2025-11-22T11:54:04.7963881Z --- start calendar --- 2025-11-22T11:54:04.7964191Z YkVHSU46VkVWRU44CkVORDo= 2025-11-22T11:54:04.7964515Z --- end calendar --- 2025-11-22T11:54:04.7964823Z --- start calendar --- 2025-11-22T11:54:04.7965137Z YkVHSU46VkVWRU44CkVORDo= 2025-11-22T11:54:04.7965451Z --- end calendar --- 2025-11-22T11:54:04.7965745Z --- start calendar --- 2025-11-22T11:54:04.7966054Z QkVHaU46Ck46Ck46Ck46Ck8= 2025-11-22T11:54:04.7966367Z --- end calendar --- 2025-11-22T11:54:04.7966665Z --- start calendar --- 2025-11-22T11:54:04.7966969Z QkVHSW46OmUAAE46CkVOZDo= 2025-11-22T11:54:04.7967293Z --- end calendar --- 2025-11-22T11:54:04.7967724Z --- start calendar --- 2025-11-22T11:54:04.7968048Z QkVHSW46OmUAAE46CkVOZDo= 2025-11-22T11:54:04.7968361Z --- end calendar --- 2025-11-22T11:54:04.7968658Z --- start calendar --- 2025-11-22T11:54:04.7968966Z YkVHSU46VkVHRU5rCkVORDo= 2025-11-22T11:54:04.7969287Z --- end calendar --- 2025-11-22T11:54:04.7969578Z --- start calendar --- 2025-11-22T11:54:04.7969894Z YkVHSU46VkVHRU5rCkVORDo= 2025-11-22T11:54:04.7970210Z --- end calendar --- 2025-11-22T11:54:04.7970506Z --- start calendar --- 2025-11-22T11:54:04.7970827Z RTtHPSwsLCwsLCwsLCwsLCw= 2025-11-22T11:54:04.7971151Z --- end calendar --- 2025-11-22T11:54:04.7971450Z --- start calendar --- 2025-11-22T11:54:04.7971759Z QkVHSU46CkRVRTowKgASFiY= 2025-11-22T11:54:04.7972081Z --- end calendar --- 2025-11-22T11:54:04.7972370Z --- start calendar --- 2025-11-22T11:54:04.7972685Z QkVHSW46VkVWRk5UCkVORDo= 2025-11-22T11:54:04.7972996Z --- end calendar --- 2025-11-22T11:54:04.7973295Z --- start calendar --- 2025-11-22T11:54:04.7973601Z QkVHSW46VkVWRk5UCkVORDo= 2025-11-22T11:54:04.7973933Z --- end calendar --- 2025-11-22T11:54:04.7974224Z --- start calendar --- 2025-11-22T11:54:04.7974549Z YkVHSU46VkVWRU4rCkVORDo= 2025-11-22T11:54:04.7974868Z --- end calendar --- 2025-11-22T11:54:04.7975159Z --- start calendar --- 2025-11-22T11:54:04.7975479Z YkVHSU46VkVWRU4rCkVORDo= 2025-11-22T11:54:04.7975797Z --- end calendar --- 2025-11-22T11:54:04.7976092Z --- start calendar --- 2025-11-22T11:54:04.7976402Z QkVHSU46ClU7Qj0sCkVOZDo= 2025-11-22T11:54:04.7976722Z --- end calendar --- 2025-11-22T11:54:04.7977017Z --- start calendar --- 2025-11-22T11:54:04.7977471Z QkVHSW46VmUxK0VOCkVORDo= 2025-11-22T11:54:04.7977793Z --- end calendar --- 2025-11-22T11:54:04.7978095Z --- start calendar --- 2025-11-22T11:54:04.7978404Z QkVHSW46VmUxK0VOCkVORDo= 2025-11-22T11:54:04.7978725Z --- end calendar --- 2025-11-22T11:54:04.7979025Z --- start calendar --- 2025-11-22T11:54:04.7979335Z YkVHSU46VkUpRXIrCkVORDo= 2025-11-22T11:54:04.7979660Z --- end calendar --- 2025-11-22T11:54:04.7979953Z --- start calendar --- 2025-11-22T11:54:04.7980283Z YkVHSU46VkUpRXIrCkVORDo= 2025-11-22T11:54:04.7980599Z --- end calendar --- 2025-11-22T11:54:04.7980894Z --- start calendar --- 2025-11-22T11:54:04.7981205Z TjtVPU47VT1OO1U9ZDtVPV4= 2025-11-22T11:54:04.7981523Z --- end calendar --- 2025-11-22T11:54:04.7981809Z --- start calendar --- 2025-11-22T11:54:04.7982124Z QkVHSU46VkVXLVJOCkVORDo= 2025-11-22T11:54:04.7982437Z --- end calendar --- 2025-11-22T11:54:04.7982740Z --- start calendar --- 2025-11-22T11:54:04.7983057Z QkVHSU46VkVXLVJOCkVORDo= 2025-11-22T11:54:04.7983369Z --- end calendar --- 2025-11-22T11:54:04.7983812Z --- start calendar --- 2025-11-22T11:54:04.7984124Z QkVHSW46VmVWRX5OCkVORDo= 2025-11-22T11:54:04.7984449Z --- end calendar --- 2025-11-22T11:54:04.7984739Z --- start calendar --- 2025-11-22T11:54:04.7985057Z QkVHSW46VmVWRX5OCkVORDo= 2025-11-22T11:54:04.7985371Z --- end calendar --- 2025-11-22T11:54:04.7985670Z --- start calendar --- 2025-11-22T11:54:04.7985977Z QkVHSU46VkVWRU4UCkVORDo= 2025-11-22T11:54:04.7986295Z --- end calendar --- 2025-11-22T11:54:04.7986735Z --- start calendar --- 2025-11-22T11:54:04.7987054Z QkVHSU46VkVWRU4UCkVORDo= 2025-11-22T11:54:04.7987507Z --- end calendar --- 2025-11-22T11:54:04.7987808Z --- start calendar --- 2025-11-22T11:54:04.7988121Z YkVHSU46QEVWRU44CkVORDo= 2025-11-22T11:54:04.7988437Z --- end calendar --- 2025-11-22T11:54:04.7988731Z --- start calendar --- 2025-11-22T11:54:04.7989038Z YkVHSU46QEVWRU44CkVORDo= 2025-11-22T11:54:04.7989364Z --- end calendar --- 2025-11-22T11:54:04.7989651Z --- start calendar --- 2025-11-22T11:54:04.7989974Z QkVHSU46VkVWOk5UCkVOZDo= 2025-11-22T11:54:04.7990293Z --- end calendar --- 2025-11-22T11:54:04.7990589Z --- start calendar --- 2025-11-22T11:54:04.7990899Z QkVHSU46VkVWOk5UCkVOZDo= 2025-11-22T11:54:04.7991222Z --- end calendar --- 2025-11-22T11:54:04.7991517Z --- start calendar --- 2025-11-22T11:54:04.7991828Z YkVHaU46Ci46Clg6CkVOZDo= 2025-11-22T11:54:04.7992145Z --- end calendar --- 2025-11-22T11:54:04.7992435Z --- start calendar --- 2025-11-22T11:54:04.7992750Z YkVHSU46KSU6RTpOCkVORDo= 2025-11-22T11:54:04.7993078Z --- end calendar --- 2025-11-22T11:54:04.7993374Z --- start calendar --- 2025-11-22T11:54:04.7993683Z YkVHSU46KSU6RTpOCkVORDo= 2025-11-22T11:54:04.7993998Z --- end calendar --- 2025-11-22T11:54:04.7994287Z --- start calendar --- 2025-11-22T11:54:04.7994608Z YkVHSU46TkVFVlYrCkVORDo= 2025-11-22T11:54:04.7994924Z --- end calendar --- 2025-11-22T11:54:04.7995224Z --- start calendar --- 2025-11-22T11:54:04.7995537Z YkVHSU46TkVFVlYrCkVORDo= 2025-11-22T11:54:04.7995861Z --- end calendar --- 2025-11-22T11:54:04.7996164Z --- start calendar --- 2025-11-22T11:54:04.7996473Z QkVHSW46CnJydUxFOj0sLCw= 2025-11-22T11:54:04.7996795Z --- end calendar --- 2025-11-22T11:54:04.7997085Z --- start calendar --- 2025-11-22T11:54:04.7997546Z YkVHSU46AQBFVnIrCkVORDo= 2025-11-22T11:54:04.7997869Z --- end calendar --- 2025-11-22T11:54:04.7998171Z --- start calendar --- 2025-11-22T11:54:04.7998480Z YkVHSU46AQBFVnIrCkVORDo= 2025-11-22T11:54:04.7998803Z --- end calendar --- 2025-11-22T11:54:04.7999099Z --- start calendar --- 2025-11-22T11:54:04.7999411Z YkVHSU46VkVWRU5FCkVORDo= 2025-11-22T11:54:04.7999731Z --- end calendar --- 2025-11-22T11:54:04.8000022Z --- start calendar --- 2025-11-22T11:54:04.8000337Z YkVHSU46VkVWRU5FCkVORDo= 2025-11-22T11:54:04.8000650Z --- end calendar --- 2025-11-22T11:54:04.8000947Z --- start calendar --- 2025-11-22T11:54:04.8001261Z YkVHSU46VkBWRU4qCkVORDo= 2025-11-22T11:54:04.8001581Z --- end calendar --- 2025-11-22T11:54:04.8001871Z --- start calendar --- 2025-11-22T11:54:04.8002189Z QkVHSW46VkVWRUFUCkVORDo= 2025-11-22T11:54:04.8002508Z --- end calendar --- 2025-11-22T11:54:04.8002811Z --- start calendar --- 2025-11-22T11:54:04.8003122Z QkVHSW46VkVWRUFUCkVORDo= 2025-11-22T11:54:04.8003444Z --- end calendar --- 2025-11-22T11:54:04.8003737Z --- start calendar --- 2025-11-22T11:54:04.8004051Z YkVHSU46VkVWBU4rCkVORDo= 2025-11-22T11:54:04.8004373Z --- end calendar --- 2025-11-22T11:54:04.8004660Z --- start calendar --- 2025-11-22T11:54:04.8004975Z YkVHSU46VkVWBU4rCkVORDo= 2025-11-22T11:54:04.8005315Z --- end calendar --- 2025-11-22T11:54:04.8005604Z --- start calendar --- 2025-11-22T11:54:04.8005923Z YkVHSU46VkVWRUY4CkVORDo= 2025-11-22T11:54:04.8006237Z --- end calendar --- 2025-11-22T11:54:04.8006535Z --- start calendar --- 2025-11-22T11:54:04.8006889Z YkVHSU46VkVWRUY4CkVORDo= 2025-11-22T11:54:04.8007212Z --- end calendar --- 2025-11-22T11:54:04.8007630Z --- start calendar --- 2025-11-22T11:54:04.8007954Z QkVHSU46VkVWMlJOCkVORDo= 2025-11-22T11:54:04.8008274Z --- end calendar --- 2025-11-22T11:54:04.8008700Z --- start calendar --- 2025-11-22T11:54:04.8009024Z QkVHSU46VkVWMlJOCkVORDo= 2025-11-22T11:54:04.8009340Z --- end calendar --- 2025-11-22T11:54:04.8009636Z --- start calendar --- 2025-11-22T11:54:04.8009945Z QkVHSU46VkVWRU42CkVORDo= 2025-11-22T11:54:04.8010263Z --- end calendar --- 2025-11-22T11:54:04.8010560Z --- start calendar --- 2025-11-22T11:54:04.8010871Z QkVHSU46VkVWRU42CkVORDo= 2025-11-22T11:54:04.8011181Z --- end calendar --- 2025-11-22T11:54:04.8011482Z --- start calendar --- 2025-11-22T11:54:04.8011932Z QkVHSU46VkVWRU4tCkVORDo= 2025-11-22T11:54:04.8012251Z --- end calendar --- 2025-11-22T11:54:04.8012549Z --- start calendar --- 2025-11-22T11:54:04.8012858Z QkVHSU46VkVWRU4tCkVORDo= 2025-11-22T11:54:04.8013179Z --- end calendar --- 2025-11-22T11:54:04.8013467Z --- start calendar --- 2025-11-22T11:54:04.8013775Z QkVHSU46Ck07Y249CkVOZDo= 2025-11-22T11:54:04.8014085Z --- end calendar --- 2025-11-22T11:54:04.8014378Z --- start calendar --- 2025-11-22T11:54:04.8014685Z QkVnSU46Cks7MT1eCkVOZDo= 2025-11-22T11:54:04.8015011Z --- end calendar --- 2025-11-22T11:54:04.8015299Z --- start calendar --- 2025-11-22T11:54:04.8015614Z YkVHSU46VlZFKk5rCkVORDo= 2025-11-22T11:54:04.8015926Z --- end calendar --- 2025-11-22T11:54:04.8016225Z --- start calendar --- 2025-11-22T11:54:04.8016537Z YkVHSU46VlZFKk5rCkVORDo= 2025-11-22T11:54:04.8016848Z --- end calendar --- 2025-11-22T11:54:04.8017143Z --- start calendar --- 2025-11-22T11:54:04.8017583Z YkVHSU46K0VWAAFyCkVORDo= 2025-11-22T11:54:04.8017912Z --- end calendar --- 2025-11-22T11:54:04.8018201Z --- start calendar --- 2025-11-22T11:54:04.8018517Z YkVHSU46K0VWAAFyCkVORDo= 2025-11-22T11:54:04.8018834Z --- end calendar --- 2025-11-22T11:54:04.8019125Z --- start calendar --- 2025-11-22T11:54:04.8019436Z WjtCPSw7MT0sOzA9LDsyPSw= 2025-11-22T11:54:04.8019757Z --- end calendar --- 2025-11-22T11:54:04.8020049Z --- start calendar --- 2025-11-22T11:54:04.8020356Z QkVHSW46VkVWRU5UCkVORDo= 2025-11-22T11:54:04.8020675Z --- end calendar --- 2025-11-22T11:54:04.8020966Z --- start calendar --- 2025-11-22T11:54:04.8021292Z QkVHSU46VitWAX9OCkVORDo= 2025-11-22T11:54:04.8021606Z --- end calendar --- 2025-11-22T11:54:04.8021902Z --- start calendar --- 2025-11-22T11:54:04.8022206Z QkVHSU46VitWAX9OCkVORDo= 2025-11-22T11:54:04.8022523Z --- end calendar --- 2025-11-22T11:54:04.8022811Z --- start calendar --- 2025-11-22T11:54:04.8023125Z QkVHSU46Vg5XRQ42CkVORDo= 2025-11-22T11:54:04.8023437Z --- end calendar --- 2025-11-22T11:54:04.8023734Z --- start calendar --- 2025-11-22T11:54:04.8024048Z QkVHSU46Vg5XRQ42CkVORDo= 2025-11-22T11:54:04.8024363Z --- end calendar --- 2025-11-22T11:54:04.8024658Z --- start calendar --- 2025-11-22T11:54:04.8024965Z QkVHSW46VmU5K0VOCkVORDo= 2025-11-22T11:54:04.8025287Z --- end calendar --- 2025-11-22T11:54:04.8025575Z --- start calendar --- 2025-11-22T11:54:04.8025891Z QkVHSW46VmU5K0VOCkVORDo= 2025-11-22T11:54:04.8026201Z --- end calendar --- 2025-11-22T11:54:04.8026494Z --- start calendar --- 2025-11-22T11:54:04.8026812Z QkVHSW46VipWRWVWCkVORDo= 2025-11-22T11:54:04.8027139Z --- end calendar --- 2025-11-22T11:54:04.8027553Z --- start calendar --- 2025-11-22T11:54:04.8027873Z QkVHSW46VipWRWVWCkVORDo= 2025-11-22T11:54:04.8028190Z --- end calendar --- 2025-11-22T11:54:04.8028488Z --- start calendar --- 2025-11-22T11:54:04.8028800Z QkVHSU46VkVWM1JOCkVORDo= 2025-11-22T11:54:04.8029114Z --- end calendar --- 2025-11-22T11:54:04.8029412Z --- start calendar --- 2025-11-22T11:54:04.8029723Z QkVHSU46VkVWM1JOCkVORDo= 2025-11-22T11:54:04.8030042Z --- end calendar --- 2025-11-22T11:54:04.8030336Z --- start calendar --- 2025-11-22T11:54:04.8030648Z QkVHSU46V0VWMVJOCkVORDo= 2025-11-22T11:54:04.8030963Z --- end calendar --- 2025-11-22T11:54:04.8031258Z --- start calendar --- 2025-11-22T11:54:04.8031566Z QkVHSU46V0VWMVJOCkVORDo= 2025-11-22T11:54:04.8031887Z --- end calendar --- 2025-11-22T11:54:04.8032173Z --- start calendar --- 2025-11-22T11:54:04.8032488Z QkVHSU46CkRVRTpQCkVOZDo= 2025-11-22T11:54:04.8032810Z --- end calendar --- 2025-11-22T11:54:04.8033099Z --- start calendar --- 2025-11-22T11:54:04.8033555Z QkVHSU46VkVWMVJOCkVORDo= 2025-11-22T11:54:04.8033875Z --- end calendar --- 2025-11-22T11:54:04.8034172Z --- start calendar --- 2025-11-22T11:54:04.8034479Z QkVHSU46VkVWMVJOCkVORDo= 2025-11-22T11:54:04.8034801Z --- end calendar --- 2025-11-22T11:54:04.8035089Z --- start calendar --- 2025-11-22T11:54:04.8035402Z YkVHSU46VkVNRU4rCkVORDo= 2025-11-22T11:54:04.8035715Z --- end calendar --- 2025-11-22T11:54:04.8036012Z --- start calendar --- 2025-11-22T11:54:04.8036460Z YkVHSU46VkVNRU4rCkVORDo= 2025-11-22T11:54:04.8036786Z --- end calendar --- 2025-11-22T11:54:04.8037081Z --- start calendar --- 2025-11-22T11:54:04.8037527Z YkVHaU46ClA6ClA6CkVOZDo= 2025-11-22T11:54:04.8037856Z --- end calendar --- 2025-11-22T11:54:04.8038145Z --- start calendar --- 2025-11-22T11:54:04.8038535Z YkVHSU46VjpfHh4eCkVOZDo= 2025-11-22T11:54:04.8038854Z --- end calendar --- 2025-11-22T11:54:04.8039154Z --- start calendar --- 2025-11-22T11:54:04.8039463Z YkVHSU46VjpfHh4eCkVOZDo= 2025-11-22T11:54:04.8039785Z --- end calendar --- 2025-11-22T11:54:04.8040085Z --- start calendar --- 2025-11-22T11:54:04.8040411Z QkVHSW46VmVPRTBNCkVORDo= 2025-11-22T11:54:04.8040730Z --- end calendar --- 2025-11-22T11:54:04.8041025Z --- start calendar --- 2025-11-22T11:54:04.8041344Z QkVHSW46VmVPRTBNCkVORDo= 2025-11-22T11:54:04.8041663Z --- end calendar --- 2025-11-22T11:54:04.8041960Z --- start calendar --- 2025-11-22T11:54:04.8042271Z QkVHSU46CkRVRToxMCAzNDg= 2025-11-22T11:54:04.8042587Z --- end calendar --- 2025-11-22T11:54:04.8042882Z --- start calendar --- 2025-11-22T11:54:04.8043197Z QkVHSW46VmVWRTFNCkVORDo= 2025-11-22T11:54:04.8043509Z --- end calendar --- 2025-11-22T11:54:04.8043804Z --- start calendar --- 2025-11-22T11:54:04.8044113Z QkVHSW46VmVWRTFNCkVORDo= 2025-11-22T11:54:04.8044435Z --- end calendar --- 2025-11-22T11:54:04.8044728Z --- start calendar --- 2025-11-22T11:54:04.8045039Z QkVHSW46Vk1lKzBOCkVORDo= 2025-11-22T11:54:04.8045365Z --- end calendar --- 2025-11-22T11:54:04.8045654Z --- start calendar --- 2025-11-22T11:54:04.8045972Z QkVHSW46Vk1lKzBOCkVORDo= 2025-11-22T11:54:04.8046283Z --- end calendar --- 2025-11-22T11:54:04.8046578Z --- start calendar --- 2025-11-22T11:54:04.8046884Z QkVHSU46VkVWRU5UCkRVRTo= 2025-11-22T11:54:04.8047205Z --- end calendar --- 2025-11-22T11:54:04.8047631Z --- start calendar --- 2025-11-22T11:54:04.8047950Z QmVHSW46VmVWRWVWCkVORDo= 2025-11-22T11:54:04.8048260Z --- end calendar --- 2025-11-22T11:54:04.8048556Z --- start calendar --- 2025-11-22T11:54:04.8048863Z QmVHSW46VmVWRWVWCkVORDo= 2025-11-22T11:54:04.8049191Z --- end calendar --- 2025-11-22T11:54:04.8049484Z --- start calendar --- 2025-11-22T11:54:04.8049801Z QmVHSU46CkRVRTpQCkRVRTo= 2025-11-22T11:54:04.8050124Z --- end calendar --- 2025-11-22T11:54:04.8050418Z --- start calendar --- 2025-11-22T11:54:04.8050733Z QkVHSW46VmVWRTBNCkVORDo= 2025-11-22T11:54:04.8051045Z --- end calendar --- 2025-11-22T11:54:04.8051343Z --- start calendar --- 2025-11-22T11:54:04.8051652Z QkVHSW46VmVWRTBNCkVORDo= 2025-11-22T11:54:04.8051977Z --- end calendar --- 2025-11-22T11:54:04.8052273Z --- start calendar --- 2025-11-22T11:54:04.8052596Z YkVHSU46VkVWRU4jCkVORDo= 2025-11-22T11:54:04.8052912Z --- end calendar --- 2025-11-22T11:54:04.8053212Z --- start calendar --- 2025-11-22T11:54:04.8053525Z YkVHSU46VkVWRU4jCkVORDo= 2025-11-22T11:54:04.8053838Z --- end calendar --- 2025-11-22T11:54:04.8054142Z --- start calendar --- 2025-11-22T11:54:04.8054451Z QkVHSU46ClJEQVRFOlAsUCw= 2025-11-22T11:54:04.8054772Z --- end calendar --- 2025-11-22T11:54:04.8055074Z --- start calendar --- 2025-11-22T11:54:04.8055390Z QkVHSW46VmVWRTJNCkVORDo= 2025-11-22T11:54:04.8055703Z --- end calendar --- 2025-11-22T11:54:04.8055997Z --- start calendar --- 2025-11-22T11:54:04.8056305Z QkVHSW46VmVWRTJNCkVORDo= 2025-11-22T11:54:04.8056629Z --- end calendar --- 2025-11-22T11:54:04.8056924Z --- start calendar --- 2025-11-22T11:54:04.8057236Z QkVHSU46ClJSVUxFOj07PTs9 2025-11-22T11:54:04.8057696Z --- end calendar --- 2025-11-22T11:54:04.8057994Z --- start calendar --- 2025-11-22T11:54:04.8058310Z QkVHSU46CkRURU5EO1RaSUQ9 2025-11-22T11:54:04.8058760Z --- end calendar --- 2025-11-22T11:54:04.8059061Z --- start calendar --- 2025-11-22T11:54:04.8059370Z QkVnSU46VkVWRU5UCiUKAAoP 2025-11-22T11:54:04.8059689Z --- end calendar --- 2025-11-22T11:54:04.8059981Z --- start calendar --- 2025-11-22T11:54:04.8060293Z QkVnSU46ClJEQXRlOlAsUCwv 2025-11-22T11:54:04.8060608Z --- end calendar --- 2025-11-22T11:54:04.8060905Z --- start calendar --- 2025-11-22T11:54:04.8061220Z QkVHSU46ClRaT0ZGU0VUVE86 2025-11-22T11:54:04.8061671Z --- end calendar --- 2025-11-22T11:54:04.8061965Z --- start calendar --- 2025-11-22T11:54:04.8062266Z QkVHSW46Ck46Ckw6Ck46Ckw6 2025-11-22T11:54:04.8062586Z --- end calendar --- 2025-11-22T11:54:04.8062873Z --- start calendar --- 2025-11-22T11:54:04.8063193Z QkVHSU46ClJyVUxFOgpFTkQ6 2025-11-22T11:54:04.8063508Z --- end calendar --- 2025-11-22T11:54:04.8063805Z --- start calendar --- 2025-11-22T11:54:04.8064112Z QjsiIiIiIiIiIiIiIiIiIiIi 2025-11-22T11:54:04.8064440Z --- end calendar --- 2025-11-22T11:54:04.8064731Z --- start calendar --- 2025-11-22T11:54:04.8065053Z QkVHSU46CkRVRTogMSAxIDAv 2025-11-22T11:54:04.8065383Z --- end calendar --- 2025-11-22T11:54:04.8065673Z --- start calendar --- 2025-11-22T11:54:04.8065984Z QkVHaU46Ck46Ck46Ck46Ck46 2025-11-22T11:54:04.8066296Z --- end calendar --- 2025-11-22T11:54:04.8066590Z --- start calendar --- 2025-11-22T11:54:04.8066903Z QkVHSU46ClU7RD0sLApFTmQ6 2025-11-22T11:54:04.8067224Z --- end calendar --- 2025-11-22T11:54:04.8067646Z --- start calendar --- 2025-11-22T11:54:04.8067973Z QkVHSU46ClJyVUxFOkZSRVE9 2025-11-22T11:54:04.8068289Z --- end calendar --- 2025-11-22T11:54:04.8068588Z --- start calendar --- 2025-11-22T11:54:04.8076880Z QkVHSW46CkJFR0lOOgpFTmQ6 2025-11-22T11:54:04.8077276Z --- end calendar --- 2025-11-22T11:54:04.8077801Z --- start calendar --- 2025-11-22T11:54:04.8078090Z QkVHSU46VkVWRU5UClIKVgpS 2025-11-22T11:54:04.8078297Z --- end calendar --- 2025-11-22T11:54:04.8078476Z --- start calendar --- 2025-11-22T11:54:04.8078667Z QkVHSU46ClJEQVRFOi1QLC1Q 2025-11-22T11:54:04.8078866Z --- end calendar --- 2025-11-22T11:54:04.8079048Z --- start calendar --- 2025-11-22T11:54:04.8079234Z QkVHSU46VkVWZU5UCjoKOgo7 2025-11-22T11:54:04.8079428Z --- end calendar --- 2025-11-22T11:54:04.8079608Z --- start calendar --- 2025-11-22T11:54:04.8079790Z QkVHSU46CkQ7MT1cLApFTmQ6 2025-11-22T11:54:04.8080002Z --- end calendar --- 2025-11-22T11:54:04.8080184Z --- start calendar --- 2025-11-22T11:54:04.8080376Z QkVHSU46ClJEQVRFOlAsUCxQ 2025-11-22T11:54:04.8080568Z --- end calendar --- 2025-11-22T11:54:04.8080743Z --- start calendar --- 2025-11-22T11:54:04.8080922Z QTs7Ozs7Ozs7Ozs7Ozs7Ozs7 2025-11-22T11:54:04.8081105Z --- end calendar --- 2025-11-22T11:54:04.8081270Z --- start calendar --- 2025-11-22T11:54:04.8081465Z QkVHSU46CjBMOgpEQjoKRU5kOg== 2025-11-22T11:54:04.8081668Z --- end calendar --- 2025-11-22T11:54:04.8081848Z --- start calendar --- 2025-11-22T11:54:04.8082039Z QkVHSW46CkUzOgpFTDoKRU5kOg== 2025-11-22T11:54:04.8082235Z --- end calendar --- 2025-11-22T11:54:04.8082407Z --- start calendar --- 2025-11-22T11:54:04.8082594Z QkVHSW46CkUzOgpFTjoKRU5kOg== 2025-11-22T11:54:04.8082793Z --- end calendar --- 2025-11-22T11:54:04.8082963Z --- start calendar --- 2025-11-22T11:54:04.8083152Z QkVHSU46CjlNOgpEQjoKRU5kOg== 2025-11-22T11:54:04.8083344Z --- end calendar --- 2025-11-22T11:54:04.8083515Z --- start calendar --- 2025-11-22T11:54:04.8083697Z NjtHPSwsLCwsLCwsLCwsLCwsLA== 2025-11-22T11:54:04.8083895Z --- end calendar --- 2025-11-22T11:54:04.8084071Z --- start calendar --- 2025-11-22T11:54:04.8084259Z QkVHSU46CkdFTzowOzAKRU5kOg== 2025-11-22T11:54:04.8084458Z --- end calendar --- 2025-11-22T11:54:04.8084624Z --- start calendar --- 2025-11-22T11:54:04.8084812Z QkVHSU46CkdFTzo0OzAKR0VPOg== 2025-11-22T11:54:04.8085007Z --- end calendar --- 2025-11-22T11:54:04.8085180Z --- start calendar --- 2025-11-22T11:54:04.8085363Z QkVHaU46ClJEQVRFOlAKRU5kOg== 2025-11-22T11:54:04.8085561Z --- end calendar --- 2025-11-22T11:54:04.8085729Z --- start calendar --- 2025-11-22T11:54:04.8085916Z QkVHSU46CkJCOgpCbDoKRU5kOg== 2025-11-22T11:54:04.8086258Z --- end calendar --- 2025-11-22T11:54:04.8086439Z --- start calendar --- 2025-11-22T11:54:04.8086626Z QkVHSU46CjFMOgpEQjoKRU5kOg== 2025-11-22T11:54:04.8086823Z --- end calendar --- 2025-11-22T11:54:04.8086995Z --- start calendar --- 2025-11-22T11:54:04.8087177Z QkVHSU46CkIyOgpDbDoKRU5kOg== 2025-11-22T11:54:04.8087529Z --- end calendar --- 2025-11-22T11:54:04.8087700Z --- start calendar --- 2025-11-22T11:54:04.8087889Z QkVHSW46CkZJOgo5QjoKRU5kOg== 2025-11-22T11:54:04.8088218Z --- end calendar --- 2025-11-22T11:54:04.8088390Z --- start calendar --- 2025-11-22T11:54:04.8088572Z QkVHSU46CkRVRTo1ODYwIDIxOA== 2025-11-22T11:54:04.8088770Z --- end calendar --- 2025-11-22T11:54:04.8088937Z --- start calendar --- 2025-11-22T11:54:04.8089123Z YkVHaU46Cm07MT07UD0KRU5EOg== 2025-11-22T11:54:04.8089320Z --- end calendar --- 2025-11-22T11:54:04.8089487Z --- start calendar --- 2025-11-22T11:54:04.8089675Z YkVHSU46Ckk7ST1eM14KRU5EOg== 2025-11-22T11:54:04.8089864Z --- end calendar --- 2025-11-22T11:54:04.8090046Z --- start calendar --- 2025-11-22T11:54:04.8090231Z QkVHSU46CkI0OgpCbDoKRU5kOg== 2025-11-22T11:54:04.8090429Z --- end calendar --- 2025-11-22T11:54:04.8090596Z --- start calendar --- 2025-11-22T11:54:04.8090786Z QkVHSU46CkRVRTogMSAwIDAvUA== 2025-11-22T11:54:04.8090980Z --- end calendar --- 2025-11-22T11:54:04.8091158Z --- start calendar --- 2025-11-22T11:54:04.8091338Z QkVHSU46Ck07OT0sLCwKRU5kOg== 2025-11-22T11:54:04.8091538Z --- end calendar --- 2025-11-22T11:54:04.8091714Z --- start calendar --- 2025-11-22T11:54:04.8091897Z QkVHSW46CkVVOgpFODoKRU5kOg== 2025-11-22T11:54:04.8092096Z --- end calendar --- 2025-11-22T11:54:04.8092261Z --- start calendar --- 2025-11-22T11:54:04.8092448Z QkVHSU46ClJEQVRFOlAsUCxQLA== 2025-11-22T11:54:04.8092641Z --- end calendar --- 2025-11-22T11:54:04.8092813Z --- start calendar --- 2025-11-22T11:54:04.8092993Z QkVHSU46CkRVRTotLS0xABIWJg== 2025-11-22T11:54:04.8093190Z --- end calendar --- 2025-11-22T11:54:04.8093361Z --- start calendar --- 2025-11-22T11:54:04.8093554Z QkVHSW46CkVLOgpFSToKRU5kOg== 2025-11-22T11:54:04.8093748Z --- end calendar --- 2025-11-22T11:54:04.8093920Z --- start calendar --- 2025-11-22T11:54:04.8094104Z QkVnSU46Ckw7Q049LCcKRU5EOg== 2025-11-22T11:54:04.8094295Z --- end calendar --- 2025-11-22T11:54:04.8094470Z --- start calendar --- 2025-11-22T11:54:04.8094654Z YkVHSU46ClJSVUxFOkJZREFZPQ== 2025-11-22T11:54:04.8094854Z --- end calendar --- 2025-11-22T11:54:04.8095023Z --- start calendar --- 2025-11-22T11:54:04.8095219Z QkVHSU46ClJSVUxFOj0KRU5kOg== 2025-11-22T11:54:04.8095411Z --- end calendar --- 2025-11-22T11:54:04.8095585Z --- start calendar --- 2025-11-22T11:54:04.8095769Z QkVHSU46CkEwOgpCbDoKRU5kOg== 2025-11-22T11:54:04.8095969Z --- end calendar --- 2025-11-22T11:54:04.8096146Z --- start calendar --- 2025-11-22T11:54:04.8096329Z QkVHSW46CkVDOgpFSToKRU5kOg== 2025-11-22T11:54:04.8096528Z --- end calendar --- 2025-11-22T11:54:04.8096694Z --- start calendar --- 2025-11-22T11:54:04.8096887Z QkVHSU46CkRVRTpQOVcKRU5kOg== 2025-11-22T11:54:04.8097084Z --- end calendar --- 2025-11-22T11:54:04.8097265Z --- start calendar --- 2025-11-22T11:54:04.8097565Z QkVHSU46CkJCOgpYazoKRU5kOg== 2025-11-22T11:54:04.8097766Z --- end calendar --- 2025-11-22T11:54:04.8097934Z --- start calendar --- 2025-11-22T11:54:04.8098119Z QkVHSU46CkRVRTpQNzNXCkVOZDo= 2025-11-22T11:54:04.8098315Z --- end calendar --- 2025-11-22T11:54:04.8098496Z --- start calendar --- 2025-11-22T11:54:04.8098688Z QkVHSU46ClJlUEVBVDoxCkVOZDo= 2025-11-22T11:54:04.8098882Z --- end calendar --- 2025-11-22T11:54:04.8099065Z --- start calendar --- 2025-11-22T11:54:04.8099248Z QkVHSU46ClJSVUxFOgpSUlVMRTo= 2025-11-22T11:54:04.8099450Z --- end calendar --- 2025-11-22T11:54:04.8099618Z --- start calendar --- 2025-11-22T11:54:04.8099810Z QkVHSU46CkVORDpWVElNRVpPTlU= 2025-11-22T11:54:04.8100006Z --- end calendar --- 2025-11-22T11:54:04.8100204Z --- start calendar --- 2025-11-22T11:54:04.8100385Z QkVnSU46Ckw7Q049LCwnCkVORDo= 2025-11-22T11:54:04.8100589Z --- end calendar --- 2025-11-22T11:54:04.8100759Z --- start calendar --- 2025-11-22T11:54:04.8101118Z QkVHSU46CkRVRTotUDJXCkVOZDo= 2025-11-22T11:54:04.8101322Z --- end calendar --- 2025-11-22T11:54:04.8101491Z --- start calendar --- 2025-11-22T11:54:04.8101679Z QkVHSU46VkVWRU5UCjEKMQowCnk= 2025-11-22T11:54:04.8101874Z --- end calendar --- 2025-11-22T11:54:04.8102047Z --- start calendar --- 2025-11-22T11:54:04.8102230Z QmVHSU46VkVWRU5UCkQ7SQpEO1Y= 2025-11-22T11:54:04.8102428Z --- end calendar --- 2025-11-22T11:54:04.8102595Z --- start calendar --- 2025-11-22T11:54:04.8102900Z QkVHSU46CkVOZDpWVElNRVpPTkU= 2025-11-22T11:54:04.8103097Z --- end calendar --- 2025-11-22T11:54:04.8103272Z --- start calendar --- 2025-11-22T11:54:04.8103456Z QkVHSU46CkRVRTotUDJECkVOZDo= 2025-11-22T11:54:04.8103655Z --- end calendar --- 2025-11-22T11:54:04.8103829Z --- start calendar --- 2025-11-22T11:54:04.8104014Z QkVHaU46VkVWRU5UCj8KPQo/Chk= 2025-11-22T11:54:04.8104213Z --- end calendar --- 2025-11-22T11:54:04.8104380Z --- start calendar --- 2025-11-22T11:54:04.8104569Z QkVHSU46ClJSVUxFOj07PTs9Oz0= 2025-11-22T11:54:04.8104772Z --- end calendar --- 2025-11-22T11:54:04.8104943Z --- start calendar --- 2025-11-22T11:54:04.8105127Z TTtVPV5eXl5eXl5eXl5eXl5eXl4= 2025-11-22T11:54:04.8105326Z --- end calendar --- 2025-11-22T11:54:04.8105493Z --- start calendar --- 2025-11-22T11:54:04.8105683Z QkVHSU46CkRVRTotUDRXCkVOZDo= 2025-11-22T11:54:04.8105877Z --- end calendar --- 2025-11-22T11:54:04.8106050Z --- start calendar --- 2025-11-22T11:54:04.8106242Z QkVHSU46VkVWZU5UCjoKOgo6Cjs= 2025-11-22T11:54:04.8106445Z --- end calendar --- 2025-11-22T11:54:04.8106618Z --- start calendar --- 2025-11-22T11:54:04.8106834Z QkVHSU46ClJSVUxFOjs7Ozs7Ozs= 2025-11-22T11:54:04.8107031Z --- end calendar --- 2025-11-22T11:54:04.8107197Z --- start calendar --- 2025-11-22T11:54:04.8107476Z QkVHSU46CkRVRTotUDVXCkVOZDo= 2025-11-22T11:54:04.8107672Z --- end calendar --- 2025-11-22T11:54:04.8107846Z --- start calendar --- 2025-11-22T11:54:04.8108029Z QkVHSU46CkRVRTpQOTFXCkVOZDo= 2025-11-22T11:54:04.8108233Z --- end calendar --- 2025-11-22T11:54:04.8108405Z --- start calendar --- 2025-11-22T11:54:04.8108596Z QkVHSU46CkVOZDpWVElNRVowRk0= 2025-11-22T11:54:04.8108793Z --- end calendar --- 2025-11-22T11:54:04.8108962Z --- start calendar --- 2025-11-22T11:54:04.8109149Z YkVHaU46ClA6Ci06Ckg6CkVOZDo= 2025-11-22T11:54:04.8109342Z --- end calendar --- 2025-11-22T11:54:04.8109516Z --- start calendar --- 2025-11-22T11:54:04.8109701Z QkVHSU46CkRVRTpQOTVXCkVOZDo= 2025-11-22T11:54:04.8109899Z --- end calendar --- 2025-11-22T11:54:04.8110072Z --- start calendar --- 2025-11-22T11:54:04.8110260Z QkVHSU46CkRVRTpQOTVXCkVOZDo= 2025-11-22T11:54:04.8110454Z --- end calendar --- 2025-11-22T11:54:04.8110624Z --- start calendar --- 2025-11-22T11:54:04.8110813Z QmVHSW46CkJFR0lOOgpCRUdJTjo= 2025-11-22T11:54:04.8111010Z --- end calendar --- 2025-11-22T11:54:04.8111182Z --- start calendar --- 2025-11-22T11:54:04.8111365Z QkVHSU46CkRVRTotUDhXCkVOZDo= 2025-11-22T11:54:04.8111566Z --- end calendar --- 2025-11-22T11:54:04.8111736Z --- start calendar --- 2025-11-22T11:54:04.8111929Z QkVHSU46CkVOZDpWVElNRVpPRk0= 2025-11-22T11:54:04.8112124Z --- end calendar --- 2025-11-22T11:54:04.8112297Z --- start calendar --- 2025-11-22T11:54:04.8112483Z YkVHSU46CkVOZDpWVElNRVpPTjI= 2025-11-22T11:54:04.8112683Z --- end calendar --- 2025-11-22T11:54:04.8112852Z --- start calendar --- 2025-11-22T11:54:04.8113038Z QkVHSU46ClJEQVRFOlAsUCxQLFA= 2025-11-22T11:54:04.8113240Z --- end calendar --- 2025-11-22T11:54:04.8113408Z --- start calendar --- 2025-11-22T11:54:04.8113602Z QkVHSU46CkVOZDpWVElNRXowTUU= 2025-11-22T11:54:04.8113796Z --- end calendar --- 2025-11-22T11:54:04.8113970Z --- start calendar --- 2025-11-22T11:54:04.8114153Z YkVHaU46ClA6ClA6ClA6CkVOZDo= 2025-11-22T11:54:04.8114351Z --- end calendar --- 2025-11-22T11:54:04.8114518Z --- start calendar --- 2025-11-22T11:54:04.8114707Z YkVHSU46CkVOZDpWVElNRVpPTjo= 2025-11-22T11:54:04.8114903Z --- end calendar --- 2025-11-22T11:54:04.8115076Z --- start calendar --- 2025-11-22T11:54:04.8115265Z YkVHSU46CkVOZDpWVElNRVpOTnw= 2025-11-22T11:54:04.8115465Z --- end calendar --- 2025-11-22T11:54:04.8115758Z --- start calendar --- 2025-11-22T11:54:04.8115948Z QkVHSU46ClNFblQtQlk6CkVOZDo= 2025-11-22T11:54:04.8116149Z --- end calendar --- 2025-11-22T11:54:04.8116315Z --- start calendar --- 2025-11-22T11:54:04.8116504Z QkVHSU46CkRJUjoKRElSOgpESVI6 2025-11-22T11:54:04.8116698Z --- end calendar --- 2025-11-22T11:54:04.8116871Z --- start calendar --- 2025-11-22T11:54:04.8117058Z QkVHSU46CkRVRTpQMjU1VwpFTmQ6 2025-11-22T11:54:04.8117259Z --- end calendar --- 2025-11-22T11:54:04.8117671Z --- start calendar --- 2025-11-22T11:54:04.8117857Z QkVnSU46ClJFQ1VSUkVOQ0UtMUk6 2025-11-22T11:54:04.8118061Z --- end calendar --- 2025-11-22T11:54:04.8118233Z --- start calendar --- 2025-11-22T11:54:04.8118422Z QkVHSU46CjE7Mj0sO3A9LApFTmQ6 2025-11-22T11:54:04.8118616Z --- end calendar --- 2025-11-22T11:54:04.8118790Z --- start calendar --- 2025-11-22T11:54:04.8118980Z QkVHSU46CkJJMToKQmlpOgpFTmQ6 2025-11-22T11:54:04.8119174Z --- end calendar --- 2025-11-22T11:54:04.8119347Z --- start calendar --- 2025-11-22T11:54:04.8119535Z QkVHSU46CkRVRTotUDgzVwpFTmQ6 2025-11-22T11:54:04.8119735Z --- end calendar --- 2025-11-22T11:54:04.8119904Z --- start calendar --- 2025-11-22T11:54:04.8120092Z QkVnSU46ClJFQ1VSUkVOQ0UtSTA6 2025-11-22T11:54:04.8120286Z --- end calendar --- 2025-11-22T11:54:04.8120460Z --- start calendar --- 2025-11-22T11:54:04.8120646Z QkVnSU46ClJFQ1VSUkVOQ0YzSS06 2025-11-22T11:54:04.8120844Z --- end calendar --- 2025-11-22T11:54:04.8121014Z --- start calendar --- 2025-11-22T11:54:04.8121214Z QkVHSU46CkRVRTotUDgwVwpFTmQ6 2025-11-22T11:54:04.8121413Z --- end calendar --- 2025-11-22T11:54:04.8121582Z --- start calendar --- 2025-11-22T11:54:04.8121774Z QkVHSU46CkJpbjoKQmlpOgpFTmQ6 2025-11-22T11:54:04.8121970Z --- end calendar --- 2025-11-22T11:54:04.8122145Z --- start calendar --- 2025-11-22T11:54:04.8122327Z QkVHSU46CjI7RD1eU14oXgpFTmQ6 2025-11-22T11:54:04.8122523Z --- end calendar --- 2025-11-22T11:54:04.8122690Z --- start calendar --- 2025-11-22T11:54:04.8122876Z QkVHaU46Ck46Ck46Ck46Ck46Ck46 2025-11-22T11:54:04.8123070Z --- end calendar --- 2025-11-22T11:54:04.8123244Z --- start calendar --- 2025-11-22T11:54:04.8123429Z QkVnSU46ClJFQ1VSUkVOQ0UtR0k6 2025-11-22T11:54:04.8123630Z --- end calendar --- 2025-11-22T11:54:04.8123803Z --- start calendar --- 2025-11-22T11:54:04.8123985Z QkVnSU46ClJFQ1VSUkVOQzdIMEk6 2025-11-22T11:54:04.8124186Z --- end calendar --- 2025-11-22T11:54:04.8124361Z --- start calendar --- 2025-11-22T11:54:04.8124556Z QkVHSU46ClJEQVRFOlAsUApFTmQ6 2025-11-22T11:54:04.8124758Z --- end calendar --- 2025-11-22T11:54:04.8124932Z --- start calendar --- 2025-11-22T11:54:04.8125114Z QkVnSU46ClJFQ1VSUkVOQ0UtSUU6 2025-11-22T11:54:04.8125311Z --- end calendar --- 2025-11-22T11:54:04.8125480Z --- start calendar --- 2025-11-22T11:54:04.8125667Z QkVnSU46ClJFQ1VSUkVOQ0V5SDA6 2025-11-22T11:54:04.8125861Z --- end calendar --- 2025-11-22T11:54:04.8126035Z --- start calendar --- 2025-11-22T11:54:04.8126226Z QkVHSU46CkJJMDoKQmlpOgpFTmQ6 2025-11-22T11:54:04.8126422Z --- end calendar --- 2025-11-22T11:54:04.8126599Z --- start calendar --- 2025-11-22T11:54:04.8126782Z QkVHSU46CkRVRTotUDE4VwpFTmQ6 2025-11-22T11:54:04.8126983Z --- end calendar --- 2025-11-22T11:54:04.8127150Z --- start calendar --- 2025-11-22T11:54:04.8127445Z QkVHSU46CkRVRTpQOTg3VwpFTmQ6 2025-11-22T11:54:04.8127648Z --- end calendar --- 2025-11-22T11:54:04.8127820Z --- start calendar --- 2025-11-22T11:54:04.8127999Z QkVnSU46ClJFQ1VSUkVOQ0UtMEk6 2025-11-22T11:54:04.8128192Z --- end calendar --- 2025-11-22T11:54:04.8128364Z --- start calendar --- 2025-11-22T11:54:04.8128543Z QkVnSU46ClJFQ1VSUkVOQ00tSFI6 2025-11-22T11:54:04.8128737Z --- end calendar --- 2025-11-22T11:54:04.8128903Z --- start calendar --- 2025-11-22T11:54:04.8129085Z QkVnSU46ClJFQ1VSUkVOQ0UtMkk6 2025-11-22T11:54:04.8129276Z --- end calendar --- 2025-11-22T11:54:04.8129443Z --- start calendar --- 2025-11-22T11:54:04.8129623Z QkVnSU46ClJFQ1VSUkVOQ0UtS0I6 2025-11-22T11:54:04.8129812Z --- end calendar --- 2025-11-22T11:54:04.8129975Z --- start calendar --- 2025-11-22T11:54:04.8130276Z QkVHSU46CkQ7NT0KRDs2PQpFTmQ6 2025-11-22T11:54:04.8130470Z --- end calendar --- 2025-11-22T11:54:04.8130636Z --- start calendar --- 2025-11-22T11:54:04.8130814Z QkVnSU46ClJFQ1VSUkVOQzVIMEk6 2025-11-22T11:54:04.8131004Z --- end calendar --- 2025-11-22T11:54:04.8131169Z --- start calendar --- 2025-11-22T11:54:04.8131345Z QkVnSU46ClJFQ1VSUkVOQ0UtSkI6 2025-11-22T11:54:04.8131541Z --- end calendar --- 2025-11-22T11:54:04.8131718Z --- start calendar --- 2025-11-22T11:54:04.8132032Z QkVHSU46CkRVRTotUDQyVwpFTmQ6 2025-11-22T11:54:04.8132236Z --- end calendar --- 2025-11-22T11:54:04.8132420Z --- start calendar --- 2025-11-22T11:54:04.8132611Z QkVHSU46CkJpbjoKQmloOgpFTmQ6 2025-11-22T11:54:04.8132814Z --- end calendar --- 2025-11-22T11:54:04.8132990Z --- start calendar --- 2025-11-22T11:54:04.8133181Z QkVHSU46CkRVRTotUDY0VwpFTmQ6 2025-11-22T11:54:04.8133385Z --- end calendar --- 2025-11-22T11:54:04.8133555Z --- start calendar --- 2025-11-22T11:54:04.8133746Z QkVHSU46ClJyVUxFOj0sLCwsLCws 2025-11-22T11:54:04.8133940Z --- end calendar --- 2025-11-22T11:54:04.8134119Z --- start calendar --- 2025-11-22T11:54:04.8134310Z QkVHSU46ClVSTDoKVVJNOgpFTmQ6 2025-11-22T11:54:04.8134511Z --- end calendar --- 2025-11-22T11:54:04.8134681Z --- start calendar --- 2025-11-22T11:54:04.8134875Z QkVnSU46ClJFQ1VSUkVOQ0UtSTE6 2025-11-22T11:54:04.8135072Z --- end calendar --- 2025-11-22T11:54:04.8135248Z --- start calendar --- 2025-11-22T11:54:04.8135434Z QkVHSU46ClVSTDoKVVJMOgpFTmQ6 2025-11-22T11:54:04.8135635Z --- end calendar --- 2025-11-22T11:54:04.8135813Z --- start calendar --- 2025-11-22T11:54:04.8135995Z QkVHSU46ClJEQVRFOi1QLC1QLC1Q 2025-11-22T11:54:04.8136199Z --- end calendar --- 2025-11-22T11:54:04.8136367Z --- start calendar --- 2025-11-22T11:54:04.8136557Z QkVnSU46ClJFQ1VSUkVOQ0UtSUI6 2025-11-22T11:54:04.8136749Z --- end calendar --- 2025-11-22T11:54:04.8136926Z --- start calendar --- 2025-11-22T11:54:04.8137111Z QkVnSU46Ckw7Q049LCwnLApFTkQ6 2025-11-22T11:54:04.8137309Z --- end calendar --- 2025-11-22T11:54:04.8137600Z --- start calendar --- 2025-11-22T11:54:04.8137799Z QkVHSU46CkRVRTotUDM2VwpFTmQ6 2025-11-22T11:54:04.8137993Z --- end calendar --- 2025-11-22T11:54:04.8138167Z --- start calendar --- 2025-11-22T11:54:04.8138355Z QkVHaU46ClJEQVRFOlAKUkRBVEU6 2025-11-22T11:54:04.8138551Z --- end calendar --- 2025-11-22T11:54:04.8138723Z --- start calendar --- 2025-11-22T11:54:04.8138905Z QkVnSU46ClJFQ1VSUkVOQ0VRQUU6 2025-11-22T11:54:04.8139106Z --- end calendar --- 2025-11-22T11:54:04.8139273Z --- start calendar --- 2025-11-22T11:54:04.8139467Z QkVnSU46ClJFQ1VSUkVOQ0UtSTM6 2025-11-22T11:54:04.8139660Z --- end calendar --- 2025-11-22T11:54:04.8139831Z --- start calendar --- 2025-11-22T11:54:04.8140013Z QkVnSU46ClJFQ1VSUkVOQ0UtNkk6 2025-11-22T11:54:04.8140218Z --- end calendar --- 2025-11-22T11:54:04.8140389Z --- start calendar --- 2025-11-22T11:54:04.8140574Z QkVnSU46ClJFQ1VSUkVOQ0VtMUg6 2025-11-22T11:54:04.8140772Z --- end calendar --- 2025-11-22T11:54:04.8140942Z --- start calendar --- 2025-11-22T11:54:04.8141129Z QkVnSU46ClJFQ1VSUkVOQzJINUo6 2025-11-22T11:54:04.8141327Z --- end calendar --- 2025-11-22T11:54:04.8141504Z --- start calendar --- 2025-11-22T11:54:04.8141686Z QkVnSU46ClJFQ1VSUkVOQ0UtSUk6 2025-11-22T11:54:04.8141883Z --- end calendar --- 2025-11-22T11:54:04.8142052Z --- start calendar --- 2025-11-22T11:54:04.8142239Z QkVHSW46CkVJbjoKRUkxOgpFTmQ6 2025-11-22T11:54:04.8142438Z --- end calendar --- 2025-11-22T11:54:04.8142611Z --- start calendar --- 2025-11-22T11:54:04.8142800Z QkVnSU46ClJFQ1VSUkVOQ0U1SS06 2025-11-22T11:54:04.8142999Z --- end calendar --- 2025-11-22T11:54:04.8143174Z --- start calendar --- 2025-11-22T11:54:04.8143357Z QkVnSU46ClJFQ1VSUkVOQ0VSSDA6 2025-11-22T11:54:04.8143557Z --- end calendar --- 2025-11-22T11:54:04.8143724Z --- start calendar --- 2025-11-22T11:54:04.8143910Z QkVnSU46ClJFQ1VSUkVOQ0VTSDA6 2025-11-22T11:54:04.8144103Z --- end calendar --- 2025-11-22T11:54:04.8144277Z --- start calendar --- 2025-11-22T11:54:04.8144458Z QkVnSU46ClJFQ1VSUkVOQzNIMEk6 2025-11-22T11:54:04.8144654Z --- end calendar --- 2025-11-22T11:54:04.8144938Z --- start calendar --- 2025-11-22T11:54:04.8145131Z QkVHSU46CkJJMjoKQmlJOgpFTmQ6 2025-11-22T11:54:04.8145329Z --- end calendar --- 2025-11-22T11:54:04.8145500Z --- start calendar --- 2025-11-22T11:54:04.8145693Z aTtLPTtGPTtGPTtGPTtGPTtGPTs9 2025-11-22T11:54:04.8145895Z --- end calendar --- 2025-11-22T11:54:04.8146070Z --- start calendar --- 2025-11-22T11:54:04.8146253Z QkVnSU46ClJFQ1VSUkVOQ0UwSS06 2025-11-22T11:54:04.8146450Z --- end calendar --- 2025-11-22T11:54:04.8146725Z --- start calendar --- 2025-11-22T11:54:04.8146926Z QkVHSU46ClJSVUxFOjU9Oz0KRU5kOg== 2025-11-22T11:54:04.8147141Z --- end calendar --- 2025-11-22T11:54:04.8147321Z --- start calendar --- 2025-11-22T11:54:04.8147623Z QkVHSU46CkRVRTpQOTAzM1cKRU5kOg== 2025-11-22T11:54:04.8147839Z --- end calendar --- 2025-11-22T11:54:04.8148016Z --- start calendar --- 2025-11-22T11:54:04.8148208Z QkVHSU46CkRVRTotUDY1NVcKRU5kOg== 2025-11-22T11:54:04.8148418Z --- end calendar --- 2025-11-22T11:54:04.8148588Z --- start calendar --- 2025-11-22T11:54:04.8148789Z QkVHSW46VgpSUlVMRTpCWURBWT0xVQ== 2025-11-22T11:54:04.8148997Z --- end calendar --- 2025-11-22T11:54:04.8149171Z --- start calendar --- 2025-11-22T11:54:04.8149359Z QkVHSW46VgpSUlVMRTpCWURBWT0xVQ== 2025-11-22T11:54:04.8149570Z --- end calendar --- 2025-11-22T11:54:04.8149736Z --- start calendar --- 2025-11-22T11:54:04.8149927Z QkVHaU46ClJEQVRFOlAKUkRBVEU6UA== 2025-11-22T11:54:04.8150134Z --- end calendar --- 2025-11-22T11:54:04.8150306Z --- start calendar --- 2025-11-22T11:54:04.8150505Z QkVHSU46CkRVRTogMCAyIDAKRU5kOg== 2025-11-22T11:54:04.8150712Z --- end calendar --- 2025-11-22T11:54:04.8150885Z --- start calendar --- 2025-11-22T11:54:04.8151071Z YkVHaU46Ck07Mj07bz07VD0KRU5kOg== 2025-11-22T11:54:04.8151279Z --- end calendar --- 2025-11-22T11:54:04.8151447Z --- start calendar --- 2025-11-22T11:54:04.8151638Z QkVHSU46CkRVRTotUDY1NlcKRU5kOg== 2025-11-22T11:54:04.8151845Z --- end calendar --- 2025-11-22T11:54:04.8152019Z --- start calendar --- 2025-11-22T11:54:04.8152206Z QkVHSU46ClRaT0ZGU0VUVE86LTE3NA== 2025-11-22T11:54:04.8152418Z --- end calendar --- 2025-11-22T11:54:04.8152594Z --- start calendar --- 2025-11-22T11:54:04.8152807Z 5oWk5oW046mM5ZWF6Z2F56m05oWk5oW046mO5ZWE4pK6 2025-11-22T11:54:04.8153065Z --- end calendar --- 2025-11-22T11:54:04.8153248Z --- start calendar --- 2025-11-22T11:54:04.8153457Z QkVHSU46CkRVRTpQOTIzM1cKRU5kOg== 2025-11-22T11:54:04.8153681Z --- end calendar --- 2025-11-22T11:54:04.8153858Z --- start calendar --- 2025-11-22T11:54:04.8154058Z QkVHSU46CkRVRTpQOTMyM1cKRU5kOg== 2025-11-22T11:54:04.8154276Z --- end calendar --- 2025-11-22T11:54:04.8154448Z --- start calendar --- 2025-11-22T11:54:04.8154643Z QkVHSW46ClJEQVRFOlBUMU0KRU5kOg== 2025-11-22T11:54:04.8154850Z --- end calendar --- 2025-11-22T11:54:04.8155025Z --- start calendar --- 2025-11-22T11:54:04.8155219Z ejtcXFxcXFxcXFxcXFxcXFxcXFxcXA== 2025-11-22T11:54:04.8155440Z --- end calendar --- 2025-11-22T11:54:04.8155610Z --- start calendar --- 2025-11-22T11:54:04.8155807Z QkVHSU46CkdFTzoxOzIKR0VPOjA7MA== 2025-11-22T11:54:04.8156019Z --- end calendar --- 2025-11-22T11:54:04.8156201Z --- start calendar --- 2025-11-22T11:54:04.8156393Z QkVHSU46ClRaT0ZGU0VUVE86LTI1NQ== 2025-11-22T11:54:04.8156599Z --- end calendar --- 2025-11-22T11:54:04.8156775Z --- start calendar --- 2025-11-22T11:54:04.8156964Z QkVHSU46VkVWRU5UCkRVRToKRFVFOg== 2025-11-22T11:54:04.8157175Z --- end calendar --- 2025-11-22T11:54:04.8157242Z --- start calendar --- 2025-11-22T11:54:04.8157325Z QkVHSU46CkZSRUVCVVNZO1RaSUQ9Cg== 2025-11-22T11:54:04.8157499Z --- end calendar --- 2025-11-22T11:54:04.8157568Z --- start calendar --- 2025-11-22T11:54:04.8157655Z STszPTs0PTtWPTtDPTtWPTtDPTtOPQ== 2025-11-22T11:54:04.8157722Z --- end calendar --- 2025-11-22T11:54:04.8157796Z --- start calendar --- 2025-11-22T11:54:04.8157877Z QkVHSU46VkVWRU5UCkdFTzoKR0VvOg== 2025-11-22T11:54:04.8157943Z --- end calendar --- 2025-11-22T11:54:04.8158017Z --- start calendar --- 2025-11-22T11:54:04.8158099Z QkVHSU46CkRVRTpQOTI4OVcKRU5kOg== 2025-11-22T11:54:04.8158165Z --- end calendar --- 2025-11-22T11:54:04.8158349Z --- start calendar --- 2025-11-22T11:54:04.8158438Z QkVHSU46CkRVRTotUDE2NVcKRU5kOg== 2025-11-22T11:54:04.8158504Z --- end calendar --- 2025-11-22T11:54:04.8158571Z --- start calendar --- 2025-11-22T11:54:04.8158653Z QkVHSU46ClJSVUxFOj0sLCwKRU5kOg== 2025-11-22T11:54:04.8158724Z --- end calendar --- 2025-11-22T11:54:04.8158792Z --- start calendar --- 2025-11-22T11:54:04.8158877Z QkVHSU46CkRVRTotUDUxMlcKRU5kOg== 2025-11-22T11:54:04.8158948Z --- end calendar --- 2025-11-22T11:54:04.8159121Z --- start calendar --- 2025-11-22T11:54:04.8159204Z QkVHSW46ClJEQVRFOlBUN1MKRU5kOg== 2025-11-22T11:54:04.8159270Z --- end calendar --- 2025-11-22T11:54:04.8159343Z --- start calendar --- 2025-11-22T11:54:04.8159424Z QkVHSU46CkRVRTotUDg0MVcKRU5kOg== 2025-11-22T11:54:04.8159494Z --- end calendar --- 2025-11-22T11:54:04.8159567Z --- start calendar --- 2025-11-22T11:54:04.8159650Z QkVHSU46CkJpaTU6CkJpaTI6CkVOZDo= 2025-11-22T11:54:04.8159715Z --- end calendar --- 2025-11-22T11:54:04.8159784Z --- start calendar --- 2025-11-22T11:54:04.8159880Z QmVHSU46CkRVRTpQCkRVRTpQCkRVRTo= 2025-11-22T11:54:04.8159947Z --- end calendar --- 2025-11-22T11:54:04.8160015Z --- start calendar --- 2025-11-22T11:54:04.8160103Z QkVHSU46CkRVRTotUDIxNDdXCkVOZDo= 2025-11-22T11:54:04.8160167Z --- end calendar --- 2025-11-22T11:54:04.8160235Z --- start calendar --- 2025-11-22T11:54:04.8160316Z QkVHSU46ClU7RD0sLCwsLCwsCkVOZDo= 2025-11-22T11:54:04.8160389Z --- end calendar --- 2025-11-22T11:54:04.8160455Z --- start calendar --- 2025-11-22T11:54:04.8160537Z QkVnSU46Ck07Y249Ck07Y249CkVOZDo= 2025-11-22T11:54:04.8160607Z --- end calendar --- 2025-11-22T11:54:04.8160674Z --- start calendar --- 2025-11-22T11:54:04.8160756Z YkVHaU46Ck86Ckg6Ck86Ckg6CkVOZDo= 2025-11-22T11:54:04.8160822Z --- end calendar --- 2025-11-22T11:54:04.8160895Z --- start calendar --- 2025-11-22T11:54:04.8160977Z QkVnSU46CkVOZDoKQkVnSU46CkVuZDo= 2025-11-22T11:54:04.8161043Z --- end calendar --- 2025-11-22T11:54:04.8161115Z --- start calendar --- 2025-11-22T11:54:04.8161204Z QkVHSW46CkJpaWk6CkJpaTA6CkVOZDo= 2025-11-22T11:54:04.8161270Z --- end calendar --- 2025-11-22T11:54:04.8161339Z --- start calendar --- 2025-11-22T11:54:04.8161432Z === Uncaught Python exception: === 2025-11-22T11:54:04.8161511Z ValueError: Invalid month: '' 2025-11-22T11:54:04.8161594Z Traceback (most recent call last): 2025-11-22T11:54:04.8161707Z File "ical_fuzzer.py", line 44, in TestOneInput 2025-11-22T11:54:04.8161886Z File "icalendar/tests/fuzzed/__init__.py", line 45, in fuzz_v1_calendar 2025-11-22T11:54:04.8162016Z File "icalendar/cal/calendar.py", line 85, in from_ical 2025-11-22T11:54:04.8162142Z File "icalendar/cal/component.py", line 446, in from_ical 2025-11-22T11:54:04.8162274Z File "icalendar/prop/__init__.py", line 2219, in from_ical 2025-11-22T11:54:04.8162404Z File "icalendar/prop/__init__.py", line 2204, in parse_type 2025-11-22T11:54:04.8162531Z File "icalendar/prop/__init__.py", line 2204, in 2025-11-22T11:54:04.8162656Z File "icalendar/prop/__init__.py", line 1953, in from_ical 2025-11-22T11:54:04.8162777Z File "icalendar/prop/__init__.py", line 1936, in __new__ 2025-11-22T11:54:04.8162855Z ValueError: Invalid month: '' 2025-11-22T11:54:04.8162861Z 2025-11-22T11:54:04.8162865Z 2025-11-22T11:54:04.8162956Z QkVHSU46ClJlcEVBVDowClJlUEVBVDo= 2025-11-22T11:54:04.8163024Z --- end calendar --- 2025-11-22T11:54:04.8163093Z --- start calendar --- 2025-11-22T11:54:04.8163183Z QkVnSU46CkJFR0lOOgpFTmQ6CkVOZDo= 2025-11-22T11:54:04.8163249Z --- end calendar --- 2025-11-22T11:54:04.8163320Z --- start calendar --- 2025-11-22T11:54:04.8163403Z QkVHSU46CkVOZDoKQkVHSU46CkVuZDo= 2025-11-22T11:54:04.8163476Z --- end calendar --- 2025-11-22T11:54:04.8163543Z --- start calendar --- 2025-11-22T11:54:04.8163627Z QkVHSW46VgpSUlVMRTpCWURBWT0tU1U= 2025-11-22T11:54:04.8163693Z --- end calendar --- 2025-11-22T11:54:04.8163768Z --- start calendar --- 2025-11-22T11:54:04.8163849Z QkVHSU46CkJFR0lOOgpFTmQ6CkVOZDo= 2025-11-22T11:54:04.8163915Z --- end calendar --- 2025-11-22T11:54:04.8163989Z --- start calendar --- 2025-11-22T11:54:04.8164157Z QkVHSU46CkRVRTpQNjU1MTdXCkVOZDo= 2025-11-22T11:54:04.8164226Z --- end calendar --- 2025-11-22T11:54:04.8164295Z --- start calendar --- 2025-11-22T11:54:04.8164384Z QkVHSW46CkJpaWk6CkJpaTY6CkVOZDo= 2025-11-22T11:54:04.8164453Z --- end calendar --- 2025-11-22T11:54:04.8164524Z --- start calendar --- 2025-11-22T11:54:04.8164622Z LTszPTswPTsxPTsxPTsyPTswPTswPTs= 2025-11-22T11:54:04.8164693Z --- end calendar --- 2025-11-22T11:54:04.8164841Z --- start calendar --- 2025-11-22T11:54:04.8164929Z QkVHSU46CkRVRTotUDk3MjdXCkVOZDo= 2025-11-22T11:54:04.8165004Z --- end calendar --- 2025-11-22T11:54:04.8165076Z --- start calendar --- 2025-11-22T11:54:04.8165165Z QkVHSW46VgpSUlVMRTpCWURBWT0wU1U= 2025-11-22T11:54:04.8165240Z --- end calendar --- 2025-11-22T11:54:04.8165311Z --- start calendar --- 2025-11-22T11:54:04.8165398Z QkVHSU46ClJEQVRFOlAsUCxQCkVOZDo= 2025-11-22T11:54:04.8165468Z --- end calendar --- 2025-11-22T11:54:04.8165544Z --- start calendar --- 2025-11-22T11:54:04.8165636Z QkVHSU46ClJEQVRFOlAsUCxQCkVOZDo= 2025-11-22T11:54:04.8165704Z --- end calendar --- 2025-11-22T11:54:04.8165783Z --- start calendar --- 2025-11-22T11:54:04.8165870Z QkVHSW46CkJpaWk6CkJpaTc6CkVOZDo= 2025-11-22T11:54:04.8165938Z --- end calendar --- 2025-11-22T11:54:04.8166010Z --- start calendar --- 2025-11-22T11:54:04.8166100Z QkVHSW46ClJEQVRFOlBUODhNCkVOZDo= 2025-11-22T11:54:04.8166169Z --- end calendar --- 2025-11-22T11:54:04.8166240Z --- start calendar --- 2025-11-22T11:54:04.8166337Z QkVHSU46CkRVRTotUDQ1MzBXCkVOZDo= 2025-11-22T11:54:04.8166407Z --- end calendar --- 2025-11-22T11:54:04.8166479Z --- start calendar --- 2025-11-22T11:54:04.8166565Z QkVHSU46CkRVRTotUDkwNjBXCkVOZDo= 2025-11-22T11:54:04.8166643Z --- end calendar --- 2025-11-22T11:54:04.8166714Z --- start calendar --- 2025-11-22T11:54:04.8166797Z YkVHaU46Ci46ClA6Ckg6Clg6CkVOZDo= 2025-11-22T11:54:04.8166876Z --- end calendar --- 2025-11-22T11:54:04.8166947Z --- start calendar --- 2025-11-22T11:54:04.8167032Z QkVHSW46CkJpaWk6CkJpaTg6CkVOZDo= 2025-11-22T11:54:04.8167110Z --- end calendar --- 2025-11-22T11:54:04.8167189Z --- start calendar --- 2025-11-22T11:54:04.8167274Z QkVHSW46ClRaT0ZGU0VUVE86CC0zNgo= 2025-11-22T11:54:04.8167448Z --- end calendar --- 2025-11-22T11:54:04.8167525Z --- start calendar --- 2025-11-22T11:54:04.8167612Z QkVHSU46CkJpaTU6CkJpaTE6CkVOZDo= 2025-11-22T11:54:04.8167682Z --- end calendar --- 2025-11-22T11:54:04.8167753Z --- start calendar --- 2025-11-22T11:54:04.8167843Z QkVHSU46CjA7RD1eU14oXiheCkVOZDo= 2025-11-22T11:54:04.8167917Z --- end calendar --- 2025-11-22T11:54:04.8167987Z --- start calendar --- 2025-11-22T11:54:04.8168079Z QkVHSU46CkJpaTU6CkJpaTM6CkVOZDo= 2025-11-22T11:54:04.8168149Z --- end calendar --- 2025-11-22T11:54:04.8168222Z --- start calendar --- 2025-11-22T11:54:04.8168311Z QkVHSU46CkVOZDoKQkVHSU46CkVuZDo= 2025-11-22T11:54:04.8168384Z --- end calendar --- 2025-11-22T11:54:04.8168456Z --- start calendar --- 2025-11-22T11:54:04.8168543Z QmVHSU46VkVWRU5UCkQ7SQpEO0kKRDtW 2025-11-22T11:54:04.8168617Z --- end calendar --- 2025-11-22T11:54:04.8168692Z --- start calendar --- 2025-11-22T11:54:04.8168780Z QkVHSU46ClJSdUxFOj07PTs9Oz07PTs9 2025-11-22T11:54:04.8168849Z --- end calendar --- 2025-11-22T11:54:04.8168925Z --- start calendar --- 2025-11-22T11:54:04.8169010Z QkVHSU46VkVWRU5UCnI7MD1/ClI7MT1/ 2025-11-22T11:54:04.8169079Z --- end calendar --- 2025-11-22T11:54:04.8169151Z --- start calendar --- 2025-11-22T11:54:04.8169245Z QkVHSW46VgpSUlVMRTpCWURBWT0tMVNV 2025-11-22T11:54:04.8169319Z --- end calendar --- 2025-11-22T11:54:04.8169395Z --- start calendar --- 2025-11-22T11:54:04.8169480Z QkVHaU46Ck46Ck46Ck46Ck46Ck46Ck46 2025-11-22T11:54:04.8169549Z --- end calendar --- 2025-11-22T11:54:04.8169621Z --- start calendar --- 2025-11-22T11:54:04.8169708Z QkVHSU46CkRVRTotUDMyNzY4VwpFTmQ6 2025-11-22T11:54:04.8169782Z --- end calendar --- 2025-11-22T11:54:04.8169853Z --- start calendar --- 2025-11-22T11:54:04.8169980Z 7oui7ouj7oui7ouo7o+i4quj7oqq7oui4o+i7oui7oqh7JO8 2025-11-22T11:54:04.8170056Z --- end calendar --- 2025-11-22T11:54:04.8170244Z --- start calendar --- 2025-11-22T11:54:04.8170336Z QkVHSU46ClJSVUxFOj0sLCwsLApFTmQ6 2025-11-22T11:54:04.8170407Z --- end calendar --- 2025-11-22T11:54:04.8170484Z --- start calendar --- 2025-11-22T11:54:04.8170571Z QkVHSU46CkRVRTotUDQ5MTUyVwpFTmQ6 2025-11-22T11:54:04.8170640Z --- end calendar --- 2025-11-22T11:54:04.8170715Z --- start calendar --- 2025-11-22T11:54:04.8170800Z QkVHSU46ClJSVUxFOjA9O1Q9Oyk9Oz1k 2025-11-22T11:54:04.8170870Z --- end calendar --- 2025-11-22T11:54:04.8171047Z --- start calendar --- 2025-11-22T11:54:04.8171139Z QkVHSU46CkRVRTotUDY1NTM2VwpFTmQ6 2025-11-22T11:54:04.8171208Z --- end calendar --- 2025-11-22T11:54:04.8171278Z --- start calendar --- 2025-11-22T11:54:04.8171373Z QkVHSU46ClJEQVRFOjAwMTAwMApFTmQ6 2025-11-22T11:54:04.8171442Z --- end calendar --- 2025-11-22T11:54:04.8171512Z --- start calendar --- 2025-11-22T11:54:04.8171597Z QkVHSU46CkRVRTotUDk4MzA0VwpFTmQ6 2025-11-22T11:54:04.8171669Z --- end calendar --- 2025-11-22T11:54:04.8171740Z --- start calendar --- 2025-11-22T11:54:04.8171834Z QkVHSU46CkRVRTpQNjU1MzU1VwpFTmQ6 2025-11-22T11:54:04.8171907Z --- end calendar --- 2025-11-22T11:54:04.8171979Z --- start calendar --- 2025-11-22T11:54:04.8172066Z QkVHSU46ClJEQVRFOlAsUCxQLFAsUCxQ 2025-11-22T11:54:04.8172135Z --- end calendar --- 2025-11-22T11:54:04.8172210Z --- start calendar --- 2025-11-22T11:54:04.8172295Z QkVHSW46Cmg6CmQ6CkQ6Cmg6CkU6CkU6 2025-11-22T11:54:04.8172364Z --- end calendar --- 2025-11-22T11:54:04.8172440Z --- start calendar --- 2025-11-22T11:54:04.8172533Z QkVHSU46ClJlUEVBVDozClJlUEVBVDow 2025-11-22T11:54:04.8172605Z --- end calendar --- 2025-11-22T11:54:04.8172675Z --- start calendar --- 2025-11-22T11:54:04.8172767Z YkVHaU46VkVWRU5UCnJzVlA6ClJzVlA6 2025-11-22T11:54:04.8172837Z --- end calendar --- 2025-11-22T11:54:04.8172910Z --- start calendar --- 2025-11-22T11:54:04.8173005Z QkVHSU46CkRVRTogMCAwIDIvRU5kOgAA 2025-11-22T11:54:04.8173077Z --- end calendar --- 2025-11-22T11:54:04.8173148Z --- start calendar --- 2025-11-22T11:54:04.8173235Z QkVHSU46ClNFblQtQlk6ClNFblQtQlk6 2025-11-22T11:54:04.8173315Z --- end calendar --- 2025-11-22T11:54:04.8173387Z --- start calendar --- 2025-11-22T11:54:04.8173474Z QkVHSU46CkRVRTotUDMyNzY3VwpFTmQ6 2025-11-22T11:54:04.8173548Z --- end calendar --- 2025-11-22T11:54:04.8173619Z --- start calendar --- 2025-11-22T11:54:04.8173704Z QkVHSU46ClJEQVRFOi1QLC1QLC1QLC1Q 2025-11-22T11:54:04.8173774Z --- end calendar --- 2025-11-22T11:54:04.8173852Z --- start calendar --- 2025-11-22T11:54:04.8173938Z QkVnSU46VkVWRU5UCkRVRTovCkRVRTov 2025-11-22T11:54:04.8174012Z --- end calendar --- 2025-11-22T11:54:04.8174089Z --- start calendar --- 2025-11-22T11:54:04.8174176Z QkVHSU46CkRVRTogMjU2IDEgMQpFTmQ6 2025-11-22T11:54:04.8174247Z --- end calendar --- 2025-11-22T11:54:04.8174318Z --- start calendar --- 2025-11-22T11:54:04.8174409Z QkVHSU46CkRVRTotUDY1NTM1VwpFTmQ6 2025-11-22T11:54:04.8174478Z --- end calendar --- 2025-11-22T11:54:04.8174549Z --- start calendar --- 2025-11-22T11:54:04.8174644Z QkVHSU46CkRVRTpQMzI3Njc2M1cKRU5kOg== 2025-11-22T11:54:04.8174722Z --- end calendar --- 2025-11-22T11:54:04.8174794Z --- start calendar --- 2025-11-22T11:54:04.8174886Z QkVHSU46CkRVRTpQNDc4OTk3OFcKRU5kOg== 2025-11-22T11:54:04.8174960Z --- end calendar --- 2025-11-22T11:54:04.8175031Z --- start calendar --- 2025-11-22T11:54:04.8175129Z UjtsPTtpPTttPTtuPTttPTttPTttPTtPPQ== 2025-11-22T11:54:04.8175198Z --- end calendar --- 2025-11-22T11:54:04.8175274Z --- start calendar --- 2025-11-22T11:54:04.8175368Z QkVHSU46CjlCMElnOgo5YjBJODoKRU5kOg== 2025-11-22T11:54:04.8175441Z --- end calendar --- 2025-11-22T11:54:04.8175518Z --- start calendar --- 2025-11-22T11:54:04.8175609Z QkVHSU46VkVWRU5UCnJEQVRlOlAKRU5kOg== 2025-11-22T11:54:04.8175678Z --- end calendar --- 2025-11-22T11:54:04.8175749Z --- start calendar --- 2025-11-22T11:54:04.8175845Z QkVHSU46CkRVRTpQMzI3Njc2MlcKRU5kOg== 2025-11-22T11:54:04.8175914Z --- end calendar --- 2025-11-22T11:54:04.8175985Z --- start calendar --- 2025-11-22T11:54:04.8176079Z QkVHSU46CkZSRUVCVVN5OjExNDEwMVkvUA== 2025-11-22T11:54:04.8176149Z --- end calendar --- 2025-11-22T11:54:04.8176307Z --- start calendar --- 2025-11-22T11:54:04.8176402Z QkVHSU46ClJEQVRFOlAsUCxQLFAKRU5kOg== 2025-11-22T11:54:04.8176478Z --- end calendar --- 2025-11-22T11:54:04.8176550Z --- start calendar --- 2025-11-22T11:54:04.8176639Z QkVHSU46ClJSVUxFOjA9O1Q9Oz0KRU5kOg== 2025-11-22T11:54:04.8176712Z --- end calendar --- 2025-11-22T11:54:04.8176782Z --- start calendar --- 2025-11-22T11:54:04.8176874Z QkVHSU46CkRVRTotUDI4NTU2MFcKRU5kOg== 2025-11-22T11:54:04.8177020Z --- end calendar --- 2025-11-22T11:54:04.8177096Z --- start calendar --- 2025-11-22T11:54:04.8177186Z QkVHSU46VkVWZU5UCjsKOjoKOgo7SjsKOw== 2025-11-22T11:54:04.8177257Z --- end calendar --- 2025-11-22T11:54:04.8177429Z --- start calendar --- 2025-11-22T11:54:04.8177526Z QkVHSU46ClJSVUxFOgpSclVMVToKRU5EOg== 2025-11-22T11:54:04.8177595Z --- end calendar --- 2025-11-22T11:54:04.8177667Z --- start calendar --- 2025-11-22T11:54:04.8177765Z QkVHSU46CkRVRTpQNDc4OTk2MVcKRU5kOg== 2025-11-22T11:54:04.8177837Z --- end calendar --- 2025-11-22T11:54:04.8177914Z --- start calendar --- 2025-11-22T11:54:04.8178010Z QkVHSU46CkRVRTpQNDc4OTk2MlcKRU5kOg== 2025-11-22T11:54:04.8178079Z --- end calendar --- 2025-11-22T11:54:04.8178152Z --- start calendar --- 2025-11-22T11:54:04.8178245Z QkVHSU46CkRVRTotUDYxMDQwMlcKRU5kOg== 2025-11-22T11:54:04.8178320Z --- end calendar --- 2025-11-22T11:54:04.8178391Z --- start calendar --- 2025-11-22T11:54:04.8178485Z QkVHSU46CkJpaWlsOgpCaWlpMDoKRU5kOg== 2025-11-22T11:54:04.8178566Z --- end calendar --- 2025-11-22T11:54:04.8178640Z --- start calendar --- 2025-11-22T11:54:04.8178732Z QkVHSU46ClJSVUxFOkZyRXE9WUVBUkxZOw== 2025-11-22T11:54:04.8178802Z --- end calendar --- 2025-11-22T11:54:04.8178878Z --- start calendar --- 2025-11-22T11:54:04.8178973Z QkVHSU46CkJpaWloOgpCaWlpMDoKRU5kOg== 2025-11-22T11:54:04.8179043Z --- end calendar --- 2025-11-22T11:54:04.8179119Z --- start calendar --- 2025-11-22T11:54:04.8179211Z QkVHSU46VgpSUlVMRTolbjtCWU1PTlRIPQ== 2025-11-22T11:54:04.8179306Z ==28== ERROR: libFuzzer: fuzz target exited 2025-11-22T11:54:04.8179631Z #0 0x7faaafd7005a in __sanitizer_print_stack_trace /root/llvm-project/compiler-rt/lib/asan/asan_stack.cpp:87:3 2025-11-22T11:54:04.8194452Z #1 0x7faaafc735f9 in fuzzer::PrintStackTrace() /root/llvm-project/compiler-rt/lib/fuzzer/FuzzerUtil.cpp:210:38 2025-11-22T11:54:04.8195329Z #2 0x7faaafc56946 in fuzzer::Fuzzer::ExitCallback() (.part.0) /root/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:250:18 2025-11-22T11:54:04.8195689Z #3 0x7faaafc56a18 in fuzzer::Fuzzer::ExitCallback() /root/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:210:1 2025-11-22T11:54:04.8196045Z #4 0x7faaafc56a18 in fuzzer::Fuzzer::StaticExitCallback() /root/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:209:18 2025-11-22T11:54:04.8196356Z #5 0x7faaafa548a6 (/lib/x86_64-linux-gnu/libc.so.6+0x468a6) (BuildId: 0323ab4806bee6f846d9ad4bccfc29afdca49a58) 2025-11-22T11:54:04.8196697Z #6 0x7faaafa54a5f in exit (/lib/x86_64-linux-gnu/libc.so.6+0x46a5f) (BuildId: 0323ab4806bee6f846d9ad4bccfc29afdca49a58) 2025-11-22T11:54:04.8196889Z #7 0x7faaad3cc078 in Py_Exit /tmp/Python-3.11.13/Python/pylifecycle.c:2944:5 2025-11-22T11:54:04.8197113Z #8 0x7faaad3cf299 in handle_system_exit /tmp/Python-3.11.13/Python/pythonrun.c:771:9 2025-11-22T11:54:04.8197316Z #9 0x7faaad3ceb35 in _PyErr_PrintEx /tmp/Python-3.11.13/Python/pythonrun.c:781:5 2025-11-22T11:54:04.8197824Z #10 0x40360f (build-out/ical_fuzzer.pkg+0x40360f) (BuildId: 04804d3c31218f938502cbed5cdd1af09d59a8f0) 2025-11-22T11:54:04.8198093Z #11 0x403ef4 (build-out/ical_fuzzer.pkg+0x403ef4) (BuildId: 04804d3c31218f938502cbed5cdd1af09d59a8f0) 2025-11-22T11:54:04.8198451Z #12 0x7faaafa32082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 0323ab4806bee6f846d9ad4bccfc29afdca49a58) 2025-11-22T11:54:04.8198721Z #13 0x40250e (build-out/ical_fuzzer.pkg+0x40250e) (BuildId: 04804d3c31218f938502cbed5cdd1af09d59a8f0) 2025-11-22T11:54:04.8198727Z 2025-11-22T11:54:04.8199233Z DEDUP_TOKEN: __sanitizer_print_stack_trace--fuzzer::PrintStackTrace()--fuzzer::Fuzzer::ExitCallback() (.part.0) 2025-11-22T11:54:04.8199335Z SUMMARY: libFuzzer: fuzz target exited 2025-11-22T11:54:04.8199451Z MS: 0 ; base unit: 0000000000000000000000000000000000000000 2025-11-22T11:54:04.8199818Z 0x40,0x44,0x47,0x42,0x45,0x47,0x49,0x4e,0x3a,0x56,0xa,0x52,0x52,0x55,0x4c,0xc5,0x3a,0x25,0xee,0x3b,0x42,0x59,0x4d,0x4f,0x4e,0x54,0x48,0x3d, 2025-11-22T11:54:04.8199901Z @DGBEGIN:V\012RRUL\305:%\356;BYMONTH= 2025-11-22T11:54:04.8200366Z artifact_prefix='/tmp/tmpp8_6szi9/'; Test unit written to /tmp/tmpp8_6szi9/crash-8b27cc3e60b1a6a95cf42ad106ed19b30a848197 2025-11-22T11:54:04.8200491Z Base64: QERHQkVHSU46VgpSUlVMxTol7jtCWU1PTlRIPQ== 2025-11-22T11:54:04.8200579Z stat::number_of_executed_units: 569 2025-11-22T11:54:04.8200669Z stat::average_exec_per_sec: 0 2025-11-22T11:54:04.8200749Z stat::new_units_added: 0 2025-11-22T11:54:04.8200828Z stat::slowest_unit_time_sec: 0 2025-11-22T11:54:04.8200904Z stat::peak_rss_mb: 266 2025-11-22T11:54:10.0868925Z 2025-11-22 11:54:10,086 - root - INFO - Reproduce command returned: 77. Reproducible on /github/workspace/build-out/ical_fuzzer. 2025-11-22T11:54:10.0869735Z 2025-11-22 11:54:10,086 - root - INFO - Crash is reproducible. 2025-11-22T11:54:10.2016735Z 2025-11-22 11:54:10,201 - root - INFO - Downloading latest build. 2025-11-22T11:54:10.2034297Z 2025-11-22 11:54:10,203 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): storage.googleapis.com:443 2025-11-22T11:54:10.3283882Z 2025-11-22 11:54:10,327 - urllib3.connectionpool - DEBUG - https://storage.googleapis.com:443 "GET /clusterfuzz-builds/icalendar/icalendar-address-202511220649.zip HTTP/1.1" 200 38802646 2025-11-22T11:54:10.9843603Z 2025-11-22 11:54:10,983 - root - INFO - Done downloading latest build. 2025-11-22T11:54:10.9844863Z 2025-11-22 11:54:10,984 - root - INFO - Trying to reproduce crash using: /tmp/tmpp8_6szi9/crash-8b27cc3e60b1a6a95cf42ad106ed19b30a848197. 2025-11-22T11:57:24.7229565Z 2025-11-22 11:57:24,722 - root - INFO - Reproduce command returned: 0. Not reproducible on /github/workspace/cifuzz-prev-build/ical_fuzzer. 2025-11-22T11:57:24.7230628Z 2025-11-22 11:57:24,722 - root - INFO - The crash is not reproducible on previous build. Code change (pr/commit) introduced crash. 2025-11-22T11:57:24.7231266Z 2025-11-22 11:57:24,722 - root - INFO - SAVING CRASH 2025-11-22T11:57:24.7234318Z 2025-11-22 11:57:24,723 - root - INFO - NOT MINIMIZED 2025-11-22T11:57:24.7237545Z 2025-11-22 11:57:24,723 - root - INFO - Deleting corpus and seed corpus of ical_fuzzer to save disk. 2025-11-22T11:57:24.7771353Z 2025-11-22 11:57:24,776 - root - INFO - Deleting fuzz target: ical_fuzzer. 2025-11-22T11:57:24.7772483Z 2025-11-22 11:57:24,776 - root - INFO - Done deleting. 2025-11-22T11:57:24.7773650Z 2025-11-22 11:57:24,777 - root - INFO - Bug found. Stopping fuzzing. 2025-11-22T11:57:24.7774243Z 2025-11-22 11:57:24,777 - root - INFO - Writing sarif results. 2025-11-22T11:57:24.7945625Z Traceback (most recent call last): 2025-11-22T11:57:24.7946355Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers_entrypoint.py", line 97, in 2025-11-22T11:57:24.7948453Z sys.exit(main()) 2025-11-22T11:57:24.7949310Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers_entrypoint.py", line 93, in main 2025-11-22T11:57:24.7949971Z return run_fuzzers_entrypoint() 2025-11-22T11:57:24.7950719Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers_entrypoint.py", line 63, in run_fuzzers_entrypoint 2025-11-22T11:57:24.7951535Z result = run_fuzzers.run_fuzzers(config) 2025-11-22T11:57:24.7952224Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers.py", line 316, in run_fuzzers 2025-11-22T11:57:24.7952916Z if not fuzz_target_runner.run_fuzz_targets(): 2025-11-22T11:57:24.7953630Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers.py", line 154, in run_fuzz_targets 2025-11-22T11:57:24.7957751Z write_fuzz_result_to_sarif(result, target_path, self.workspace) 2025-11-22T11:57:24.7958873Z File "/opt/oss-fuzz/infra/cifuzz/run_fuzzers.py", line 162, in write_fuzz_result_to_sarif 2025-11-22T11:57:24.7959742Z sarif_utils.write_stacktrace_to_sarif(fuzz_result.stacktrace, target_path, 2025-11-22T11:57:24.7960586Z File "/opt/oss-fuzz/infra/cifuzz/sarif_utils.py", line 247, in write_stacktrace_to_sarif 2025-11-22T11:57:24.7961276Z data = get_sarif_data(stacktrace, target_path) 2025-11-22T11:57:24.7961923Z File "/opt/oss-fuzz/infra/cifuzz/sarif_utils.py", line 214, in get_sarif_data 2025-11-22T11:57:24.7962804Z error_source_info = get_error_source_info(crash_info) 2025-11-22T11:57:24.7963510Z File "/opt/oss-fuzz/infra/cifuzz/sarif_utils.py", line 180, in get_error_source_info 2025-11-22T11:57:24.7963924Z frame = get_error_frame(crash_info) 2025-11-22T11:57:24.7964281Z File "/opt/oss-fuzz/infra/cifuzz/sarif_utils.py", line 166, in get_error_frame 2025-11-22T11:57:24.7964665Z [f.function_name for f in crash_info.frames[0]]) 2025-11-22T11:57:24.7964927Z IndexError: list index out of range 2025-11-22T11:57:24.9618309Z ##[group]Run actions/upload-artifact@v5 2025-11-22T11:57:24.9618601Z with: 2025-11-22T11:57:24.9618820Z name: artifacts 2025-11-22T11:57:24.9618997Z path: ./out/artifacts 2025-11-22T11:57:24.9619194Z if-no-files-found: warn 2025-11-22T11:57:24.9619386Z compression-level: 6 2025-11-22T11:57:24.9619567Z overwrite: false 2025-11-22T11:57:24.9619742Z include-hidden-files: false 2025-11-22T11:57:24.9619943Z ##[endgroup] 2025-11-22T11:57:25.1742885Z With the provided path, there will be 2 files uploaded 2025-11-22T11:57:25.1748224Z Artifact name is valid! 2025-11-22T11:57:25.1749582Z Root directory input is valid! 2025-11-22T11:57:25.2863056Z Beginning upload of artifact content to blob storage 2025-11-22T11:57:25.3971037Z Uploaded bytes 6621 2025-11-22T11:57:25.4252136Z Finished uploading artifact content to blob storage! 2025-11-22T11:57:25.4256019Z SHA256 digest of uploaded artifact zip is 2b7ff6dcb286d8d7b30914daaea4943e6e7fe0bdefee3f8c9bb5913f2278bc9b 2025-11-22T11:57:25.4258223Z Finalizing artifact upload 2025-11-22T11:57:25.5231286Z Artifact artifacts.zip successfully finalized. Artifact ID 4648702157 2025-11-22T11:57:25.5232676Z Artifact artifacts has been successfully uploaded! Final size is 6621 bytes. Artifact ID is 4648702157 2025-11-22T11:57:25.5239481Z Artifact download URL: https://github.com/collective/icalendar/actions/runs/19595066705/artifacts/4648702157 2025-11-22T11:57:25.5406029Z ##[group]Run github/codeql-action/upload-sarif@v4 2025-11-22T11:57:25.5406318Z with: 2025-11-22T11:57:25.5406510Z sarif_file: cifuzz-sarif/results.sarif 2025-11-22T11:57:25.5406764Z checkout_path: cifuzz-sarif 2025-11-22T11:57:25.5407159Z token: *** 2025-11-22T11:57:25.5407507Z matrix: null 2025-11-22T11:57:25.5407725Z wait-for-processing: true 2025-11-22T11:57:25.5407940Z ##[endgroup] 2025-11-22T11:57:25.6945668Z git call failed. Continuing with commit SHA from user input or environment. Error: 2025-11-22T11:57:26.0688623Z ##[error]Path does not exist: cifuzz-sarif/results.sarif 2025-11-22T11:57:26.0732684Z git call failed. Continuing with commit SHA from user input or environment. Error: 2025-11-22T11:57:26.2253166Z Post job cleanup. 2025-11-22T11:57:26.4119892Z ##[group]Uploading combined SARIF debug artifact 2025-11-22T11:57:26.4121574Z ##[endgroup] 2025-11-22T11:57:26.4221449Z Cleaning up orphan processes collective-icalendar-138c845/docs/how-to/fuzz-testing.rst000066400000000000000000000110371524134033600234170ustar00rootroot00000000000000============ Fuzz testing ============ This chapter describes how to perform automated fuzz testing. `Fuzz testing or fuzzing `_ is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. The program is then monitored for exceptions such as crashes, failing built-in code assertions, or potential memory leaks. icalendar uses `OSS-Fuzz`_ and `GitHub Actions `_ for fuzz testing. Identify uncaught fuzzing errors ================================ Occasionally, fuzz testing creates uncaught errors for which icalendar needs a test case. In a GitHub Actions log, calendars are printed out as base64-encoded lines of text with a leading timestamp. A sample log file is available at :download:`fuzz-testing-log.txt`. The following examples come from that log file. For simplicity, the timestamp is omitted. A typical calendar example consists of a start and end delimiter surrounding the base64 encoded calendar as shown. .. code-block:: text --- start calendar --- QkVHSU46CkJpaWloOgpCaWlpMDoKRU5kOg== --- end calendar --- To decode this calendar, use the following command. .. code-block:: shell $ echo "QkVHSU46CkJpaWloOgpCaWlpMDoKRU5kOg==" | base64 -d Observe all kinds of random data in the output that the fuzzer generates, and which may trigger errors. .. code-block:: console BEGIN: Biiih: Biiih0: END: When fuzz testing fails, it appears as an error in the log file. To identify uncaught fuzzing errors for which icalendar needs a test case, look for a start delimiter, followed by the base64 encoded calendar, and finally a line with an error code followed by the text ``libFuzzer: fuzz target exited``. .. code-block:: console --- start calendar --- QkVHSU46VgpSUlVMRTolbjtCWU1PTlRIPQ== ==28== ERROR: libFuzzer: fuzz target exited Create a fuzz test case ======================= To create the fuzz test case locally, use the following template, filling in the ```` string and adding a descriptive ````. .. code-block:: console echo "" | base64 -d | tee src/icalendar/tests/calendars/fuzz_testcase_.ics Then, you can run the tests and see that the error is reproduced. .. code-block:: console tox -e py313 -- src/icalendar/tests/fuzzed/ Ignore valid errors. Fix invalid errors. Reproduction example -------------------- In the sample log file :download:`fuzz-testing-log.txt`, find the fuzz test error by searching for the string ``libFuzzer: fuzz target exited``. Copy the immediately preceding base64 encoded calendar. .. code-block:: console QkVHSU46VgpSUlVMRTolbjtCWU1PTlRIPQ== The log shows the Python traceback usually a few hundred lines above this calendar. .. code-block:: text --- start calendar --- === Uncaught Python exception: === ValueError: Invalid month: '' Traceback (most recent call last): This provides a clue for how to write a test case. Generate the test case file by running the following command from the root of the repository, using the copied calendar. .. code-block:: console $ echo "QkVHSU46VgpSUlVMRTolbjtCWU1PTlRIPQ==" | base64 -d | tee src/icalendar/tests/calendars/fuzz_testcase_invalid_month.ics BEGIN:V RRULE:%n;BYMONTH= Next, run the tests. .. code-block:: shell $ tox -e py313 -- src/icalendar/tests/fuzzed/ The output shows that the error is reproduced. .. code-block:: console src/icalendar/tests/fuzzed/test_fuzzed_calendars.py .F # ... ValueError: Invalid month: '' Some tests cases point to code to fix. This one points to a valid error for a wrong month value. An empty month is invalid by the specification. As such, it is correct to have this error. Commit the test case and push it to a pull request, so that the error can be safely ignored during fuzz testing. Reproduce fuzzing issues locally ================================ To reproduce fuzzing issues locally, install `OSS-Fuzz`_ locally, following its `setup instructions `_. Then, you can run the script ``generate_python_test_cases_from_downloaded_clusterfuzz_test_cases.sh`` in ``src/icalendar/tests/fuzzed/`` to generate python test cases from the downloaded fuzzer test cases. .. code-block:: shell src/icalendar/tests/fuzzed/generate_python_test_cases_from_downloaded_clusterfuzz_test_cases.sh .. _OSS-Fuzz: https://google.github.io/oss-fuzz/ collective-icalendar-138c845/docs/how-to/index.rst000066400000000000000000000003211524134033600220470ustar00rootroot00000000000000============= How to guides ============= .. toctree:: :maxdepth: 2 install configure upgrade usage parse-errors attendees custom-components jcal cli fuzz-testing collective-icalendar-138c845/docs/how-to/install.rst000066400000000000000000000041731524134033600224170ustar00rootroot00000000000000======= Install ======= This chapter describes how to install icalendar as a Python package for use in your projects. Python package with pip ----------------------- The following command will install the icalendar package with pip. .. code-block:: shell pip install icalendar If installation is successful, you will be able to import the iCalendar package from a Python interpreter as shown. .. code-block:: pycon >>> import icalendar Distribution packages --------------------- The icalendar package is also published in many Linux distributions. The following badge shows where it is available, and which versions are packaged. .. image:: https://repology.org/badge/vertical-allrepos/python%3Aicalendar.svg?columns=4 :target: https://repology.org/project/python%3Aicalendar/versions :alt: Packaging status of icalendar across distributions Repology is a service that tracks the availability and versions of packages across many Linux distributions. `View the packaging status of icalendar across distributions `_. Alpine Linux ------------ On Alpine Linux the `py3-icalendar `_ package can be installed using ``apk``: .. code-block:: shell sudo apk add py3-icalendar Arch Linux ----------- On Arch Linux and derivatives, the package is available in the community repository. .. code-block:: shell sudo pacman -S python-icalendar Debian or Ubuntu ---------------- You can install the `python-icalendar `_ package for Debian or its derivatives. .. code-block:: shell sudo apt-get install python3-icalendar Fedora ------ You can install the `python3-icalendar package `_ for Fedora or its derivatives. .. code-block:: shell sudo dnf install python3-icalendar NixOS ----- You can install the icalendar package on NixOS using ``nix-env``: .. code-block:: shell nix-env -iA nixpkgs.python3Packages.icalendar collective-icalendar-138c845/docs/how-to/jcal.rst000066400000000000000000000050151524134033600216560ustar00rootroot00000000000000=============== jCal - RFC 7265 =============== This chapter describes how to read and write jCal files, and how to convert them between iCalendar files. :rfc:`7265` specifies how to convert the iCalendar format to and from the jCal format, a JSON based representation. Read jCal input =============== The following jCal calendar contains one event. .. code-block:: pycon >>> jCal = """ ... ["vcalendar", ... [ ... ["calscale", {}, "text", "GREGORIAN"], ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], ... ["version", {}, "text", "2.0"] ... ], ... [ ... ["vevent", ... [ ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], ... ["dtstart", {}, "date", "2008-10-06"], ... ["summary", {}, "text", "Planning meeting"], ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"] ... ], ... [] ... ] ... ] ... ] ... """ Use :meth:`~icalendar.cal.component.Component.from_jcal` to parse the :class:`list` or JSON :class:`str`. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.from_jcal(jCal) After parsing the calendar, inspect it as shown. .. code-block:: pycon >>> print(calendar.prodid) -//Example Inc.//Example Calendar//EN >>> print(calendar.events[0].summary) Planning meeting Write jCal output ================= To convert a :class:`~icalendar.cal.calendar.Calendar` and any other :class:`~icalendar.cal.component.Component` to jCal, use the :meth:`~icalendar.cal.component.Component.to_jcal` method. .. code-block:: pycon >>> from pprint import pprint >>> pprint(calendar.to_jcal()) ['vcalendar', [['calscale', {}, 'text', 'GREGORIAN'], ['prodid', {}, 'text', '-//Example Inc.//Example Calendar//EN'], ['version', {}, 'text', '2.0']], [['vevent', [['dtstamp', {}, 'date-time', '2008-02-05T19:12:24Z'], ['dtstart', {}, 'date', '2008-10-06'], ['summary', {}, 'text', 'Planning meeting'], ['uid', {}, 'text', '4088E990AD89CB3DBB484909']], []]]] To directly generate JSON output and write it to a file or send it to a server, use :meth:`~icalendar.cal.component.Component.to_json`. The following commands writes the jCal version of the calendar to a temporary file. .. code-block:: pycon >>> from tempfile import NamedTemporaryFile >>> file = NamedTemporaryFile(suffix=".jcal") >>> file.write(calendar.to_json().encode("UTF-8")) 358 collective-icalendar-138c845/docs/how-to/parse-errors.rst000066400000000000000000000101661524134033600233740ustar00rootroot00000000000000===================== Handle parsing errors ===================== iCalendar files from a source might contain invalid property values. By default, event components use error-tolerant parsing, allowing you to work with partially valid calendar data. .. note:: To avoid repetition, the code examples in this chapter use the following imports. .. code-block:: pycon >>> from icalendar import Calendar, BrokenCalendarProperty >>> from icalendar.prop import vBroken Check for parsing errors ------------------------ When a component is parsed, any properties with invalid values are recorded in the ``errors`` attribute. This attribute is a list of tuples containing the property name and error information. .. code-block:: pycon >>> ical_str = b"""BEGIN:VCALENDAR ... VERSION:2.0 ... PRODID:test ... BEGIN:VEVENT ... UID:test-123 ... DTSTART:INVALID-DATE ... SUMMARY:Meeting ... END:VEVENT ... END:VCALENDAR""" >>> cal = Calendar.from_ical(ical_str) >>> event = cal.walk("VEVENT")[0] >>> event.errors [('DTSTART', "Expected datetime, date, or time. Got: 'INVALID-DATE'")] Errors are populated immediately after parsing, without needing to access the problematic properties. Access broken properties ------------------------ Properties that fail to parse are converted to :class:`~icalendar.prop.broken.vBroken` instances. This preserves the raw value for inspection or round-trip serialization. .. code-block:: pycon >>> dtstart = event["DTSTART"] >>> isinstance(dtstart, vBroken) True >>> str(dtstart) 'INVALID-DATE' The broken property includes metadata about the parsing failure. .. code-block:: pycon >>> dtstart.property_name 'DTSTART' >>> dtstart.expected_type 'vDDDTypes' >>> isinstance(dtstart.parse_error, Exception) True Work with partially valid data ------------------------------ One broken property does not prevent access to other valid properties in the same component. .. code-block:: pycon >>> ical_str = b"""BEGIN:VCALENDAR ... VERSION:2.0 ... PRODID:test ... BEGIN:VEVENT ... UID:test-123 ... DTSTART:INVALID-DATE ... DTEND:20250102T120000Z ... SUMMARY:Meeting ... END:VEVENT ... END:VCALENDAR""" >>> cal = Calendar.from_ical(ical_str) >>> event = cal.walk("VEVENT")[0] >>> event["DTEND"].dt.year 2025 >>> str(event["SUMMARY"]) 'Meeting' Round-trip serialization ------------------------ Broken properties preserve their raw values, so they serialize correctly with :meth:`Component.to_ical `. .. code-block:: pycon >>> output = cal.to_ical() >>> b"DTSTART:INVALID-DATE" in output True Handle errors programmatically ------------------------------ You can iterate over errors to log or handle them. .. code-block:: pycon >>> for prop_name, error_msg in event.errors: ... print(f"{prop_name}: {error_msg}") DTSTART: Expected datetime, date, or time. Got: 'INVALID-DATE' To check if a specific property failed to parse, check if it is a :class:`~icalendar.prop.broken.vBroken`. .. code-block:: pycon >>> if isinstance(event["DTSTART"], vBroken): ... print("DTSTART failed to parse") DTSTART failed to parse Catch broken property access ---------------------------- When you access a property descriptor like :attr:`event.DTSTART ` on a broken property, a :class:`~icalendar.error.BrokenCalendarProperty` is raised with the original parse error chained as ``__cause__``. .. code-block:: pycon >>> try: ... event.DTSTART ... except BrokenCalendarProperty as e: ... print(f"Broken: {e}") ... print(f"Original error: {e.__cause__}") Broken: Cannot access 'dt' on broken property 'DTSTART' (expected 'vDDDTypes'): Expected datetime, date, or time. Got: 'INVALID-DATE' Original error: Expected datetime, date, or time. Got: 'INVALID-DATE' :class:`~icalendar.error.BrokenCalendarProperty` is a subclass of :class:`~icalendar.error.InvalidCalendar`, so existing ``except InvalidCalendar`` handlers will still catch it. collective-icalendar-138c845/docs/how-to/upgrade.rst000066400000000000000000000076561524134033600224110ustar00rootroot00000000000000============= Upgrade guide ============= This chapter describes how to upgrade icalendar to the latest version from previous versions. Its purpose is to help developers adapt their existing code. This guide includes only breaking changes and deprecation notices. For a comprehensive list of new features and bug fixes, see the :doc:`../reference/changelog`. .. _upgrade-7.0.0: 7.0.0 ===== This section describes the major changes to icalendar in version 7.0.0. .. _upgrade-7.0.0-breaking: Developers may be concerned about upgrading to a **new major** release. Upgrading from 6.x to 7.x should have **no complications for most developers**, because: - the core API stays compatible with 4.x - the breaking changes likely affect you only if you are an icalendar expert, not a normal user We still recommend checking out the new features and giving feedback in the repository. Breaking changes ---------------- This section describes the breaking changes in icalendar 7.0.0, and how to adapt your code to these changes. Python 3.8 and 3.9 support '''''''''''''''''''''''''' Python 3.8 and 3.9 have reached end of life. Support for Python 3.8 and 3.9 is removed in icalendar 7.0.0. .. seealso:: - `Status of Python versions `_ - `GitHub Actions Python support policy `_ ``Component.decoded`` return type changed ''''''''''''''''''''''''''''''''''''''''' The method :meth:`Component.decoded ` now returns a string instead of bytes for text properties. Property creation error change '''''''''''''''''''''''''''''' icalendar now correctly throws a ``TypeError`` for wrong types during property creation, instead of a ``ValueError``. If your code expects a ``ValueError`` when creating a property, then you should change your code to use ``TypeError``. Moved ``types_factory`` ''''''''''''''''''''''' ``types_factory`` was moved into :attr:`Component.types_factory `. Adjust your imports from the old location of ``types_factory``: .. code-block:: python from icalendar import cal …to the new location. .. code-block:: python from icalendar.cal.component import Component Moved ``components_factory`` '''''''''''''''''''''''''''' ``components_factory`` was moved into :attr:`Component.get_component_class `. Adjust your imports from the old location of ``components_factory``: .. code-block:: python from icalendar import cal …to the new location. .. code-block:: python from icalendar.cal.component import Component Moved ``IncompleteComponent`` ''''''''''''''''''''''''''''' The error ``icalendar.cal.IncompleteComponent`` was moved to :exc:`icalendar.error.IncompleteComponent`. Adjust your imports from the old location of ``IncompleteComponent``: .. code-block:: python from icalendar.cal import IncompleteComponent …to the new location. .. code-block:: python from icalendar.error import IncompleteComponent Removed ``icalendar.UIDGenerator`` '''''''''''''''''''''''''''''''''' ``icalendar.UIDGenerator`` was removed. Use the Python standard library's :mod:`uuid` module instead. .. _upgrade-6.0.0: 6.0.0 ===== This section describes the major changes to icalendar in version 6.0.0. .. _upgrade-6.0.0-deprecation: Deprecations ------------ The following deprecation notice describes which features may be removed in a future major release of icalendar. ``pytz`` support '''''''''''''''' :mod:`zoneinfo` is the recommended replacement for `pytz `_. ``zoneinfo`` was added in Python 3.9, and is not available in Python 3.8. In icalendar 6.0.0a, full support for ``zoneinfo`` was added. ``pytz`` may still be used, but developers are encouraged to follow the advice of the maintainers of ``pytz`` and move to ``zoneinfo``. collective-icalendar-138c845/docs/how-to/usage.rst000066400000000000000000000460001524134033600220500ustar00rootroot00000000000000===== Usage ===== This chapter describes how to use icalendar. .. note:: To avoid repetition, the code examples in this chapter use the following imports. .. code-block:: pycon >>> from icalendar import Calendar, Event Components ---------- Components are case-insensitive Python dicts. The ``Calendar`` object is a component. The following example shows how to set two properties for it, then display them. .. code-block:: pycon >>> cal = Calendar() >>> cal["dtstart"] = "20050404T080000" >>> cal["summary"] = "Python meeting about calendaring" >>> for k,v in cal.items(): ... k,v ('DTSTART', '20050404T080000') ('SUMMARY', 'Python meeting about calendaring') .. note:: To add components to the calendar, create the :ref:`subcomponent `, then add it via :meth:`icalendar.cal.component.Component.add()`. The example above adds a string, but not a ``vText`` component. You can generate a string for a file with the :meth:`icalendar.cal.component.Component.to_ical` method. .. code-block:: pycon >>> cal.to_ical() b'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n' The rendered view is easier to read. .. code-block:: pycon BEGIN:VCALENDAR DTSTART:20050404T080000 SUMMARY:Python meeting about calendaring END:VCALENDAR You can define a function to display :meth:`~icalendar.cal.component.Component.to_ical` output, as shown in the following example. .. code-block:: pycon >>> def display(cal): ... return cal.to_ical().decode("utf-8").replace("\r\n", "\n").strip() You can set multiple properties, as shown in the following example. .. code-block:: pycon >>> cal = Calendar() >>> cal["attendee"] = ["MAILTO:maxm@mxm.dk","MAILTO:test@example.com"] >>> print(display(cal)) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com END:VCALENDAR If you don't want to care about whether a property value is a list or a single value, use the :meth:`icalendar.cal.component.Component.add()` method. It will automatically convert the property to a list of values if more than one value is added. Here is an example. .. code-block:: pycon >>> cal = Calendar() >>> cal.add("attendee", "MAILTO:maxm@mxm.dk") >>> cal.add("attendee", "MAILTO:test@example.com") >>> print(display(cal)) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com END:VCALENDAR .. note:: This example doesn't check for compliance, so you should look in the :rfc:`5545` specification for legal properties for each component. You can also look in the :file:`icalendar/cal/calendar.py` file, where it is at least defined for each component. .. note:: icalendar automatically handles custom components (X-components and IANA-components) without special configuration. See :doc:`custom-components` for details on working with vendor-specific or non-standard components. .. _subcomponents: Subcomponents ------------- Any component can have subcomponents. For example, inside a calendar, there can be events. They can be arbitrarily nested. To demonstrate, first, make a new component. .. code-block:: pycon >>> event = Event() >>> event["uid"] = "42" >>> event["dtstart"] = "20050404T080000" Then append it to a parent. .. code-block:: pycon >>> cal.add_component(event) >>> print(display(cal)) BEGIN:VCALENDAR ATTENDEE:MAILTO:maxm@mxm.dk ATTENDEE:MAILTO:test@example.com BEGIN:VEVENT DTSTART:20050404T080000 UID:42 END:VEVENT END:VCALENDAR Subcomponents are appended to the subcomponents property on the component. .. code-block:: pycon >>> cal.subcomponents [VEVENT({'UID': '42', 'DTSTART': '20050404T080000'})] Value types ----------- Property values are UTF-8 encoded strings. This is impractical if you want to use the data for further computation. The datetime format, for example, looks like `20050404T080000`. icalendar can parse and generate iCalendar formatted strings. You can either use the :meth:`~icalendar.cal.component.Component.add()` method to do the work, or you can do it manually. To add a datetime value, you can use Python's built in :mod:`datetime` types, and the set the encode parameter to ``True``, and it will convert to the type defined in the specification. .. code-block:: pycon >>> from datetime import datetime >>> cal.add("dtstart", datetime(2005,4,4,8,0,0)) >>> cal["dtstart"].to_ical() b'20050404T080000' If that doesn't work satisfactorily for some reason, you can also do it manually. In :file:`icalendar/prop.py`, all the iCalendar data types are defined. Each type has a class that can parse and encode the type. Thus, to parse it manually, you would do the following. .. code-block:: pycon >>> from icalendar import vDatetime >>> now = datetime(2005,4,4,8,0,0) >>> vDatetime(now).to_ical() b'20050404T080000' To summarize, initialize the object with a Python built in type, then call the :meth:`~icalendar.cal.component.Component.to_ical` method on the object. That will return an iCal-encoded string. You can do it the other way around, too. To parse an encoded string, call the :meth:`~icalendar.cal.component.Component.from_ical` method, and it will return an instance of the corresponding Python type. .. code-block:: pycon >>> vDatetime.from_ical("20050404T080000") datetime.datetime(2005, 4, 4, 8, 0) >>> vDatetime.from_ical("20050404T080000Z") datetime.datetime(2005, 4, 4, 8, 0, tzinfo=ZoneInfo(key='UTC')) You can also choose to use the :meth:`icalendar.cal.component.Component.decoded` method, which will return a decoded value directly. .. code-block:: pycon >>> cal = Calendar() >>> cal.add("dtstart", datetime(2005,4,4,8,0,0)) >>> cal["dtstart"].to_ical() b'20050404T080000' >>> cal.decoded("dtstart") datetime.datetime(2005, 4, 4, 8, 0) Property parameters ------------------- Property parameters are automatically added, depending on the input value. For example, for date or time related properties, the value type and timezone identifier, if applicable, are automatically added. .. code-block:: pycon >>> import zoneinfo >>> event = Event() >>> event.add("dtstart", datetime(2010, 10, 10, 10, 0, 0, ... tzinfo=zoneinfo.ZoneInfo("Europe/Vienna"))) >>> lines = event.to_ical().splitlines() >>> assert ( ... b'DTSTART;TZID=Europe/Vienna:20101010T100000' ... in lines) You can also add arbitrary property parameters by passing a parameters dictionary to the :meth:`~icalendar.cal.component.Component.add()` method as shown. .. code-block:: pycon >>> event = Event() >>> event.add("X-TEST-PROP", "tryout.", ... parameters={"prop1":"val1", "prop2":"val2"}) >>> lines = event.to_ical().splitlines() >>> assert b'X-TEST-PROP;PROP1=val1;PROP2=val2:tryout.' in lines Examples -------- This section describes how to use icalendar's essential features. Read calendar from file ''''''''''''''''''''''' Read a calendar from a local :file:`.ics` file and view its events. .. code-block:: pycon >>> import icalendar >>> from pathlib import Path >>> ics_path = Path("src/icalendar/tests/calendars/example.ics") >>> calendar = icalendar.Calendar.from_ical(ics_path) >>> for event in calendar.events: ... print(event.get("SUMMARY")) New Year's Day Orthodox Christmas International Women's Day Read calendar from URL '''''''''''''''''''''' To read a calendar from a URL, retrieve the data first, then parse it in icalendar. .. code-block:: python from urllib.request import urlopen url = "https://www.gov.uk/bank-holidays/england-and-wales.ics" ics_data = urlopen(url).read() calendar = icalendar.Calendar.from_ical(ics_data) Modify content '''''''''''''' After loading the example file, edit and save it. .. code-block:: pycon >>> calendar.calendar_name = "My Modified Calendar" # modify >>> print(calendar.to_ical()[:121]) # save modification BEGIN:VCALENDAR VERSION:2.0 PRODID:collective/icalendar CALSCALE:GREGORIAN METHOD:PUBLISH NAME:My Modified Calendar iCalendar objects ''''''''''''''''' ``icalendar`` supports the creation and parsing of all kinds of objects in the :rfc:`5545` iCalendar standard. .. seealso:: For a complete list of all supported objects, see the :doc:`icalendar API reference guide <../reference/api/icalendar>`. Events `````` Show events. .. code-block:: pycon >>> icalendar.Event() VEVENT({}) Free/busy times ``````````````` Show free/busy times. .. code-block:: pycon >>> icalendar.FreeBusy() VFREEBUSY({}) To-do items ``````````` Show to-do items. .. code-block:: pycon >>> icalendar.Todo() VTODO({}) Alarms `````` Show alarms. .. code-block:: pycon >>> icalendar.Alarm() VALARM({}) Journal entries ``````````````` Show journal entries. .. code-block:: pycon >>> icalendar.Journal() VJOURNAL({}) timezone implementations '''''''''''''''''''''''' You can localize your events to take place in different timezones. icalendar supports multiple timezone implementations, including :mod:`zoneinfo`, `dateutil.tz `_, and `pytz `_. To demonstrate icalendar's flexibility, the following example creates an event that uses all of the timezone implementations with the same result. .. code-block:: pycon >>> import pytz, zoneinfo, dateutil.tz # timezone libraries >>> import datetime, icalendar >>> e = icalendar.Event() >>> tz = dateutil.tz.tzstr("Europe/London") >>> e["X-DT-DATEUTIL"] = icalendar.vDatetime(datetime.datetime(2024, 6, 19, 10, 1, tzinfo=tz)) >>> tz = pytz.timezone("Europe/London") >>> e["X-DT-USE-PYTZ"] = icalendar.vDatetime(datetime.datetime(2024, 6, 19, 10, 1, tzinfo=tz)) >>> tz = zoneinfo.ZoneInfo("Europe/London") >>> e["X-DT-ZONEINFO"] = icalendar.vDatetime(datetime.datetime(2024, 6, 19, 10, 1, tzinfo=tz)) >>> print(e.to_ical()) # the libraries yield the same result BEGIN:VEVENT X-DT-DATEUTIL;TZID=Europe/London:20240619T100100 X-DT-USE-PYTZ;TZID=Europe/London:20240619T100100 X-DT-ZONEINFO;TZID=Europe/London:20240619T100100 END:VEVENT ``zoneinfo`` default implementation ''''''''''''''''''''''''''''''''''' .. versionchanged:: 6.0.0 Version 6 of icalendar switches the default timezone implementation from ``pytz`` to :mod:`zoneinfo`. This only affects you if you parse icalendar objects with :meth:`from_ical `. The functionality is extended and tested since 6.0.0 with both timezone implementations ``pytz`` and :mod:`zoneinfo`. Since 6.0.0 by default, :mod:`zoneinfo` timezones are created. .. code-block:: pycon >>> dt = icalendar.Calendar.example("timezoned").events[0].start >>> dt.tzinfo ZoneInfo(key='Europe/Vienna') To continue to receive ``pytz`` timezones in parsed results, you can receive all the latest updates, and switch back to earlier behavior. .. code-block:: pycon >>> icalendar.use_pytz() >>> dt = icalendar.Calendar.example("timezoned").events[0].start >>> dt.tzinfo Complete recurring meeting '''''''''''''''''''''''''' This section shows how to create a complete and typical iCalendar file of a recurring meeting event, then add some properties to it, including a location, organizer, attendees, alarms, recurrence, and other properties. This file can be loaded into any application that supports iCalendar files. Initialize the calendar. .. code-block:: pycon >>> cal = Calendar() >>> from datetime import datetime >>> import zoneinfo Add some properties to be compliant with :rfc:`5545`. .. code-block:: pycon >>> cal.add("prodid", "-//My calendar product//mxm.dk//") >>> cal.add("version", "2.0") At least one subcomponent is required for a calendar to be compliant. .. code-block:: pycon >>> event = Event() >>> event.add("summary", "Python meeting about calendaring") >>> event.add("dtstart", datetime(2005,4,4,8,0,0,tzinfo=zoneinfo.ZoneInfo("UTC"))) >>> event.add("dtend", datetime(2005,4,4,10,0,0,tzinfo=zoneinfo.ZoneInfo("UTC"))) >>> event.add("dtstamp", datetime(2005,4,4,0,10,0,tzinfo=zoneinfo.ZoneInfo("UTC"))) Create a property with parameters. Notice that they are an attribute on the value. .. code-block:: pycon >>> from icalendar import vCalAddress, vText >>> organizer = vCalAddress("MAILTO:noone@example.com") Automatic encoding is not yet implemented for parameter values, so you must use the ``v*`` types which you can import from the icalendar :mod:`icalendar.prop` module. .. code-block:: pycon >>> organizer.params["cn"] = vText("Max Rasmussen") >>> organizer.params["role"] = vText("CHAIR") >>> event["organizer"] = organizer >>> event["location"] = vText("Odense, Denmark") >>> event["uid"] = "20050115T101010/27346262376@mxm.dk" >>> event.add("priority", 5) >>> attendee = vCalAddress("MAILTO:maxm@example.com") >>> attendee.params["cn"] = vText("Max Rasmussen") >>> attendee.params["ROLE"] = vText("REQ-PARTICIPANT") >>> event.add("attendee", attendee, encode=0) >>> attendee = vCalAddress("MAILTO:the-dude@example.com") >>> attendee.params["cn"] = vText("The Dude") >>> attendee.params["ROLE"] = vText("REQ-PARTICIPANT") >>> event.add("attendee", attendee, encode=0) Add the event to the calendar. .. code-block:: pycon >>> cal.add_component(event) By extending the event with subcomponents, you can create multiple alarms. .. code-block:: pycon >>> from icalendar import Alarm >>> from datetime import timedelta >>> alarm_1h_before = Alarm() >>> alarm_1h_before.add("action", "DISPLAY") >>> alarm_1h_before.add("trigger", timedelta(hours=-1)) >>> alarm_1h_before.add("description", "Reminder: Event in 1 hour") >>> event.add_component(alarm_1h_before) >>> alarm_24h_before = Alarm() >>> alarm_24h_before.add("action", "DISPLAY") >>> alarm_24h_before.add("trigger", timedelta(hours=-24)) >>> alarm_24h_before.add("description", "Reminder: Event in 24 hours") >>> event.add_component(alarm_24h_before) You can even add a recurrence, either from a dictionary or a string. Note that if you want to add the recurrence rule from a string, you must use the :class:`~icalendar.prop.recur.recur.vRecur` property. Otherwise the rule will be escaped, making it invalid. .. code-block:: pycon >>> event.add("rrule", {"freq": "daily"}) Write to disk. .. code-block:: pycon >>> import tempfile, os >>> directory = tempfile.mkdtemp() >>> f = open(os.path.join(directory, "example.ics"), "wb") >>> f.write(cal.to_ical()) 733 >>> f.close() Print out the calendar. .. code-block:: pycon >>> print(cal.to_ical().decode("utf-8")) # doctest: +NORMALIZE_WHITESPACE BEGIN:VCALENDAR VERSION:2.0 PRODID:-//My calendar product//mxm.dk// BEGIN:VEVENT SUMMARY:Python meeting about calendaring DTSTART:20050404T080000Z DTEND:20050404T100000Z DTSTAMP:20050404T001000Z UID:20050115T101010/27346262376@mxm.dk RRULE:FREQ=DAILY ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com ATTENDEE;CN="The Dude";ROLE=REQ-PARTICIPANT:MAILTO:the-dude@example.com LOCATION:Odense\, Denmark ORGANIZER;CN="Max Rasmussen";ROLE=CHAIR:MAILTO:noone@example.com PRIORITY:5 BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Reminder: Event in 1 hour TRIGGER:-PT1H END:VALARM BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Reminder: Event in 24 hours TRIGGER:-P1D END:VALARM END:VEVENT END:VCALENDAR Examples in source code ----------------------- The docstrings in the :mod:`icalendar` package API documentation provide other usage examples. The `tests `_ of icalendar also have more examples. Miscellaneous examples ---------------------- This section describes how to solve specific usage issues with icalendar. Post your usage question to the `icalendar discussion Q&A category `_. You may :doc:`contribute answered questions to this section of the documentation <../contribute/documentation/index>` through a pull request, too. Convert iCalendar to JSON ''''''''''''''''''''''''' See the :doc:`jcal` documentation for how to convert iCalendar data to JSON. Print event information ''''''''''''''''''''''' To print all events of an iCalendar, iterate through them and print each as shown. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.example() # open a calendar >>> for event in calendar.events: # iterate over all events ... print(event.summary) New Year's Day Orthodox Christmas International Women's Day .. seealso:: - :attr:`Calendar.events ` - :attr:`~icalendar.cal.component.Component.subcomponents` - :meth:`~icalendar.cal.component.Component.walk` Modify specific events '''''''''''''''''''''' To find :attr:`~icalendar.cal.component.Component.subcomponents` that match specific requirements, use :meth:`~icalendar.cal.component.Component.walk`. The following example filters through all subcomponents of the calendar, and if they have a specific word ``Christmas`` in their summary, makes the summary uppercase. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.example() # open a calendar >>> find_christmas = lambda c: "christmas" in c.get("SUMMARY", "").lower() >>> for component in calendar.walk(select=find_christmas): ... component["SUMMARY"] = component["SUMMARY"].upper() >>> print(calendar.events[1].summary) ORTHODOX CHRISTMAS Add timezones to a calendar ''''''''''''''''''''''''''' Dates with times in icalendar can be timezone specific. ``UTC`` is such a timezone. Other examples are ``Europe/Berlin``, ``America/New_York``, and ``Asia/Tokyo``. The following example creates a calendar with an event in ``Europe/Zurich``, and adds all needed timezones just before saving the calendar. .. code-block:: pycon >>> from icalendar import Calendar, Event >>> from datetime import datetime >>> from zoneinfo import ZoneInfo >>> event = Event.new( ... summary="Meeting in Zurich", ... start=datetime(2022, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("Europe/Zurich")), ... end=datetime(2022, 1, 1, 13, 0, 0, tzinfo=ZoneInfo("Europe/Zurich")), ... location="Zurich, Switzerland", ... ) >>> calendar = Calendar.new( ... subcomponents=[event], ... ) >>> calendar.add_missing_timezones() >>> 'Europe/Zurich' in [tz.tz_name for tz in calendar.timezones] True After running :meth:`~icalendar.cal.calendar.Calendar.add_missing_timezones`, the calendar now contains all needed timezones and can be saved as a file with :meth:`~icalendar.cal.component.Component.to_ical`. collective-icalendar-138c845/docs/index.rst000066400000000000000000000050741524134033600206440ustar00rootroot00000000000000======================= icalendar documentation ======================= icalendar is an Internet Calendaring and Scheduling (iCalendar) Python package. icalendar is an :RFC:`5545` compatible parser and generator for iCalendar files. icalendar lets you create, inspect, and modify calendaring information with Python. .. grid:: 1 1 2 2 :gutter: 3 3 4 5 .. grid-item-card:: :link: how-to/index :link-type: doc :octicon:`zap;1.5em;sd-text-info` Get started ^^^ This part of the documentation describes how to get started with icalendar, including how to install, upgrade, and use icalendar. +++ .. button-ref:: how-to/index :color: primary :expand: Go to How to guides .. grid-item-card:: :link: tutorials/index :link-type: doc :octicon:`mortar-board;1.5em;sd-text-info` Tutorials ^^^ This part of the documentation contains lessons that guide you through various iCalendar concepts step by step. +++ .. button-ref:: tutorials/index :color: primary :expand: Go to Tutorials .. grid-item-card:: :link: reference/index :link-type: doc :octicon:`bookmark;1.5em;sd-text-info` Reference guide ^^^ This part of the documentation is a reference guide, including API, design architecture, and change log. +++ .. button-ref:: reference/index :ref-type: doc :color: primary :expand: Go to Reference guide .. grid-item-card:: :link: contribute/index :link-type: doc :octicon:`heart;1.5em;sd-text-info` Contributing guide ^^^ This part of the documentation describes how to contribute to icalendar. +++ .. button-ref:: contribute/index :ref-type: doc :color: primary :expand: Go to Contributing guide .. toctree:: :caption: How to guides :maxdepth: 2 :hidden: how-to/index .. toctree:: :caption: Tutorials :maxdepth: 2 :hidden: tutorials/index .. toctree:: :caption: Explanation :maxdepth: 2 :hidden: explanation/index .. toctree:: :caption: Reference :hidden: reference/index .. toctree:: :caption: Contribute :hidden: contribute/index - :doc:`reference/glossary` - :ref:`genindex` License ------- See `LICENSE.rst `_. collective-icalendar-138c845/docs/reference/000077500000000000000000000000001524134033600207335ustar00rootroot00000000000000collective-icalendar-138c845/docs/reference/api/000077500000000000000000000000001524134033600215045ustar00rootroot00000000000000collective-icalendar-138c845/docs/reference/api/icalendar.alarms.rst000066400000000000000000000002371524134033600254400ustar00rootroot00000000000000icalendar.alarms module ======================= .. automodule:: icalendar.alarms :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.attr.rst000066400000000000000000000002311524134033600251250ustar00rootroot00000000000000icalendar.attr module ===================== .. automodule:: icalendar.attr :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.alarm.rst000066400000000000000000000002501524134033600260060ustar00rootroot00000000000000icalendar.cal.alarm module ========================== .. automodule:: icalendar.cal.alarm :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.availability.rst000066400000000000000000000002751524134033600273730ustar00rootroot00000000000000icalendar.cal.availability module ================================= .. automodule:: icalendar.cal.availability :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.available.rst000066400000000000000000000002641524134033600266370ustar00rootroot00000000000000icalendar.cal.available module ============================== .. automodule:: icalendar.cal.available :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.calendar.rst000066400000000000000000000002611524134033600264650ustar00rootroot00000000000000icalendar.cal.calendar module ============================= .. automodule:: icalendar.cal.calendar :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.component.rst000066400000000000000000000002641524134033600267210ustar00rootroot00000000000000icalendar.cal.component module ============================== .. automodule:: icalendar.cal.component :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.component_factory.rst000066400000000000000000000003161524134033600304460ustar00rootroot00000000000000icalendar.cal.component\_factory module ======================================= .. automodule:: icalendar.cal.component_factory :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.event.rst000066400000000000000000000002501524134033600260330ustar00rootroot00000000000000icalendar.cal.event module ========================== .. automodule:: icalendar.cal.event :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.examples.rst000066400000000000000000000002611524134033600265320ustar00rootroot00000000000000icalendar.cal.examples module ============================= .. automodule:: icalendar.cal.examples :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.free_busy.rst000066400000000000000000000002661524134033600267040ustar00rootroot00000000000000icalendar.cal.free\_busy module =============================== .. automodule:: icalendar.cal.free_busy :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.journal.rst000066400000000000000000000002561524134033600263720ustar00rootroot00000000000000icalendar.cal.journal module ============================ .. automodule:: icalendar.cal.journal :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.lazy.rst000066400000000000000000000002451524134033600256750ustar00rootroot00000000000000icalendar.cal.lazy module ========================= .. automodule:: icalendar.cal.lazy :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.rst000066400000000000000000000010531524134033600247150ustar00rootroot00000000000000icalendar.cal package ===================== Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.cal.alarm icalendar.cal.availability icalendar.cal.available icalendar.cal.calendar icalendar.cal.component icalendar.cal.component_factory icalendar.cal.event icalendar.cal.examples icalendar.cal.free_busy icalendar.cal.journal icalendar.cal.lazy icalendar.cal.timezone icalendar.cal.todo Module contents --------------- .. automodule:: icalendar.cal :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.timezone.rst000066400000000000000000000002611524134033600265460ustar00rootroot00000000000000icalendar.cal.timezone module ============================= .. automodule:: icalendar.cal.timezone :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cal.todo.rst000066400000000000000000000002451524134033600256630ustar00rootroot00000000000000icalendar.cal.todo module ========================= .. automodule:: icalendar.cal.todo :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.caselessdict.rst000066400000000000000000000002611524134033600266240ustar00rootroot00000000000000icalendar.caselessdict module ============================= .. automodule:: icalendar.caselessdict :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.cli.rst000066400000000000000000000002261524134033600247260ustar00rootroot00000000000000icalendar.cli module ==================== .. automodule:: icalendar.cli :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.compatibility.rst000066400000000000000000000002641524134033600270320ustar00rootroot00000000000000icalendar.compatibility module ============================== .. automodule:: icalendar.compatibility :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.config.rst000066400000000000000000000002371524134033600254260ustar00rootroot00000000000000icalendar.config module ======================= .. automodule:: icalendar.config :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.enums.rst000066400000000000000000000002341524134033600253050ustar00rootroot00000000000000icalendar.enums module ====================== .. automodule:: icalendar.enums :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.error.rst000066400000000000000000000002341524134033600253070ustar00rootroot00000000000000icalendar.error module ====================== .. automodule:: icalendar.error :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.param.rst000066400000000000000000000002341524134033600252560ustar00rootroot00000000000000icalendar.param module ====================== .. automodule:: icalendar.param :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.content_line.rst000066400000000000000000000003101524134033600301250ustar00rootroot00000000000000icalendar.parser.content\_line module ===================================== .. automodule:: icalendar.parser.content_line :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.ical.calendar.rst000066400000000000000000000003111524134033600301250ustar00rootroot00000000000000icalendar.parser.ical.calendar module ===================================== .. automodule:: icalendar.parser.ical.calendar :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.ical.component.rst000066400000000000000000000003141524134033600303610ustar00rootroot00000000000000icalendar.parser.ical.component module ====================================== .. automodule:: icalendar.parser.ical.component :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.ical.lazy.rst000066400000000000000000000002751524134033600273440ustar00rootroot00000000000000icalendar.parser.ical.lazy module ================================= .. automodule:: icalendar.parser.ical.lazy :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.ical.rst000066400000000000000000000005231524134033600263620ustar00rootroot00000000000000icalendar.parser.ical package ============================= Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.parser.ical.calendar icalendar.parser.ical.component icalendar.parser.ical.lazy Module contents --------------- .. automodule:: icalendar.parser.ical :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.parameter.rst000066400000000000000000000002751524134033600274360ustar00rootroot00000000000000icalendar.parser.parameter module ================================= .. automodule:: icalendar.parser.parameter :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.property.rst000066400000000000000000000002721524134033600273370ustar00rootroot00000000000000icalendar.parser.property module ================================ .. automodule:: icalendar.parser.property :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.rst000066400000000000000000000006511524134033600254550ustar00rootroot00000000000000icalendar.parser package ======================== Subpackages ----------- .. toctree:: :maxdepth: 4 icalendar.parser.ical Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.parser.content_line icalendar.parser.parameter icalendar.parser.property icalendar.parser.string Module contents --------------- .. automodule:: icalendar.parser :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser.string.rst000066400000000000000000000002641524134033600267620ustar00rootroot00000000000000icalendar.parser.string module ============================== .. automodule:: icalendar.parser.string :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.parser_tools.rst000066400000000000000000000002631524134033600266740ustar00rootroot00000000000000icalendar.parser\_tools module ============================== .. automodule:: icalendar.parser_tools :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.adr.rst000066400000000000000000000002451524134033600257050ustar00rootroot00000000000000icalendar.prop.adr module ========================= .. automodule:: icalendar.prop.adr :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.binary.rst000066400000000000000000000002561524134033600264250ustar00rootroot00000000000000icalendar.prop.binary module ============================ .. automodule:: icalendar.prop.binary :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.boolean.rst000066400000000000000000000002611524134033600265540ustar00rootroot00000000000000icalendar.prop.boolean module ============================= .. automodule:: icalendar.prop.boolean :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.broken.rst000066400000000000000000000002561524134033600264210ustar00rootroot00000000000000icalendar.prop.broken module ============================ .. automodule:: icalendar.prop.broken :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.cal_address.rst000066400000000000000000000002771524134033600274100ustar00rootroot00000000000000icalendar.prop.cal\_address module ================================== .. automodule:: icalendar.prop.cal_address :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.categories.rst000066400000000000000000000002721524134033600272640ustar00rootroot00000000000000icalendar.prop.categories module ================================ .. automodule:: icalendar.prop.categories :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.conference.rst000066400000000000000000000002721524134033600272460ustar00rootroot00000000000000icalendar.prop.conference module ================================ .. automodule:: icalendar.prop.conference :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.base.rst000066400000000000000000000002611524134033600264550ustar00rootroot00000000000000icalendar.prop.dt.base module ============================= .. automodule:: icalendar.prop.dt.base :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.date.rst000066400000000000000000000002611524134033600264600ustar00rootroot00000000000000icalendar.prop.dt.date module ============================= .. automodule:: icalendar.prop.dt.date :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.datetime.rst000066400000000000000000000002751524134033600273440ustar00rootroot00000000000000icalendar.prop.dt.datetime module ================================= .. automodule:: icalendar.prop.dt.datetime :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.duration.rst000066400000000000000000000002751524134033600273750ustar00rootroot00000000000000icalendar.prop.dt.duration module ================================= .. automodule:: icalendar.prop.dt.duration :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.list.rst000066400000000000000000000002611524134033600265160ustar00rootroot00000000000000icalendar.prop.dt.list module ============================= .. automodule:: icalendar.prop.dt.list :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.period.rst000066400000000000000000000002671524134033600270330ustar00rootroot00000000000000icalendar.prop.dt.period module =============================== .. automodule:: icalendar.prop.dt.period :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.rst000066400000000000000000000007371524134033600255540ustar00rootroot00000000000000icalendar.prop.dt package ========================= Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.prop.dt.base icalendar.prop.dt.date icalendar.prop.dt.datetime icalendar.prop.dt.duration icalendar.prop.dt.list icalendar.prop.dt.period icalendar.prop.dt.time icalendar.prop.dt.types icalendar.prop.dt.utc_offset Module contents --------------- .. automodule:: icalendar.prop.dt :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.time.rst000066400000000000000000000002611524134033600265010ustar00rootroot00000000000000icalendar.prop.dt.time module ============================= .. automodule:: icalendar.prop.dt.time :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.types.rst000066400000000000000000000002641524134033600267120ustar00rootroot00000000000000icalendar.prop.dt.types module ============================== .. automodule:: icalendar.prop.dt.types :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.dt.utc_offset.rst000066400000000000000000000003051524134033600277030ustar00rootroot00000000000000icalendar.prop.dt.utc\_offset module ==================================== .. automodule:: icalendar.prop.dt.utc_offset :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.factory.rst000066400000000000000000000002611524134033600266040ustar00rootroot00000000000000icalendar.prop.factory module ============================= .. automodule:: icalendar.prop.factory :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.float.rst000066400000000000000000000002531524134033600262430ustar00rootroot00000000000000icalendar.prop.float module =========================== .. automodule:: icalendar.prop.float :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.geo.rst000066400000000000000000000002451524134033600257110ustar00rootroot00000000000000icalendar.prop.geo module ========================= .. automodule:: icalendar.prop.geo :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.image.rst000066400000000000000000000002531524134033600262200ustar00rootroot00000000000000icalendar.prop.image module =========================== .. automodule:: icalendar.prop.image :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.inline.rst000066400000000000000000000002561524134033600264170ustar00rootroot00000000000000icalendar.prop.inline module ============================ .. automodule:: icalendar.prop.inline :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.integer.rst000066400000000000000000000002611524134033600265720ustar00rootroot00000000000000icalendar.prop.integer module ============================= .. automodule:: icalendar.prop.integer :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.n.rst000066400000000000000000000002371524134033600253750ustar00rootroot00000000000000icalendar.prop.n module ======================= .. automodule:: icalendar.prop.n :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.org.rst000066400000000000000000000002451524134033600257260ustar00rootroot00000000000000icalendar.prop.org module ========================= .. automodule:: icalendar.prop.org :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.frequency.rst000066400000000000000000000003111524134033600302510ustar00rootroot00000000000000icalendar.prop.recur.frequency module ===================================== .. automodule:: icalendar.prop.recur.frequency :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.month.rst000066400000000000000000000002751524134033600274060ustar00rootroot00000000000000icalendar.prop.recur.month module ================================= .. automodule:: icalendar.prop.recur.month :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.recur.rst000066400000000000000000000002751524134033600274010ustar00rootroot00000000000000icalendar.prop.recur.recur module ================================= .. automodule:: icalendar.prop.recur.recur :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.rst000066400000000000000000000006101524134033600262530ustar00rootroot00000000000000icalendar.prop.recur package ============================ Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.prop.recur.frequency icalendar.prop.recur.month icalendar.prop.recur.recur icalendar.prop.recur.skip icalendar.prop.recur.weekday Module contents --------------- .. automodule:: icalendar.prop.recur :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.skip.rst000066400000000000000000000002721524134033600272240ustar00rootroot00000000000000icalendar.prop.recur.skip module ================================ .. automodule:: icalendar.prop.recur.skip :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.recur.weekday.rst000066400000000000000000000003031524134033600277020ustar00rootroot00000000000000icalendar.prop.recur.weekday module =================================== .. automodule:: icalendar.prop.recur.weekday :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.rst000066400000000000000000000014641524134033600251440ustar00rootroot00000000000000icalendar.prop package ====================== Subpackages ----------- .. toctree:: :maxdepth: 4 icalendar.prop.dt icalendar.prop.recur Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.prop.adr icalendar.prop.binary icalendar.prop.boolean icalendar.prop.broken icalendar.prop.cal_address icalendar.prop.categories icalendar.prop.conference icalendar.prop.factory icalendar.prop.float icalendar.prop.geo icalendar.prop.image icalendar.prop.inline icalendar.prop.integer icalendar.prop.n icalendar.prop.org icalendar.prop.text icalendar.prop.uid icalendar.prop.unknown icalendar.prop.uri icalendar.prop.xml_reference Module contents --------------- .. automodule:: icalendar.prop :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.text.rst000066400000000000000000000002501524134033600261170ustar00rootroot00000000000000icalendar.prop.text module ========================== .. automodule:: icalendar.prop.text :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.uid.rst000066400000000000000000000002451524134033600257200ustar00rootroot00000000000000icalendar.prop.uid module ========================= .. automodule:: icalendar.prop.uid :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.unknown.rst000066400000000000000000000002611524134033600266340ustar00rootroot00000000000000icalendar.prop.unknown module ============================= .. automodule:: icalendar.prop.unknown :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.uri.rst000066400000000000000000000002451524134033600257360ustar00rootroot00000000000000icalendar.prop.uri module ========================= .. automodule:: icalendar.prop.uri :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.prop.xml_reference.rst000066400000000000000000000003051524134033600277520ustar00rootroot00000000000000icalendar.prop.xml\_reference module ==================================== .. automodule:: icalendar.prop.xml_reference :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.rst000066400000000000000000000011141524134033600241550ustar00rootroot00000000000000icalendar package ================= Subpackages ----------- .. toctree:: :maxdepth: 4 icalendar.cal icalendar.parser icalendar.prop icalendar.timezone Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.alarms icalendar.attr icalendar.caselessdict icalendar.cli icalendar.compatibility icalendar.config icalendar.enums icalendar.error icalendar.param icalendar.parser_tools icalendar.tools icalendar.version Module contents --------------- .. automodule:: icalendar :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.equivalent_timezone_ids.rst000066400000000000000000000003611524134033600327360ustar00rootroot00000000000000icalendar.timezone.equivalent\_timezone\_ids module =================================================== .. automodule:: icalendar.timezone.equivalent_timezone_ids :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.provider.rst000066400000000000000000000003001524134033600276330ustar00rootroot00000000000000icalendar.timezone.provider module ================================== .. automodule:: icalendar.timezone.provider :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.pytz.rst000066400000000000000000000002641524134033600270200ustar00rootroot00000000000000icalendar.timezone.pytz module ============================== .. automodule:: icalendar.timezone.pytz :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.rst000066400000000000000000000007121524134033600260110ustar00rootroot00000000000000icalendar.timezone package ========================== Submodules ---------- .. toctree:: :maxdepth: 4 icalendar.timezone.equivalent_timezone_ids icalendar.timezone.provider icalendar.timezone.pytz icalendar.timezone.tzid icalendar.timezone.tzp icalendar.timezone.windows_to_olson icalendar.timezone.zoneinfo Module contents --------------- .. automodule:: icalendar.timezone :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.tzid.rst000066400000000000000000000002641524134033600267640ustar00rootroot00000000000000icalendar.timezone.tzid module ============================== .. automodule:: icalendar.timezone.tzid :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.tzp.rst000066400000000000000000000002611524134033600266240ustar00rootroot00000000000000icalendar.timezone.tzp module ============================= .. automodule:: icalendar.timezone.tzp :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.windows_to_olson.rst000066400000000000000000000003341524134033600314160ustar00rootroot00000000000000icalendar.timezone.windows\_to\_olson module ============================================ .. automodule:: icalendar.timezone.windows_to_olson :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.timezone.zoneinfo.rst000066400000000000000000000003001524134033600276300ustar00rootroot00000000000000icalendar.timezone.zoneinfo module ================================== .. automodule:: icalendar.timezone.zoneinfo :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.tools.rst000066400000000000000000000002341524134033600253160ustar00rootroot00000000000000icalendar.tools module ====================== .. automodule:: icalendar.tools :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/icalendar.version.rst000066400000000000000000000002421524134033600256420ustar00rootroot00000000000000icalendar.version module ======================== .. automodule:: icalendar.version :ignore-module-all: :members: :show-inheritance: :undoc-members: collective-icalendar-138c845/docs/reference/api/modules.rst000066400000000000000000000001001524134033600236750ustar00rootroot00000000000000icalendar ========= .. toctree:: :maxdepth: 4 icalendar collective-icalendar-138c845/docs/reference/changelog.rst000066400000000000000000000000371524134033600234140ustar00rootroot00000000000000.. include:: ../../CHANGES.rst collective-icalendar-138c845/docs/reference/glossary.rst000066400000000000000000000035131524134033600233320ustar00rootroot00000000000000======== Glossary ======== .. glossary:: :sorted: icalendar This Python package, with a lower-case "c", used for parsing and generating :term:`iCalendar` files following the standard in :rfc:`5545`. iCalendar The RFC called Internet Calendaring and Scheduling Core Object Specification (iCalendar), with an upper-case "C". Files that follow the iCalendar specification are called iCalendar files. Vale `Vale `_ is an open-source, command-line tool that helps maintain a consistent and on-brand voice in documentation. icalendar documentation uses it to check spelling, English grammar and syntax, and style guides. properties property A property in an iCalendar file is the definition of an individual attribute describing a calendar object or a calendar :term:`component`. A property takes the form defined by the "contentline" notation defined in :rfc:`5545#section-3.1`. The body of an iCalendar file consists of a sequence of calendar properties and one or more calendar components. .. seealso:: :rfc:`5545#section-3.5` components component The body of the iCalendar object consists of a sequence of calendar :term:`properties` and one or more calendar components. The calendar properties are attributes that apply to the calendar object as a whole. The calendar components are collections of properties that express a particular calendar semantic. For example, the calendar component can specify an event, a to-do, a journal entry, time zone information, free/busy time information, or an alarm. All calendar components start with the letter "V". For example, "VEVENT" refers to the event calendar component. .. seealso:: :rfc:`5545#section-3.6` collective-icalendar-138c845/docs/reference/index.rst000066400000000000000000000006061524134033600225760ustar00rootroot00000000000000================ Reference guides ================ Design reference ================ .. toctree:: :maxdepth: 2 rfc-support versions-branches Change log history ================== .. toctree:: :maxdepth: 1 changelog Glossary of terms ================= .. toctree:: glossary API reference ============= .. toctree:: :maxdepth: 2 api/icalendar collective-icalendar-138c845/docs/reference/rfc-support.rst000066400000000000000000000022051524134033600237500ustar00rootroot00000000000000=========== RFC support =========== icalendar is used for parsing and generating iCalendar files following the standards in several RFCs. Support for specific RFCs, or no claim of support, is described in the following sections. Supported RFCs -------------- icalendar supports the following RFC standards. :rfc:`2445` obsoleted by :rfc:`5545` :rfc:`5545` Internet Calendaring and Scheduling Core Object Specification (iCalendar) :rfc:`6868` Parameter Value Encoding in iCalendar and vCard :rfc:`7529` Non-Gregorian Recurrence Rules in the Internet Calendaring and Scheduling Core Object Specification (iCalendar) :rfc:`9074` "VALARM" Extensions for iCalendar :rfc:`7953` Calendar Availability :rfc:`7986` New Properties for iCalendar :rfc:`9253` Support for iCalendar Relationships Unsupported RFCs ---------------- The maintainers of icalendar do not claim compatibility with the following RFCs. :rfc:`6321` xCal: The XML Format for iCalendar. See :issue:`1135`. :rfc:`6350` vCard 4.0. See :issue:`859`. :rfc:`9073` Event Publishing Extensions to iCalendar. This might work partially. See :issue:`656`. collective-icalendar-138c845/docs/reference/versions-branches.rst000066400000000000000000000043101524134033600251160ustar00rootroot00000000000000Versions and branches ===================== This chapter is a reference guide for icalendar's versioning and branch policies. Semantic versioning ------------------- We use `Semantic Versioning `_. - Breaking changes increase the **major** version number. - New features increase the **minor** version number. - Minor changes and bug fixes increase the **patch** version number. - Stable release numbers consist of three numbers separated by two dots, such as 7.0.0 or 6.3.12. - Unstable releases are denoted by ``a``, ``b``, or ``rc``, such as ``7.0.0a1``. .. _branch-policy: Branches -------- This section describes the branches used in icalendar development. ``main`` The `main `_ branch receives the latest updates and features. Active development takes place on this branch. It's compatible with Python versions 3.10 - 3.14, and PyPy3.10, subject to change because it's an active development branch. ``7.x`` icalendar version 7 is on the branch `7.x `_. It's compatible with Python versions 3.10 - 3.14, and PyPy3.10. Security updates, bug fixes, and new features that are not breaking changes are continuously backported from `main` and released on this branch. ``6.x`` icalendar version 6 is on the branch `6.x `_. It is compatible with Python versions 3.8 - 3.13, and PyPy3.9. Security updates and bug fixes can be backported and added to ``6.x`` on request. ``5.x`` icalendar version 5 is on the branch `5.x `_. It is compatible with Python versions 3.7 - 3.11, and PyPy3.9. Security updates and bug fixes can be backported and added to ``5.x`` on request. ``4.x`` icalendar version 4 is on the branch `4.x `_. It is compatible with Python versions 2.7, 3.4 - 3.10, and PyPy2.7 and PyPy3.9. Security updates and bug fixes can be backported and added to ``4.x`` on request. .. seealso:: `icalendar security policy `_. collective-icalendar-138c845/docs/styles/000077500000000000000000000000001524134033600203205ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/000077500000000000000000000000001524134033600215655ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/vocabularies/000077500000000000000000000000001524134033600242445ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/vocabularies/Base/000077500000000000000000000000001524134033600251165ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/vocabularies/Base/accept.txt000066400000000000000000000000101524134033600271050ustar00rootroot00000000000000inviter collective-icalendar-138c845/docs/styles/config/vocabularies/Base/reject.txt000066400000000000000000000000001524134033600271210ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/vocabularies/icalendar/000077500000000000000000000000001524134033600261665ustar00rootroot00000000000000collective-icalendar-138c845/docs/styles/config/vocabularies/icalendar/accept.txt000066400000000000000000000005561524134033600301740ustar00rootroot00000000000000Args backported (?i)datetime dicts [Dd]ocstrings? fuzzer Hacktoberfest Hanoka \.ical iCal (?i)icalendar jCal Kunzmann Napolean Nicco Params Penner (?i)Plone Repology recur.recur reformats RFCs? SARIF sphinx_copybutton sphinx_design sphinx_reredirects src (?i)subcomponents? (?i)subpackages? subtyping towncrier (?i)Traceback (?i)unescaped uv vCard xCal Xcode zizmor collective-icalendar-138c845/docs/styles/config/vocabularies/icalendar/reject.txt000066400000000000000000000000171524134033600302010ustar00rootroot00000000000000[Dd]ate-times? collective-icalendar-138c845/docs/tutorials/000077500000000000000000000000001524134033600210235ustar00rootroot00000000000000collective-icalendar-138c845/docs/tutorials/create-event-with-attendees.rst000066400000000000000000000161621524134033600270700ustar00rootroot00000000000000=========================== Create event with attendees =========================== In this tutorial, you'll create an iCalendar file with events and attendees. Creating a calendar ------------------- First, you create a :class:`~icalendar.cal.calendar.Calendar`, the highest component type which can contain one or more subcomponents. The Calendar component itself contains its own properties. When you create one using the :meth:`~icalendar.cal.calendar.Calendar.new()` method, it automatically sets required and recommended attributes that apply to the iCalendar object as a whole, as well as any optional ones given as arguments. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.new() After creating a new calendar, you can view its properties by using :meth:`~icalendar.cal.component.Component.to_ical()`, which generates a bytes object of the component, and :meth:`~bytes.decode`, which converts the output to Unicode, making it easier to read. .. code-block:: pycon >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//7.0.0//EN UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VCALENDAR Notice that by default a calendar contains the version, product identifier (``PRODID``), and a unique identifier (``UID``). All these properties are required for a calendar. You can also set and change these properties. .. code-block:: pycon >>> calendar.prodid = "-//icalendar//example.com//EN" Here you change the product identifier and print the revised component. The output confirms the change: .. code-block:: pycon >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar//example.com//EN UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VCALENDAR Adding an event --------------- Now that you have a base calendar, you can add an event to it. You can similarly create an :class:`~icalendar.cal.event.Event` subcomponent using the :meth:`~icalendar.cal.event.Event.new()` method, which includes appropriate defaults. You can also set these properties on creation. .. code-block:: pycon >>> from icalendar import Event >>> import datetime as dt >>> import zoneinfo >>> >>> event = Event.new( ... start=dt.datetime(2026, 3, 21, 6, 30, 0, tzinfo=zoneinfo.ZoneInfo("UTC")), ... end=dt.datetime(2026, 3, 21, 7, 30, 0, tzinfo=zoneinfo.ZoneInfo("UTC")), ... ) This creates an event with a start and end date and time defined using :class:`~datetime.datetime` with a timezone. The output shows your new event: .. code-block:: pycon >>> print(event.to_ical().decode()) BEGIN:VEVENT DTSTART:20260321T063000Z DTEND:20260321T073000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT Notice that the event component includes the start and end dates with their times (``DTSTART`` and ``DTEND``) along with a unique identifier (``UID``), and its creation timestamp (``DTSTAMP``). Similar to the calendar event, you can edit these properties or add new ones. .. code-block:: pycon >>> event.summary = "Pick up bicycle from the workshop." Using the :meth:`~icalendar.cal.component.Component.add()` method on a component, you can add new properties with their name and value. The summary property represents the title of an event. To use an event, you must add it to the calendar. .. code-block:: pycon >>> calendar.add_component(event) You can add the newly created event to the previously created calendar using :meth:`~icalendar.cal.component.Component.add_component()`. Now print the calendar to verify everything that it contains: .. code-block:: pycon >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar//example.com//EN UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 BEGIN:VEVENT SUMMARY:Pick up bicycle from the workshop. DTSTART:20260321T063000Z DTEND:20260321T073000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT END:VCALENDAR You can now see your calendar with the event nested inside. Creating events with attendees ------------------------------ Next, create a second event in your calendar that includes attendees. .. code-block:: pycon >>> from icalendar import vCalAddress >>> ride_event = Event.new( ... start=dt.datetime(2026, 3, 28, 7, 00, 0, tzinfo=zoneinfo.ZoneInfo("UTC")), ... end=dt.datetime(2026, 3, 28, 13, 30, 0, tzinfo=zoneinfo.ZoneInfo("UTC")), ... summary="Morning ride with the team.", ... attendees=[ ... vCalAddress("mailto:me@example.com"), ... vCalAddress("mailto:another_friend@example.com"), ... ] ... ) You've now created an event with a set start and end date and time, including a timezone, the event has a title, and lists two attendees by their email address. The output confirms your additions: .. code-block:: pycon >>> print(ride_event.to_ical().decode()) BEGIN:VEVENT SUMMARY:Morning ride with the team. DTSTART:20260328T070000Z DTEND:20260328T133000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 ATTENDEE:mailto:me@example.com ATTENDEE:mailto:another_friend@example.com END:VEVENT You can see that both attendees were added to the event, along with the custom start and end timestamps. You can also update the list of attendees after you created the event to add new attendees. .. code-block:: pycon >>> ride_event.attendees.append(vCalAddress("mailto:late_joiner@example.com")) Lastly, add this event to the same calendar you created in the beginning. .. code-block:: pycon >>> calendar.add_component(ride_event) Now print the calendar to view everything you've added so far, this should include the calendar component with two events. .. code-block:: pycon >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar//example.com//EN UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 BEGIN:VEVENT SUMMARY:Pick up bicycle from the workshop. DTSTART:20260321T063000Z DTEND:20260321T073000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT BEGIN:VEVENT SUMMARY:Morning ride with the team. DTSTART:20260328T070000Z DTEND:20260328T133000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 ATTENDEE:mailto:me@example.com ATTENDEE:mailto:another_friend@example.com ATTENDEE:mailto:late_joiner@example.com END:VEVENT END:VCALENDAR Creating an .ics file --------------------- Now that you have finished creating your calendar, you can write it to a file. .. code-block:: pycon >>> from pathlib import Path >>> path = Path("example.ics") >>> path.write_bytes(calendar.to_ical()) 611 This creates a new file called :download:`example.ics` and writes the bytes object returned by :meth:`~icalendar.cal.component.Component.to_ical()` to the file. Look on your file system for the file, local to where you issued commands. Its contents should be the following. .. literalinclude:: example.ics :language: ics collective-icalendar-138c845/docs/tutorials/example.ics000066400000000000000000000011431524134033600231550ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar//example.com//EN UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 BEGIN:VEVENT SUMMARY:Pick up bicycle from the workshop. DTSTART:20260321T063000Z DTEND:20260321T073000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT BEGIN:VEVENT SUMMARY:Morning ride with the team. DTSTART:20260328T070000Z DTEND:20260328T133000Z DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 ATTENDEE:mailto:me@example.com ATTENDEE:mailto:another_friend@example.com ATTENDEE:mailto:late_joiner@example.com END:VEVENT END:VCALENDAR collective-icalendar-138c845/docs/tutorials/index.rst000066400000000000000000000001371524134033600226650ustar00rootroot00000000000000========= Tutorials ========= .. toctree:: :maxdepth: 2 create-event-with-attendees collective-icalendar-138c845/funding.json000066400000000000000000000077551524134033600204100ustar00rootroot00000000000000{ "$schema": "https://fundingjson.org/schema/v1.1.0.json", "version": "v1.1.0", "entity": { "type": "group", "role": "maintainer", "name": "icalendar developer community", "email": "icalendar-coc@googlegroups.com", "description": "We support the development of the icalendar library for Python, an RFC 5545 compatible parser and generator of iCalendar files.", "webpageUrl": { "url": "https://github.com/collective/icalendar/", "wellKnown": "https://github.com/collective/icalendar/raw/main/.well-known/funding-manifest-urls" } }, "projects": [ { "guid": "python-icalendar", "name": "icalendar", "description": "The icalendar package is an RFC 5545 compatible parser and generator of iCalendar files.", "webpageUrl": { "url": "https://github.com/collective/icalendar/", "wellKnown": "https://github.com/collective/icalendar/raw/main/.well-known/funding-manifest-urls" }, "repositoryUrl": { "url": "https://github.com/collective/icalendar/", "wellKnown": "https://github.com/collective/icalendar/raw/main/.well-known/funding-manifest-urls" }, "licenses": [ "spdx:BSD-2-Clause" ], "tags": [ "icalendar", "rfc5545", "parser", "generator", "programming", "python" ] } ], "funding": { "channels": [ { "guid": "icalendar-open-source-collective", "type": "payment-provider", "address": "https://opencollective.com/python-icalendar", "description": "Through Open Collective, icalendar accepts payment via credit card, Google Pay, or US bank ACH transfer." } ], "plans": [ { "guid": "fund-once-any-amount-usd", "status": "active", "name": "One-time donation", "description": "Choose any amount to give to icalendar.", "amount": 0, "currency": "USD", "frequency": "one-time", "channels": [ "icalendar-open-source-collective" ] }, { "guid": "fund-monthly-any-amount-usd", "status": "active", "name": "Monthly donation", "description": "Choose any amount to give to icalendar.", "amount": 0, "currency": "USD", "frequency": "monthly", "channels": [ "icalendar-open-source-collective" ] }, { "guid": "fund-monthly-5-usd", "status": "active", "name": "Backer", "description": "Support icalendar with a monthly donation of $5 USD.", "amount": 5, "currency": "USD", "frequency": "monthly", "channels": [ "icalendar-open-source-collective" ] }, { "guid": "fund-monthly-100-usd", "status": "active", "name": "Sponsor", "description": "Support icalendar with a monthly donation of $100 USD.", "amount": 100, "currency": "USD", "frequency": "monthly", "channels": [ "icalendar-open-source-collective" ] } ], "history": [ { "year": 2024, "income": 202, "currency": "USD" }, { "year": 2025, "income": 12, "currency": "USD" } ] } } collective-icalendar-138c845/generate_windows_to_olson_mapping.py000066400000000000000000000054601524134033600254170ustar00rootroot00000000000000"""Generate the Windows to Olson timezone mapping module windows_to_olson.py.""" # Source - https://stackoverflow.com/a/16157049 # Posted by unutbu, modified by community. See post 'Timeline' for change history # Retrieved 2026-07-16, License - CC BY-SA 3.0 # Source - https://stackoverflow.com/a/10469748 # Posted by mzjn, modified by community. See post 'Timeline' for change history # Retrieved 2026-07-16, License - CC BY-SA 3.0 import json import re import urllib.request as req import xml.etree.ElementTree as ET from pathlib import Path CLDR_PATH = "common/supplemental/windowsZones.xml" # GitHub returns commits newest-first by default; [0] is the latest commit # touching this file. commits_url = f"https://api.github.com/repos/unicode-org/cldr/commits?path={CLDR_PATH}&sha=main&per_page=1" with req.urlopen(commits_url) as response: # noqa: S310 commit = json.load(response)[0] version = commit["sha"] version_date = commit["commit"]["committer"]["date"][:10] if re.fullmatch(r"[0-9a-f]{40}", version) is None: raise ValueError(f"Invalid CLDR commit SHA: {version!r}") if re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", version_date) is None: raise ValueError(f"Invalid CLDR commit date: {version_date!r}") # Fetch the XML from the commit recorded in the generated module. Using the # commit instead of `main` ensures that the data and version cannot diverge if # CLDR changes between the two requests. url = f"https://raw.githubusercontent.com/unicode-org/cldr/{version}/{CLDR_PATH}" with req.urlopen(url) as response: # noqa: S310 xml_content = response.read() # Parse the XML file and extract the timezone mapping. result = {} tree = ET.fromstring(xml_content) # noqa: S314 for zone in tree.findall(".//mapZone"): attrib = zone.attrib if attrib["territory"] == "001": result[attrib["other"]] = attrib["type"] HERE = Path(__file__).parent DEST = HERE / "src" / "icalendar" / "timezone" file = "windows_to_olson.py" filepath = Path(DEST, file) print(f"Writing {filepath}") # noqa: T201 with Path.open(filepath, "w") as f: f.write(f'''""" This module contains mappings from Windows timezone identifiers to Olson timezone identifiers. This file is automatically generated by generate_windows_to_olson_mapping.py. Do not edit manually. The data is taken from the Unicode Consortium [0] at commit ``{version}``, dated {version_date}. The proposal and rationale for this mapping is also available at the Unicode Consortium [1]. [0] https://github.com/unicode-org/cldr/blob/{version}/{CLDR_PATH} [1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping """ version = {json.dumps(version)} version_date = {json.dumps(version_date)} ''') f.write("WINDOWS_TO_OLSON = ") f.write(json.dumps(dict(result), indent=4, sort_keys=True)) f.write("\n") collective-icalendar-138c845/news/000077500000000000000000000000001524134033600170215ustar00rootroot00000000000000collective-icalendar-138c845/news/.gitkeep000066400000000000000000000000001524134033600204400ustar00rootroot00000000000000collective-icalendar-138c845/pyproject.toml000066400000000000000000000274011524134033600207650ustar00rootroot00000000000000# # We use the pyproject.toml package specification. # See https://packaging.python.org/en/latest/guides/section-build-and-publish/ # See https://github.com/collective/icalendar/issues/686 # See https://packaging.python.org/en/latest/specifications/pyproject-toml/ # [build-system] requires = ["hatchling>=1.27.0", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "icalendar" license = "BSD-2-Clause" license-files = ["LICENSE.rst"] keywords = [ "calendar", "calendaring", "ical", "icalendar", "event", "todo", "journal", "recurring", "rfc5545", ] # If an email address for the authors ever arises, then add it here. # See https://github.com/collective/icalendar/pull/707#discussion_r1775275335 authors = [ { name="Plone Foundation" }, ] maintainers = [ { name="Nicco Kunzmann", email="niccokunzmann@rambler.ru" }, { name="Sashank Bhamidi" }, { name="Christian Geier" }, { name="Jaca", email="vitouejj@gmail.com" }, ] # These attributes are dynamically generated by hatch-vcs dynamic = [ "version" ] description = "RFC 5545 compatible parser and generator of iCalendar files" readme = { file = "README.rst", content-type = "text/x-rst" } ## ## When adjusting the Python Version, adjust also: ## - .github/workflows/tests.yml ## - .github/workflows/documentation.yml ## - the classifiers below ## - the documentation: ## - docs/contribute/development.rst ## - docs/how-to/upgrade.rst ## - dependencies - some are for compatibility and can be removed ## - tool.ruff.target-version ## - src/icalendar/compatibility.py ## - Makefile ## requires-python = ">=3.10" # see https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "python-dateutil", # tzdata: IANA timezone database fallback for zoneinfo (required on Windows, # provides current timezone data on all platforms) "tzdata>=2025.3", # typing.TypeIs was added in Python 3.13 # TODO: remove this dependency here and throughout code # when we eventually drop support for Python 3.12. "typing-extensions~=4.10; python_version < '3.13'", ] # Static URLs for GitHub dependency graph compatibility # See https://github.com/collective/icalendar/issues/1035 [project.urls] Homepage = "https://icalendar.readthedocs.io/en/stable/" Repository = "https://github.com/collective/icalendar" Issues = "https://github.com/collective/icalendar/issues" Documentation = "https://icalendar.readthedocs.io/en/stable/" Changelog = "https://icalendar.readthedocs.io/en/stable/changelog.html" Funding = "https://opencollective.com/python-icalendar" # Local development dependencies [dependency-groups] dev = [ {include-group = "test"}, {include-group = "docs"}, {include-group = "formatting"}, "towncrier>=25.8.0", "tox", "tox-uv", "zizmor==1.29.0", # also update .github/workflows/zizmor.yml ] test = [ "coverage", "hypothesis", "mypy>=1.0", "pytest", "pytz>=2025.2", "types-python-dateutil", "types-pytz", ] formatting = [ "ruff==0.15.2", # also update .pre-commit-config.yaml "pre-commit", ] docs = [ "pydata-sphinx-theme>=0.14.4", "sphinx>=7", "sphinx-autobuild>=2021.3.14", "sphinx-autodoc-typehints", "sphinx-copybutton>=0.5.2", "sphinx-design>=0.5.0", "sphinx-icalendar>=0.1.1", "sphinx-issues>=5.0.1", "sphinx-llms-txt>=0.7.1", "sphinx-notfound-page>=1.1.0", "sphinx-reredirects>=0.1.6", "vale>=3.12.0", ] [project.scripts] icalendar = "icalendar.cli:main" [tool.hatch.build] exclude = [ "/.*", "/*.*", "/src/icalendar/fuzzing", "/dist", "/build", "/htmlcov", ] [tool.hatch.build.targets.sdist.force-include] "funding.json" = "funding.json" [tool.hatch.build.targets.wheel] packages = ["src/icalendar"] [tool.hatch.metadata.hooks.vcs.urls] # Dynamic URLs for built packages (hatch-vcs replaces static [project.urls]) Homepage = "https://icalendar.readthedocs.io/en/stable/" Repository = "https://github.com/collective/icalendar" source_archive = "https://github.com/collective/icalendar/archive/{commit_hash}.zip" Issues = "https://github.com/collective/icalendar/issues" Documentation = "https://icalendar.readthedocs.io/en/stable/" Changelog = "https://icalendar.readthedocs.io/en/stable/changelog.html" Funding = "https://opencollective.com/python-icalendar" [tool.hatch.version] # This configuration allows us to use the version from the tags and dynamically generate # version files. This speeds up the release process. source = "vcs" [tool.hatch.version.raw-options] # see https://github.com/ofek/hatch-vcs/issues/43#issuecomment-1553065222 local_scheme = "no-local-version" [tool.hatch.build.hooks.vcs] version-file = "src/icalendar/_version.py" [tool.hatch.metadata] allow-direct-references = true [tool.ruff] target-version = "py310" unsafe-fixes = true [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN", # flake8-annotations "B020", # Loop control variable {name} overrides iterable it iterates "C401", # Unnecessary generator (rewrite as a set comprehension) "C901", # {name} is too complex ({complexity} > {max_complexity}) "COM812", # Trailing comma missing "D1", # Missing docstring "D2", # docstrings stuffs "D4", # docstrings stuffs "EM10", # Exception string usage "ERA001", # Found commented-out code "FBT002", # Boolean default positional argument in function definition "FIX", # TODO comments "ISC001", # Implicitly concatenated string literals on one line (to avoid with formatter) "N818", # Exception name {name} should be named with an Error suffix "PLR091", # Too many things (complexity, arguments, branches, etc...) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "RUF012", # Mutable class attributes should be annotated with typing.ClassVar "RUF015", # Prefer next({iterable}) over single element slice "S101", # Use of assert detected "TD", # TODO comments "TRY003", # Avoid specifying long messages outside the exception class "S104", # Possible binding to all interfaces "E722", # Do not use bare `except` "RUF005", # Consider iterable unpacking instead of concatenation "DTZ005", # `datetime.datetime.now()` called without a `tz` argument "PERF401", # Use a list comprehension to create a transformed list "ARG002", # Unused method argument: ... "ARG001", # Unused function argument: ... ] extend-safe-fixes = [ "PT006", # Wrong type passed to first argument of @pytest.mark.parametrize; expected {expected_string} ] [tool.ruff.lint.per-file-ignores] "src/icalendar/tests/**" = [ "B011", # Do not assert False (python -O removes these calls), raise AssertionError() "B018", # useless expression, used to test property access errors "DTZ001", # datetime.datetime() called without a tzinfo argument "E501", # line too long "E721", # comparing types with == "E741", # ambiguous variable name "FBT001", # Boolean positional argument in function definition "FBT003", # Boolean positional value in function call "INP001", # implicit namespace package "N802", # Function name {name} should be lowercase "N803", # Argument name {name} for {param_type} should be lowercase "N806", # Variable should be lowercase "N999", # module name "PLC0415", # import outside top-level - common in tests for isolation "PT011", # pytest.raises({exception}) is too broad "PT012", # pytest.raises() block should contain a single simple statement "PT015", # Assertion always fails, replace with pytest.fail() "PT027", # assertRaises "RUF001", # String contains ambiguous character "SIM300", # Yoda condition "SLF001", # Private member accessed "T201", # print found "T203", # `pprint` found ] "src/icalendar/cal/**" = [ "A002", # argument shadowing builtin (sorted) "DTZ001", # datetime() called without tzinfo "E501", # line too long (docstrings with RFC references) "N802", # function name should be lowercase (TRIGGER_RELATED etc.) "PERF203", # try-except in loop (parsing loop) "PLW1641", # eq without hash (Component intentionally unhashable) "PLW2901", # loop variable overwritten "SLF001", # private member access ] "src/icalendar/prop/**" = [ "A003", # class attribute shadowing builtin (vBinary.bytes, see #1289) "DTZ001", # datetime() called without tzinfo "DTZ007", # naive datetime from strptime "E501", # line too long (docstrings with code examples) "N801", # Class name should use CamelCase "PERF203", # try-except in loop "PLW1641", # eq without hash (mutable structured types) "PLW2901", # loop variable overwritten ] "src/icalendar/**" = [ "FBT001", # boolean positional argument "FBT003", # boolean positional value in function call "PLC0415", # import outside top-level (circular import avoidance) ] "src/icalendar/fuzzing/*" = [ "T201", # print found "N802", # Function name {name} should be lowercase ] "docs/conf.py" = [ "INP001", # needs __init__.py ] "src/icalendar/timezone/equivalent_timezone_ids_result.py" = [ "DTZ001", # `datetime.datetime()` called without a `tzinfo` argument "E501", # line too long ] "src/icalendar/timezone/equivalent_timezone_ids.py" = [ "T201", # print "T203", # pprint ] ".github/workflows/test_generate_matrix.py" = [ "T201", # print "INP001", # needs __init__.py ] [tool.pytest.ini_options] # see https://docs.pytest.org/en/6.2.x/customize.html minversion = "6.0" # see https://docs.pytest.org/en/6.2.x/reference.html?highlight=testpaths#confval-testpaths testpaths = [ "src/icalendar/tests", ] # see https://docs.pytest.org/en/6.2.x/reference.html?highlight=testpaths#confval-norecursedirs norecursedirs = [ "src/icalendar/tests/hypothesis", "build", ] filterwarnings = [ "ignore::DeprecationWarning", ] [tool.towncrier] directory = "news/" filename = "CHANGES.rst" issue_format = "`Issue #{issue} `_" start_string = ".. towncrier release notes start\n" title_format = "{version} ({project_date})" # First underline is used for version/date header. # Second underline is used for the type names (like 'Bug fixes:'). underlines = ["-", "~", "`"] [[tool.towncrier.type]] directory = "breaking" name = "Breaking changes" showcontent = true [[tool.towncrier.type]] directory = "removal" name = "Removals and deprecations" showcontent = true [[tool.towncrier.type]] directory = "feature" name = "New features" showcontent = true [[tool.towncrier.type]] directory = "bugfix" name = "Bug fixes" showcontent = true [[tool.towncrier.type]] directory = "documentation" name = "Documentation" showcontent = true [[tool.towncrier.type]] directory = "deps" name = "Dependency changes" showcontent = true [[tool.towncrier.type]] directory = "internal" name = "Internal changes" showcontent = true [[tool.towncrier.type]] directory = "chore" name = "Other tasks" showcontent = false [[tool.towncrier.type]] directory = "security" name = "Security fixes" showcontent = true [tool.uv] python-preference = "managed" [tool.uv.sources] icalendar = { workspace = true, editable = true } [tool.uv.workspace] members = [ ".", ] collective-icalendar-138c845/src/000077500000000000000000000000001524134033600166345ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/000077500000000000000000000000001524134033600205565ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/__init__.py000066400000000000000000000062621524134033600226750ustar00rootroot00000000000000from icalendar.alarms import ( Alarms, AlarmTime, ) from icalendar.cal import ( Alarm, Availability, Available, Calendar, Component, ComponentFactory, Event, FreeBusy, Journal, LazyCalendar, Timezone, TimezoneDaylight, TimezoneStandard, Todo, ) from icalendar.enums import ( BUSYTYPE, CLASS, CUTYPE, FBTYPE, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, STATUS, TRANSP, VALUE, ) from icalendar.error import ( BrokenCalendarProperty, ComponentEndMissing, ComponentStartMissing, FeatureWillBeRemovedInFutureVersion, GloballyUniqueTZIDGuessed, ICalParsingError, IncompleteAlarmInformation, IncompleteComponent, InvalidCalendar, JCalParsingError, LocalTimezoneMissing, ) # Parameters and helper methods for splitting and joining string with escaped # chars. from icalendar.parser import ( Parameters, q_join, q_split, ) # Property Data Value Types from icalendar.prop import ( VPROPERTY, AdrFields, TypesFactory, vAdr, vBinary, vBoolean, vBroken, vCalAddress, vCategory, vDate, vDatetime, vDDDLists, vDDDTypes, vDuration, vFloat, vFrequency, vGeo, vInt, vMonth, vN, vOrg, vPeriod, vRecur, vSkip, vText, vTime, vUid, vUnknown, vUri, vUTCOffset, vWeekday, vXmlReference, ) from icalendar.prop.conference import Conference from icalendar.prop.image import Image # Switching the timezone provider from icalendar.prop.n import NFields from icalendar.timezone import is_utc, use_pytz, use_zoneinfo from .version import __version__, __version_tuple__, version, version_tuple __all__ = [ "BUSYTYPE", "CLASS", "CUTYPE", "FBTYPE", "PARTSTAT", "RANGE", "RELATED", "RELTYPE", "ROLE", "STATUS", "TRANSP", "VALUE", "VPROPERTY", "AdrFields", "Alarm", "AlarmTime", "Alarms", "Availability", "Available", "BrokenCalendarProperty", "Calendar", "Component", "ComponentEndMissing", "ComponentFactory", "ComponentStartMissing", "Conference", "Event", "FeatureWillBeRemovedInFutureVersion", "FreeBusy", "GloballyUniqueTZIDGuessed", "ICalParsingError", "Image", "IncompleteAlarmInformation", "IncompleteComponent", "InvalidCalendar", "JCalParsingError", "Journal", "LazyCalendar", "LocalTimezoneMissing", "NFields", "Parameters", "Timezone", "TimezoneDaylight", "TimezoneStandard", "Todo", "TypesFactory", "__version__", "__version_tuple__", "is_utc", "q_join", "q_split", "use_pytz", "use_zoneinfo", "vAdr", "vBinary", "vBoolean", "vBroken", "vCalAddress", "vCategory", "vDDDLists", "vDDDTypes", "vDate", "vDatetime", "vDuration", "vFloat", "vFrequency", "vGeo", "vInt", "vMonth", "vN", "vOrg", "vPeriod", "vRecur", "vSkip", "vText", "vTime", "vUTCOffset", "vUid", "vUnknown", "vUri", "vWeekday", "vXmlReference", "version", "version_tuple", ] collective-icalendar-138c845/src/icalendar/alarms.py000066400000000000000000000334731524134033600224210ustar00rootroot00000000000000"""Compute the times and states of alarms. This takes different calendar software into account and the RFC 9074 (Alarm Extension). - RFC 9074 defines an ACKNOWLEDGED property in the VALARM. - Outlook does not export VALARM information. - Google Calendar uses the DTSTAMP to acknowledge the alarms. - Thunderbird snoozes the alarms with a X-MOZ-SNOOZE-TIME attribute in the event. - Thunderbird acknowledges the alarms with a X-MOZ-LASTACK attribute in the event. - Etar deletes alarms that are acknowledged. - Nextcloud's Webinterface does not do anything with the alarms when the time passes. """ from __future__ import annotations from datetime import date, timedelta, tzinfo from typing import TYPE_CHECKING from icalendar.cal.event import Event from icalendar.cal.todo import Todo from icalendar.error import ( ComponentEndMissing, ComponentStartMissing, IncompleteAlarmInformation, LocalTimezoneMissing, ) from icalendar.timezone import tzp from icalendar.tools import is_date, normalize_pytz, to_datetime if TYPE_CHECKING: from collections.abc import Generator from datetime import datetime from icalendar.cal.alarm import Alarm Parent = Event | Todo class AlarmTime: """Represents a computed alarm occurrence with its timing and state. An AlarmTime instance combines an alarm component with its resolved trigger time and additional state information, such as acknowledgment and snoozing. """ def __init__( self, alarm: Alarm, trigger: datetime, acknowledged_until: datetime | None = None, snoozed_until: datetime | None = None, parent: Parent | None = None, ) -> None: """Create an instance of ``AlarmTime`` with any of its parameters. Parameters: alarm: The underlying alarm component. trigger: A date or datetime at which to trigger the alarm. acknowledged_until: Optional datetime in UTC until which the alarm has been acknowledged. snoozed_until: Optional datetime in UTC until which the alarm has been snoozed. parent: Optional parent component to which the alarm refers. """ self._alarm = alarm self._parent = parent self._trigger = trigger self._last_ack = acknowledged_until self._snooze_until = snoozed_until @property def acknowledged(self) -> datetime | None: """The time in UTC at which this alarm was last acknowledged. If the alarm was not acknowledged (dismissed), then this is None. """ ack = self.alarm.ACKNOWLEDGED if ack is None: return self._last_ack if self._last_ack is None: return ack return max(ack, self._last_ack) @property def alarm(self) -> Alarm: """The alarm component.""" return self._alarm @property def action(self) -> str: """The action invoked when this alarm triggers. This delegates to :attr:`Alarm.ACTION `. """ return self.alarm.ACTION @property def parent(self) -> Parent | None: """The component that contains the alarm. This is ``None`` if you didn't use :meth:`Alarms.add_component() `. """ return self._parent def is_active(self) -> bool: """Whether this alarm is active (``True``) or acknowledged (``False``). For example, in some calendar software, this is ``True`` until the user views the alarm message and dismisses it. Alarms can be in local time without a timezone. To calculate whether the alarm has occurred, the time must include timezone information. Raises: LocalTimezoneMissing: If a timezone is required but not given. """ acknowledged = self.acknowledged if not acknowledged: return True if self._snooze_until is not None and self._snooze_until > acknowledged: return True trigger = self.trigger if trigger.tzinfo is None: raise LocalTimezoneMissing( "A local timezone is required to check if the alarm is still active. " "Use Alarms.set_local_timezone()." ) return trigger > acknowledged @property def trigger(self) -> date: """The time at which the alarm triggers. If the alarm has been snoozed, this may differ from the TRIGGER property. """ if self._snooze_until is not None and self._snooze_until > self._trigger: return self._snooze_until return self._trigger class Alarms: """Compute the times and states of alarms. This is an example using RFC 9074. One alarm is 30 minutes before the event and acknowledged. Another alarm is 15 minutes before the event and still active. >>> from icalendar import Event, Alarms >>> event = Event.from_ical( ... '''BEGIN:VEVENT ... CREATED:20210301T151004Z ... UID:AC67C078-CED3-4BF5-9726-832C3749F627 ... DTSTAMP:20210301T151004Z ... DTSTART;TZID=America/New_York:20210302T103000 ... DTEND;TZID=America/New_York:20210302T113000 ... SUMMARY:Meeting ... BEGIN:VALARM ... UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 ... TRIGGER:-PT30M ... ACKNOWLEDGED:20210302T150004Z ... DESCRIPTION:Event reminder ... ACTION:DISPLAY ... END:VALARM ... BEGIN:VALARM ... UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 ... TRIGGER:-PT15M ... DESCRIPTION:Event reminder ... ACTION:DISPLAY ... END:VALARM ... END:VEVENT ... ''') >>> alarms = Alarms(event) >>> len(alarms.times) # all alarms including those acknowledged 2 >>> len(alarms.active) # the alarms that are not acknowledged, yet 1 >>> alarms.active[0].trigger # this alarm triggers 15 minutes before 10:30 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) RFC 9074 specifies that alarms can also be triggered by proximity. This is not implemented yet. """ def __init__(self, component: Alarm | Event | Todo | None = None) -> None: """Start computing alarm times.""" self._absolute_alarms: list[Alarm] = [] self._start_alarms: list[Alarm] = [] self._end_alarms: list[Alarm] = [] self._start: date | None = None self._end: date | None = None self._parent: Parent | None = None self._last_ack: datetime | None = None self._snooze_until: datetime | None = None self._local_tzinfo: tzinfo | None = None if component is not None: self.add_component(component) def add_component(self, component: Alarm | Parent) -> None: """Add a component. If this is an alarm, it is added. Events and Todos are added as a parent and all their alarms are added, too. """ if isinstance(component, (Event, Todo)): self.set_parent(component) self.set_start(component.start) self.set_end(component.end) if component.is_thunderbird(): self.acknowledge_until(component.X_MOZ_LASTACK) self.snooze_until(component.X_MOZ_SNOOZE_TIME) else: self.acknowledge_until(component.DTSTAMP) for alarm in component.walk("VALARM"): self.add_alarm(alarm) def set_parent(self, parent: Parent): """Set the parent of all the alarms. If you would like to collect alarms from a component, use add_component """ if self._parent is not None and self._parent is not parent: raise ValueError("You can only set one parent for this alarm calculation.") self._parent = parent def add_alarm(self, alarm: Alarm) -> None: """Optional: Add an alarm component.""" trigger = alarm.TRIGGER if trigger is None: return if isinstance(trigger, date): self._absolute_alarms.append(alarm) elif alarm.TRIGGER_RELATED == "START": self._start_alarms.append(alarm) else: self._end_alarms.append(alarm) def set_start(self, dt: date | None): """Set the start of the component. If you have only absolute alarms, this is not required. If you have alarms relative to the start of a component, set the start here. """ self._start = dt def set_end(self, dt: date | None): """Set the end of the component. If you have only absolute alarms, this is not required. If you have alarms relative to the end of a component, set the end here. """ self._end = dt def _add(self, dt: date, td: timedelta): """Add a timedelta to a datetime.""" if is_date(dt): if td.seconds == 0: return dt + td dt = to_datetime(dt) return normalize_pytz(dt + td) def acknowledge_until(self, dt: date | None) -> None: """The time in UTC when all the alarms of this component were acknowledged. Only the last call counts. Since RFC 9074 (Alarm Extension) was created later, calendar implementations differ in how they acknowledge alarms. For example, Thunderbird and Google Calendar store the last time an event has been acknowledged because of an alarm. All alarms that happen before this time count as acknowledged. """ self._last_ack = tzp.localize_utc(dt) if dt is not None else None def snooze_until(self, dt: date | None) -> None: """This is the time in UTC when all the alarms of this component were snoozed. Only the last call counts. The alarms are supposed to turn up again at dt when they are not acknowledged but snoozed. """ self._snooze_until = tzp.localize_utc(dt) if dt is not None else None def set_local_timezone(self, tzinfo: tzinfo | str | None): """Set the local timezone. Events are sometimes in local time. In order to compute the exact time of the alarm, some alarms without timezone are considered local. Some computations work without setting this, others don't. If they need this information, expect a :exc:`~icalendar.error.LocalTimezoneMissing` exception somewhere down the line. """ self._local_tzinfo = tzp.timezone(tzinfo) if isinstance(tzinfo, str) else tzinfo @property def times(self) -> list[AlarmTime]: """Compute and return the times of the alarms given. If the information for calculation is incomplete, this will raise a :exc:`~icalendar.error.IncompleteAlarmInformation` exception. Please make sure to set all the required parameters before calculating. If you forget to set the acknowledged times, that is not problem. """ return ( self._get_end_alarm_times() + self._get_start_alarm_times() + self._get_absolute_alarm_times() ) def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]: """The times when the alarm is triggered relative to start.""" yield first # we trigger at the start repeat = alarm.repeat duration = alarm.DURATION if repeat and duration: for i in range(1, repeat + 1): yield self._add(first, duration * i) def _alarm_time(self, alarm: Alarm, trigger: date): """Create an alarm time with the additional attributes.""" if getattr(trigger, "tzinfo", None) is None and self._local_tzinfo is not None: trigger = normalize_pytz(trigger.replace(tzinfo=self._local_tzinfo)) return AlarmTime( alarm, trigger, self._last_ack, self._snooze_until, self._parent ) def _get_absolute_alarm_times(self) -> list[AlarmTime]: """Return a list of absolute alarm times.""" return [ self._alarm_time(alarm, trigger) for alarm in self._absolute_alarms for trigger in self._repeat(alarm.TRIGGER, alarm) ] def _get_start_alarm_times(self) -> list[AlarmTime]: """Return a list of alarm times relative to the start of the component.""" if self._start is None and self._start_alarms: raise ComponentStartMissing( "Use Alarms.set_start because at least one alarm is relative to the " "start of a component." ) return [ self._alarm_time(alarm, trigger) for alarm in self._start_alarms for trigger in self._repeat(self._add(self._start, alarm.TRIGGER), alarm) ] def _get_end_alarm_times(self) -> list[AlarmTime]: """Return a list of alarm times relative to the end of the component.""" if self._end is None and self._end_alarms: raise ComponentEndMissing( "Use Alarms.set_end because at least one alarm is relative to the end " "of a component." ) return [ self._alarm_time(alarm, trigger) for alarm in self._end_alarms for trigger in self._repeat(self._add(self._end, alarm.TRIGGER), alarm) ] @property def active(self) -> list[AlarmTime]: """The alarm times that are still active and not acknowledged. This considers snoozed alarms. Alarms can be in local time (without a timezone). To calculate if the alarm really happened, we need it to be in a timezone. If a timezone is required but not given, we throw an :exc:`~icalendar.error.IncompleteAlarmInformation`. """ return [alarm_time for alarm_time in self.times if alarm_time.is_active()] __all__ = [ "AlarmTime", "Alarms", "ComponentEndMissing", "ComponentStartMissing", "IncompleteAlarmInformation", ] collective-icalendar-138c845/src/icalendar/attr.py000066400000000000000000002722021524134033600221070ustar00rootroot00000000000000"""Attributes of Components and properties.""" from __future__ import annotations import itertools from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Literal, TypeAlias from icalendar.enums import BUSYTYPE, CLASS, STATUS, TRANSP, StrEnum from icalendar.error import IncompleteComponent, InvalidCalendar from icalendar.parser_tools import SEQUENCE_TYPES from icalendar.prop import ( vCalAddress, vCategory, vDDDTypes, vDuration, vRecur, vText, vUid, vUnknown, vUri, vXmlReference, ) from icalendar.prop.conference import Conference from icalendar.prop.image import Image from icalendar.timezone import tzp from icalendar.tools import is_date if TYPE_CHECKING: from collections.abc import Callable, Sequence from icalendar.cal import Component def _get_rdates( self: Component, ) -> list[tuple[date, None] | tuple[datetime, None] | tuple[datetime, datetime]]: """The RDATE property defines the list of DATE-TIME values for recurring components. RDATE is defined in :rfc:`5545`. The return value is a list of tuples ``(start, end)``. ``start`` can be a :class:`datetime.date` or a :class:`datetime.datetime`, with and without timezone. ``end`` is :obj:`None` if the end is not specified and a :class:`datetime.datetime` if the end is specified. Value Type: The default value type for this property is DATE-TIME. The value type can be set to DATE or PERIOD. Property Parameters: IANA, non-standard, value data type, and time zone identifier property parameters can be specified on this property. Conformance: This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE" calendar component. Description: This property can appear along with the "RRULE" property to define an aggregate set of repeating occurrences. When they both appear in a recurring component, the recurrence instances are defined by the union of occurrences defined by both the "RDATE" and "RRULE". The recurrence dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set of recurrence instances for a calendar component. The recurrence set is generated by considering the initial "DTSTART" property along with the "RRULE", "RDATE", and "EXDATE" properties contained within the recurring component. The "DTSTART" property defines the first instance in the recurrence set. The "DTSTART" property value SHOULD match the pattern of the recurrence rule, if specified. The recurrence set generated with a "DTSTART" property value that doesn't match the pattern of the rule is undefined. The final recurrence set is generated by gathering all of the start DATE-TIME values generated by any of the specified "RRULE" and "RDATE" properties, and then excluding any start DATE-TIME values specified by "EXDATE" properties. This implies that start DATE-TIME values specified by "EXDATE" properties take precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only one recurrence is considered. Duplicate instances are ignored. Example: Below, we set one RDATE in a list and get the resulting tuple of start and end. .. code-block:: pycon >>> from icalendar import Event >>> from datetime import datetime >>> event = Event() # Add a list of recurrence dates >>> event.add("RDATE", [datetime(2025, 4, 28, 16, 5)]) >>> event.rdates [(datetime.datetime(2025, 4, 28, 16, 5), None)] .. note:: Modifying the returned list does not change the RDATE value. Assign to :attr:`rdates` or use :func:`icalendar.cal.Component.add` instead. If you want to compute recurrences, have a look at `Related Projects `_. """ result = [] rdates = self.get("RDATE", []) for rdates in (rdates,) if not isinstance(rdates, list) else rdates: for dts in rdates.dts: rdate = dts.dt if isinstance(rdate, tuple): # we have a period as rdate if isinstance(rdate[1], timedelta): result.append((rdate[0], rdate[0] + rdate[1])) else: result.append(rdate) else: # we have a date/datetime result.append((rdate, None)) return result def _set_rdates(self: Component, value) -> None: """Set the RDATE values, replacing any existing ones. ``value`` is a list as returned by :attr:`rdates` (each item a date, a datetime, or a ``(start, end)`` period tuple). Setting an empty list or :obj:`None` removes the RDATE property. """ _del_rdates(self) if value: self.add("RDATE", value) def _del_rdates(self: Component) -> None: """Delete all RDATE values.""" self.pop("RDATE", None) rdates_property = property(_get_rdates, _set_rdates, _del_rdates) def _get_exdates(self: Component) -> list[date | datetime]: """EXDATE defines the list of DATE-TIME exceptions for recurring components. EXDATE is defined in :rfc:`5545`. Value Type: The default value type for this property is DATE-TIME. The value type can be set to DATE. Property Parameters: IANA, non-standard, value data type, and time zone identifier property parameters can be specified on this property. Conformance: This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE" calendar component. Description: The exception dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set of recurrence instances for a calendar component. The recurrence set is generated by considering the initial "DTSTART" property along with the "RRULE", "RDATE", and "EXDATE" properties contained within the recurring component. The "DTSTART" property defines the first instance in the recurrence set. The "DTSTART" property value SHOULD match the pattern of the recurrence rule, if specified. The recurrence set generated with a "DTSTART" property value that doesn't match the pattern of the rule is undefined. The final recurrence set is generated by gathering all of the start DATE-TIME values generated by any of the specified "RRULE" and "RDATE" properties, and then excluding any start DATE-TIME values specified by "EXDATE" properties. This implies that start DATE-TIME values specified by "EXDATE" properties take precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). When duplicate instances are generated by the "RRULE" and "RDATE" properties, only one recurrence is considered. Duplicate instances are ignored. The "EXDATE" property can be used to exclude the value specified in "DTSTART". However, in such cases, the original "DTSTART" date MUST still be maintained by the calendaring and scheduling system because the original "DTSTART" value has inherent usage dependencies by other properties such as the "RECURRENCE-ID". Example: Below, we add an exdate in a list and get the resulting list of exdates. .. code-block:: pycon >>> from icalendar import Event >>> from datetime import datetime >>> event = Event() # Add a list of excluded dates >>> event.add("EXDATE", [datetime(2025, 4, 28, 16, 5)]) >>> event.exdates [datetime.datetime(2025, 4, 28, 16, 5)] .. note:: Modifying the returned list does not change the EXDATE value. Assign to :attr:`exdates` or use :func:`icalendar.cal.Component.add` instead. If you want to compute recurrences, have a look at `Related Projects `_. """ result = [] exdates = self.get("EXDATE", []) for exdates in (exdates,) if not isinstance(exdates, list) else exdates: for dts in exdates.dts: exdate = dts.dt # we have a date/datetime result.append(exdate) return result def _set_exdates(self: Component, value) -> None: """Set the EXDATE values, replacing any existing ones. ``value`` is a list as returned by :attr:`exdates` (each item a date or a datetime). Setting an empty list or :obj:`None` removes the EXDATE property. """ _del_exdates(self) if value: self.add("EXDATE", value) def _del_exdates(self: Component) -> None: """Delete all EXDATE values.""" self.pop("EXDATE", None) exdates_property = property(_get_exdates, _set_exdates, _del_exdates) def _get_rrules(self: Component) -> list[vRecur]: """RRULE defines a rule or repeating pattern for recurring components. RRULE is defined in :rfc:`5545`. :rfc:`7529` adds the ``SKIP`` parameter :class:`icalendar.prop.vSkip`. Property Parameters: IANA and non-standard property parameters can be specified on this property. Conformance: This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once. The recurrence set generated with multiple "RRULE" properties is undefined. Description: The recurrence rule, if specified, is used in computing the recurrence set. The recurrence set is the complete set of recurrence instances for a calendar component. The recurrence set is generated by considering the initial "DTSTART" property along with the "RRULE", "RDATE", and "EXDATE" properties contained within the recurring component. The "DTSTART" property defines the first instance in the recurrence set. The "DTSTART" property value SHOULD be synchronized with the recurrence rule, if specified. The recurrence set generated with a "DTSTART" property value not synchronized with the recurrence rule is undefined. The final recurrence set is generated by gathering all of the start DATE-TIME values generated by any of the specified "RRULE" and "RDATE" properties, and then excluding any start DATE-TIME values specified by "EXDATE" properties. This implies that start DATE- TIME values specified by "EXDATE" properties take precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only one recurrence is considered. Duplicate instances are ignored. The "DTSTART" property specified within the iCalendar object defines the first instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value type used with a recurrence rule, should be specified as a date with local time and time zone reference to make sure all the recurrence instances start at the same local time regardless of time zone changes. If the duration of the recurring component is specified with the "DTEND" or "DUE" property, then the same exact duration will apply to all the members of the generated recurrence set. Else, if the duration of the recurring component is specified with the "DURATION" property, then the same nominal duration will apply to all the members of the generated recurrence set and the exact duration of each recurrence instance will depend on its specific start time. For example, recurrence instances of a nominal duration of one day will have an exact duration of more or less than 24 hours on a day where a time zone shift occurs. The duration of a specific recurrence may be modified in an exception component or simply by using an "RDATE" property of PERIOD value type. Examples: Daily for 10 occurrences: .. code-block:: pycon >>> from icalendar import Event >>> from datetime import datetime >>> from zoneinfo import ZoneInfo >>> event = Event() >>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York")) >>> event.add("RRULE", "FREQ=DAILY;COUNT=10") >>> print(event.to_ical()) BEGIN:VEVENT DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;COUNT=10 END:VEVENT >>> event.rrules [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})] Daily until December 24, 1997: .. code-block:: pycon >>> from icalendar import Event, vRecur >>> from datetime import datetime >>> from zoneinfo import ZoneInfo >>> event = Event() >>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York")) >>> event.add("RRULE", vRecur({"FREQ": ["DAILY"]}, until=datetime(1997, 12, 24, tzinfo=ZoneInfo("UTC")))) >>> print(event.to_ical()) BEGIN:VEVENT DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;UNTIL=19971224T000000Z END:VEVENT >>> event.rrules [vRecur({'FREQ': ['DAILY'], 'UNTIL': [datetime.datetime(1997, 12, 24, 0, 0, tzinfo=ZoneInfo(key='UTC'))]})] .. note:: You cannot modify the RRULE value by modifying the result. Use :func:`icalendar.cal.Component.add` to add values. If you want to compute recurrences, have a look at `Related Projects `_. """ # noqa: E501 rrules = self.get("RRULE", []) if not isinstance(rrules, list): return [rrules] return rrules rrules_property = property(_get_rrules) def multi_language_text_property( main_prop: str, compatibility_prop: str | None, doc: str ) -> property: """This creates a text property. This property can be defined several times with different ``LANGUAGE`` parameters. Parameters: main_prop (str): The property to set and get, such as ``NAME`` compatibility_prop (str): An old property used before, such as ``X-WR-CALNAME`` doc (str): The documentation string """ def fget(self: Component) -> str | None: """Get the property""" result = self.get(main_prop) if result is None and compatibility_prop is not None: result = self.get(compatibility_prop) if isinstance(result, list): for item in result: if "LANGUAGE" not in item.params: return item return result def fset(self: Component, value: str | None): """Set the property.""" fdel(self) if value is not None: self.add(main_prop, value) if compatibility_prop is not None: self.add(compatibility_prop, value) def fdel(self: Component): """Delete the property.""" self.pop(main_prop, None) if compatibility_prop is not None: self.pop(compatibility_prop, None) return property(fget, fset, fdel, doc) def single_int_property( prop: str, default: int, doc: str, *, min_value: int | None = None ) -> property: """Create a property for an int value that exists only once. Parameters: default: Required. The default value. doc: Required. The documentation string. prop: Required. The name of the property. min_value: If set, the value must be greater than or equal to this minimum. .. versionadded:: 7.2.3 Added the ``min_value`` parameter. """ def fget(self: Component) -> int: """Get the property""" try: return int(self.get(prop, default)) except ValueError as e: raise InvalidCalendar(f"{prop} must be an int") from e def fset(self: Component, value: int | None): """Set the property.""" if value is not None: if not isinstance(value, int) or isinstance(value, bool): raise TypeError(f"{prop} must be an int, got {value!r}") if min_value is not None and value < min_value: raise InvalidCalendar(f"{prop} must be >= {min_value}, got {value}") fdel(self) if value is not None: self.add(prop, value) def fdel(self: Component): """Delete the property.""" self.pop(prop, None) return property(fget, fset, fdel, doc) def single_utc_property(name: str, docs: str) -> property: """Create a property to access a value of datetime in UTC timezone. Parameters: name: name of the property docs: documentation string """ docs = ( f"""The {name} property with all values converted to a :class:`~datetime.datetime` in UTC. """ + docs ) def fget(self: Component) -> datetime | None: """Get the value.""" if name not in self: return None dt = self.get(name) if isinstance(dt, (vText, vUnknown)): # we might be in an attribute that is not typed value = vDDDTypes.from_ical(dt) else: value = getattr(dt, "dt", dt) if value is None or not isinstance(value, date): raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}") return tzp.localize_utc(value) def fset(self: Component, value: datetime | None): """Set the value""" if value is None: fdel(self) return if not isinstance(value, date): raise TypeError(f"{name} takes a datetime in UTC, not {value}") fdel(self) self.add(name, tzp.localize_utc(value)) def fdel(self: Component): """Delete the property.""" self.pop(name, None) return property(fget, fset, fdel, doc=docs) def single_string_property( name: str, docs: str, other_name: str | list[str] | None = None, default: str = "" ) -> property: """Create a property to access a single string value.""" other_names = ( [] if other_name is None else [other_name] if isinstance(other_name, str) else list(other_name) ) def fget(self: Component) -> str: """Get the value.""" result = self.get(name, None) if result is None: for alias in other_names: result = self.get(alias, None) if result is not None: break if result is None or result == []: return default if isinstance(result, list): return result[0] return result def fset(self: Component, value: str | None): """Set the value. Setting the value to None will delete it. """ fdel(self) if value is not None: self.add(name, value) def fdel(self: Component): """Delete the property.""" self.pop(name, None) for alias in other_names: self.pop(alias, None) return property(fget, fset, fdel, doc=docs) color_property = single_string_property( "COLOR", """This property specifies a color used for displaying the component. This implements :rfc:`7986` ``COLOR`` property. Property Parameters: IANA and non-standard property parameters can be specified on this property. Conformance: This property can be specified once in an iCalendar object or in ``VEVENT``, ``VTODO``, or ``VJOURNAL`` calendar components. Description: This property specifies a color that clients MAY use when presenting the relevant data to a user. Typically, this would appear as the "background" color of events or tasks. The value is a case-insensitive color name taken from the CSS3 set of names, defined in Section 4.3 of `W3C.REC-css3-color-20110607 `_. Example: ``"turquoise"``, ``"#ffffff"`` .. code-block:: pycon >>> from icalendar import Todo >>> todo = Todo() >>> todo.color = "green" >>> print(todo.to_ical()) BEGIN:VTODO COLOR:green END:VTODO """, ) sequence_property = single_int_property( "SEQUENCE", 0, """This property defines the revision sequence number of the calendar component within a sequence of revisions. Value Type: INTEGER Property Parameters: IANA and non-standard property parameters can be specified on this property. Conformance: The property can be specified in "VEVENT", "VTODO", or "VJOURNAL" calendar component. Description: When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's" CUA each time the "Organizer" makes a significant revision to the calendar component. The "Organizer" includes this property in an iCalendar object that it sends to an "Attendee" to specify the current version of the calendar component. The "Attendee" includes this property in an iCalendar object that it sends to the "Organizer" to specify the version of the calendar component to which the "Attendee" is referring. A change to the sequence number is not the mechanism that an "Organizer" uses to request a response from the "Attendees". The "RSVP" parameter on the "ATTENDEE" property is used by the "Organizer" to indicate that a response from the "Attendees" is requested. Recurrence instances of a recurring component MAY have different sequence numbers. Examples: The following is an example of this property for a calendar component that was just created by the "Organizer": .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.sequence 0 The following is an example of this property for a calendar component that has been revised 10 different times by the "Organizer": .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.example("issue_156_RDATE_with_PERIOD_TZID_khal") >>> event = calendar.events[0] >>> event.sequence 10 Raises: TypeError: If the value is not an ``int``. Booleans are rejected, too, even though ``bool`` subclasses ``int``. ~icalendar.error.InvalidCalendar: If the value is negative. .. versionchanged:: 7.2.3 Negative values are no longer accepted. """, # noqa: E501 min_value=0, ) def _get_categories(component: Component) -> list[str]: """Get all the categories.""" categories: vCategory | list[vCategory] | None = component.get("CATEGORIES") if isinstance(categories, list): _set_categories( component, list(itertools.chain.from_iterable(cat.cats for cat in categories)), ) return _get_categories(component) if categories is None: categories = vCategory([]) component.add("CATEGORIES", categories) return categories.cats def _set_categories(component: Component, cats: Sequence[str] | None) -> None: """Set the categories.""" if not cats and cats != []: _del_categories(component) return component["CATEGORIES"] = categories = vCategory(cats) if isinstance(cats, list): cats.clear() cats.extend(categories.cats) categories.cats = cats def _del_categories(component: Component) -> None: """Delete the categories.""" component.pop("CATEGORIES", None) categories_property = property( _get_categories, _set_categories, _del_categories, """This property defines the categories for a component. The categories property is used to specify categories or subtypes of the calendar component. The categories are useful to search for a calendar component of a particular type and category. Within the calendar components, specify categories as a list of strings. You can get, set, and delete categories for a component. This property can be used in icalendar through its Python attributes of: - :attr:`Available.categories ` - :attr:`Availability.categories ` - :attr:`Calendar.categories ` - :attr:`Event.categories ` - :attr:`Journal.categories ` - :attr:`Todo.categories ` The categories property for ``Available`` and ``Availability`` complies with :rfc:`7953#section-3.1`, for ``Event``, ``Journal``, and ``Todo`` with :rfc:`5545#section-3.8.1.2`, and for ``Calendar`` with :rfc:`7986#section-5.6`. Note: At present, icalendar doesn't take the LANGUAGE parameter as defined in :rfc:`5545#section-3.2.10` into account. Parameters: categories(list[str]): A list of categories as strings. Example: Create an event, add categories to it, print its ical representation, append another category, and finally compare the result against its expected value. .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.categories = ["Work", "Meeting"] >>> print(event.to_ical()) BEGIN:VEVENT CATEGORIES:Work,Meeting END:VEVENT >>> event.categories.append("Lecture") >>> event.categories == ["Work", "Meeting", "Lecture"] True See also: :attr:`Component.concepts ` """, ) def _get_attendees(self: Component) -> list[vCalAddress]: """Get attendees.""" value = self.get("ATTENDEE") if value is None: value = [] self["ATTENDEE"] = value return value if isinstance(value, vCalAddress): return [value] return value def _set_attendees(self: Component, value: list[vCalAddress] | vCalAddress | None): """Set attendees.""" _del_attendees(self) if value is None: return if not isinstance(value, list): value = [value] self["ATTENDEE"] = value def _del_attendees(self: Component): """Delete all attendees.""" self.pop("ATTENDEE", None) attendees_property = property( _get_attendees, _set_attendees, _del_attendees, """ATTENDEE defines one or more "Attendees" within a calendar component. Conformance: This property MUST be specified in an iCalendar object that specifies a group-scheduled calendar entity. This property MUST NOT be specified in an iCalendar object when publishing the calendar information (e.g., NOT in an iCalendar object that specifies the publication of a calendar user's busy time, event, to-do, or journal). This property is not specified in an iCalendar object that specifies only a time zone definition or that defines calendar components that are not group-scheduled components, but are components only on a single user's calendar. Description: This property MUST only be specified within calendar components to specify participants, non-participants, and the chair of a group-scheduled calendar entity. The property is specified within an "EMAIL" category of the "VALARM" calendar component to specify an email address that is to receive the email type of iCalendar alarm. Examples: Add a new attendee to an existing event. .. code-block:: pycon >>> from icalendar import Event, vCalAddress >>> event = Event() >>> event.attendees.append(vCalAddress("mailto:me@my-domain.com")) >>> print(event.to_ical()) BEGIN:VEVENT ATTENDEE:mailto:me@my-domain.com END:VEVENT Create an email alarm with several attendees: >>> from icalendar import Alarm, vCalAddress >>> alarm = Alarm.new(attendees = [ ... vCalAddress("mailto:me@my-domain.com"), ... vCalAddress("mailto:you@my-domain.com"), ... ], summary = "Email alarm") >>> print(alarm.to_ical()) BEGIN:VALARM ATTENDEE:mailto:me@my-domain.com ATTENDEE:mailto:you@my-domain.com SUMMARY:Email alarm END:VALARM """, ) uid_property = single_string_property( "UID", """UID specifies the persistent, globally unique identifier for a component. We recommend using :func:`uuid.uuid4` to generate new values. Returns: The value of the UID property as a string or ``""`` if no value is set. Description: The "UID" itself MUST be a globally unique identifier. The generator of the identifier MUST guarantee that the identifier is unique. This is the method for correlating scheduling messages with the referenced "VEVENT", "VTODO", or "VJOURNAL" calendar component. The full range of calendar components specified by a recurrence set is referenced by referring to just the "UID" property value corresponding to the calendar component. The "RECURRENCE-ID" property allows the reference to an individual instance within the recurrence set. This property is an important method for group-scheduling applications to match requests with later replies, modifications, or deletion requests. Calendaring and scheduling applications MUST generate this property in "VEVENT", "VTODO", and "VJOURNAL" calendar components to assure interoperability with other group- scheduling applications. This identifier is created by the calendar system that generates an iCalendar object. Implementations MUST be able to receive and persist values of at least 255 octets for this property, but they MUST NOT truncate values in the middle of a UTF-8 multi-octet sequence. :rfc:`7986` states that UID can be used, for example, to identify duplicate calendar streams that a client may have been given access to. It can be used in conjunction with the "LAST-MODIFIED" property also specified on the "VCALENDAR" object to identify the most recent version of a calendar. Conformance: :rfc:`5545` states that the "UID" property can be specified on "VEVENT", "VTODO", and "VJOURNAL" calendar components. :rfc:`7986` modifies the definition of the "UID" property to allow it to be defined in an iCalendar object. :rfc:`9074` adds a "UID" property to "VALARM" components to allow a unique identifier to be specified. The value of this property can then be used to refer uniquely to the "VALARM" component. This property can be specified once only. Security: :rfc:`7986` states that UID values MUST NOT include any data that might identify a user, host, domain, or any other security- or privacy-sensitive information. It is RECOMMENDED that calendar user agents now generate "UID" values that are hex-encoded random Universally Unique Identifier (UUID) values as defined in Sections 4.4 and 4.5 of :rfc:`4122`. You can use the :mod:`uuid` module to generate new UUIDs. Compatibility: For Alarms, ``X-ALARMUID`` is also considered. Examples: The following is an example of such a property value: ``5FC53010-1267-4F8E-BC28-1D7AE55A7C99``. Set the UID of a calendar: .. code-block:: pycon >>> from icalendar import Calendar >>> from uuid import uuid4 >>> calendar = Calendar() >>> calendar.uid = uuid4() >>> print(calendar.to_ical()) BEGIN:VCALENDAR UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VCALENDAR """, ) summary_property = multi_language_text_property( "SUMMARY", None, """SUMMARY defines a short summary or subject for the calendar component. Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. Conformance: The property can be specified in "VEVENT", "VTODO", "VJOURNAL", or "VALARM" calendar components. Description: This property is used in the "VEVENT", "VTODO", and "VJOURNAL" calendar components to capture a short, one-line summary about the activity or journal entry. This property is used in the "VALARM" calendar component to capture the subject of an EMAIL category of alarm. Examples: The following is an example of this property: .. code-block:: pycon SUMMARY:Department Party """, ) description_property = multi_language_text_property( "DESCRIPTION", None, """DESCRIPTION provides a more complete description of the calendar component than that provided by the "SUMMARY" property. Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. Conformance: The property can be specified in the "VEVENT", "VTODO", "VJOURNAL", or "VALARM" calendar components. The property can be specified multiple times only within a "VJOURNAL" calendar component. Description: This property is used in the "VEVENT" and "VTODO" to capture lengthy textual descriptions associated with the activity. This property is used in the "VALARM" calendar component to capture the display text for a DISPLAY category of alarm, and to capture the body text for an EMAIL category of alarm. Examples: The following is an example of this property with formatted line breaks in the property value: .. code-block:: pycon DESCRIPTION:Meeting to provide technical review for "Phoenix" design.\\nHappy Face Conference Room. Phoenix design team MUST attend this meeting.\\nRSVP to team leader. """, # noqa: E501 ) def create_single_property( prop: str, value_attr: str | None, value_type: tuple[type], type_def: type, doc: str, vProp: type = vDDDTypes, # noqa: N803 convert: Callable[[object], object] | None = None, ): """Create a single property getter and setter. Parameters: prop: The name of the property. value_attr: The name of the attribute to get the value from. value_type: The type of the value. type_def: The type of the property. doc: The docstring of the property. vProp: The type of the property from :mod:`icalendar.prop`. """ def p_get(self: Component): default = object() result = self.get(prop, default) if result is default: return None if isinstance(result, list): raise InvalidCalendar(f"Multiple {prop} defined.") value = result if value_attr is None else getattr(result, value_attr, result) value = value if convert is None else convert(value) if not isinstance(value, value_type): raise InvalidCalendar( f"{prop} must be either a " f"{' or '.join(t.__name__ for t in value_type)}," f" not {value}." ) return value def p_set(self: Component, value) -> None: if value is None: p_del(self) return value = convert(value) if convert is not None else value if not isinstance(value, value_type): raise TypeError( f"Use {' or '.join(t.__name__ for t in value_type)}, " f"not {type(value).__name__}." ) self[prop] = vProp(value) if prop in self.exclusive: for other_prop in self.exclusive: if other_prop != prop: self.pop(other_prop, None) p_set.__annotations__["value"] = p_get.__annotations__["return"] = type_def | None def p_del(self: Component): self.pop(prop) p_doc = f"""The {prop} property. {doc} To delete the value, either use ``del`` or set it to ``None``. Raises: InvalidCalendar: if the attribute has invalid values. """ return property(p_get, p_set, p_del, p_doc) X_MOZ_SNOOZE_TIME_property = single_utc_property( "X-MOZ-SNOOZE-TIME", "Thunderbird: Alarms before this time are snoozed." ) X_MOZ_LASTACK_property = single_utc_property( "X-MOZ-LASTACK", "Thunderbird: Alarms before this time are acknowledged." ) def property_get_duration(self: Component) -> timedelta | None: """Getter for property DURATION.""" default = object() duration = self.get("duration", default) if duration is default: return None result = getattr(duration, "td", None) if result is None: raise InvalidCalendar( f"DURATION must be a timedelta, not {type(duration).__name__}." ) return result def property_set_duration(self: Component, value: timedelta | None): """Setter for property DURATION.""" if value is None: self.pop("duration", None) return if not isinstance(value, timedelta): raise TypeError(f"Use timedelta, not {type(value).__name__}.") self["duration"] = vDuration(value) self.pop("DTEND") self.pop("DUE") def property_del_duration(self: Component): """Delete property DURATION.""" self.pop("DURATION") property_doc_duration_template = """The DURATION property. The "DTSTART" property for a "{component}" specifies the inclusive start of the {component}. The "DURATION" property in conjunction with the DTSTART property for a "{component}" calendar component specifies the non-inclusive end of the event. If you would like to calculate the duration of a {component}, do not use this. Instead use the duration property (lower case). """ def duration_property(component: str) -> property: """Return the duration property.""" return property( property_get_duration, property_set_duration, property_del_duration, property_doc_duration_template.format(component=component), ) def multi_text_property(name: str, docs: str) -> property: """Get a property that can occur several times and is text. Examples: Journal.descriptions, Event.comments """ def fget(self: Component) -> list[str]: """Get the values.""" descriptions = self.get(name) if descriptions is None: return [] if not isinstance(descriptions, SEQUENCE_TYPES): return [descriptions] return descriptions def fset(self: Component, values: str | Sequence[str] | None): """Set the values.""" fdel(self) if values is None: return if isinstance(values, str): self.add(name, values) else: for description in values: self.add(name, description) def fdel(self: Component): """Delete the values.""" self.pop(name) return property(fget, fset, fdel, docs) descriptions_property = multi_text_property( "DESCRIPTION", """DESCRIPTION provides a more complete description of the calendar component than that provided by the "SUMMARY" property. Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. Conformance: The property can be specified multiple times only within a "VJOURNAL" calendar component. Description: This property is used in the "VJOURNAL" calendar component to capture one or more textual journal entries. Examples: The following is an example of this property with formatted line breaks in the property value: .. code-block:: pycon DESCRIPTION:Meeting to provide technical review for "Phoenix" design.\\nHappy Face Conference Room. Phoenix design team MUST attend this meeting.\\nRSVP to team leader. """, # noqa: E501 ) comments_property = multi_text_property( "COMMENT", """COMMENT is used to specify a comment to the calendar user. Purpose: This property specifies non-processing information intended to provide a comment to the calendar user. Conformance: In :rfc:`5545`, this property can be specified multiple times in "VEVENT", "VTODO", "VJOURNAL", and "VFREEBUSY" calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components. In :rfc:`7953`, this property can be specified multiple times in "VAVAILABILITY" and "VAVAILABLE". Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. """, ) RECURRENCE_ID = create_single_property( "RECURRENCE-ID", "dt", (date, datetime), date | datetime, """ Identify a specific occurrence of a recurring calendar object. This property is used together with ``UID`` and ``SEQUENCE`` to refer to one particular instance in a recurrence set. The value is the original start date or datetime of that instance, not the rescheduled time. The value is usually a DATE-TIME and must use the same value type as the ``DTSTART`` property in the same component. A DATE value may be used for all-day items instead. This property corresponds to ``RECURRENCE-ID`` as defined in RFC 5545 and may appear in recurring ``VEVENT``, ``VTODO``, and ``VJOURNAL`` components. """, vDDDTypes, ) def _get_organizer(self: Component) -> vCalAddress | None: """ORGANIZER defines the organizer for a calendar component. Property Parameters: IANA, non-standard, language, common name, directory entry reference, and sent-by property parameters can be specified on this property. Conformance: This property MUST be specified in an iCalendar object that specifies a group-scheduled calendar entity. This property MUST be specified in an iCalendar object that specifies the publication of a calendar user's busy time. This property MUST NOT be specified in an iCalendar object that specifies only a time zone definition or that defines calendar components that are not group-scheduled components, but are components only on a single user's calendar. Description: This property is specified within the "VEVENT", "VTODO", and "VJOURNAL" calendar components to specify the organizer of a group-scheduled calendar entity. The property is specified within the "VFREEBUSY" calendar component to specify the calendar user requesting the free or busy time. When publishing a "VFREEBUSY" calendar component, the property is used to specify the calendar that the published busy time came from. The property has the property parameters "CN", for specifying the common or display name associated with the "Organizer", "DIR", for specifying a pointer to the directory information associated with the "Organizer", "SENT-BY", for specifying another calendar user that is acting on behalf of the "Organizer". The non-standard parameters may also be specified on this property. If the "LANGUAGE" property parameter is specified, the identified language applies to the "CN" parameter value. """ return self.get("ORGANIZER") def _set_organizer(self: Component, value: vCalAddress | str | None): """Set the value.""" _del_organizer(self) if value is not None: self.add("ORGANIZER", value) def _del_organizer(self: Component): """Delete the value.""" self.pop("ORGANIZER") organizer_property = property(_get_organizer, _set_organizer, _del_organizer) def single_string_enum_property( name: str, enum: type[StrEnum], default: StrEnum, docs: str ) -> property: """Create a property to access a single string value and convert it to an enum.""" prop = single_string_property(name, docs, default=default) def fget(self: Component) -> StrEnum: """Get the value.""" value = prop.fget(self) if value == default: return default return enum(str(value)) def fset(self: Component, value: str | StrEnum | None) -> None: """Set the value.""" if value == "": value = None prop.fset(self, value) return property(fget, fset, prop.fdel, doc=docs) busy_type_property = single_string_enum_property( "BUSYTYPE", BUSYTYPE, BUSYTYPE.BUSY_UNAVAILABLE, """BUSYTYPE specifies the default busy time type. Returns: :class:`icalendar.enums.BUSYTYPE` Description: This property is used to specify the default busy time type. The values correspond to those used by the "FBTYPE" parameter used on a "FREEBUSY" property, with the exception that the "FREE" value is not used in this property. If not specified on a component that allows this property, the default is "BUSY- UNAVAILABLE". """, ) def _make_repeat_property() -> property: from icalendar.config import _clamp_repeat _base = single_int_property( "REPEAT", 0, """The number of additional times the alarm is triggered after the initial trigger. Defaults to ``0``, meaning the alarm fires once. Must be paired with :attr:`~icalendar.cal.alarm.Alarm.DURATION`. Conforms with :rfc:`5545#section-3.8.6.2`. The value is capped at :data:`icalendar.config.MAX_ALARM_REPEAT` on read. Raises: TypeError: If the value is not an ``int``. Booleans are rejected, too, even though ``bool`` subclasses ``int``. ~icalendar.error.InvalidCalendar: If the value is negative. .. versionchanged:: 7.2.3 Negative values are no longer accepted. """, min_value=0, ) def fget(self): return _clamp_repeat(_base.fget(self)) return property(fget, _base.fset, _base.fdel, _base.__doc__) repeat_property = _make_repeat_property() priority_property = single_int_property( "PRIORITY", 0, """ Conformance: This property can be specified in "VEVENT" and "VTODO" calendar components according to :rfc:`5545`. :rfc:`7953` adds this property to "VAVAILABILITY". Description: This priority is specified as an integer in the range 0 to 9. A value of 0 specifies an undefined priority. A value of 1 is the highest priority. A value of 2 is the second highest priority. Subsequent numbers specify a decreasing ordinal priority. A value of 9 is the lowest priority. A CUA with a three-level priority scheme of "HIGH", "MEDIUM", and "LOW" is mapped into this property such that a property value in the range of 1 to 4 specifies "HIGH" priority. A value of 5 is the normal or "MEDIUM" priority. A value in the range of 6 to 9 is "LOW" priority. A CUA with a priority schema of "A1", "A2", "A3", "B1", "B2", ..., "C3" is mapped into this property such that a property value of 1 specifies "A1", a property value of 2 specifies "A2", a property value of 3 specifies "A3", and so forth up to a property value of 9 specifies "C3". Other integer values are reserved for future use. Within a "VEVENT" calendar component, this property specifies a priority for the event. This property may be useful when more than one event is scheduled for a given time period. Within a "VTODO" calendar component, this property specified a priority for the to-do. This property is useful in prioritizing multiple action items for a given time period. Raises: TypeError: If the value is not an ``int``. Booleans are rejected, too, even though ``bool`` subclasses ``int``. ~icalendar.error.InvalidCalendar: If the value is negative. .. versionchanged:: 7.2.3 Negative values are no longer accepted. """, min_value=0, ) class_property = single_string_enum_property( "CLASS", CLASS, CLASS.PUBLIC, """CLASS specifies the class of the calendar component. Returns: :class:`icalendar.enums.CLASS` Description: An access classification is only one component of the general security system within a calendar application. It provides a method of capturing the scope of the access the calendar owner intends for information within an individual calendar entry. The access classification of an individual iCalendar component is useful when measured along with the other security components of a calendar system (e.g., calendar user authentication, authorization, access rights, access role, etc.). Hence, the semantics of the individual access classifications cannot be completely defined by this memo alone. Additionally, due to the "blind" nature of most exchange processes using this memo, these access classifications cannot serve as an enforcement statement for a system receiving an iCalendar object. Rather, they provide a method for capturing the intention of the calendar owner for the access to the calendar component. If not specified in a component that allows this property, the default value is PUBLIC. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the PRIVATE value. """, ) transparency_property = single_string_enum_property( "TRANSP", TRANSP, TRANSP.OPAQUE, """TRANSP defines whether or not an event is transparent to busy time searches. Returns: :class:`icalendar.enums.TRANSP` Description: Time Transparency is the characteristic of an event that determines whether it appears to consume time on a calendar. Events that consume actual time for the individual or resource associated with the calendar SHOULD be recorded as OPAQUE, allowing them to be detected by free/busy time searches. Other events, which do not take up the individual's (or resource's) time SHOULD be recorded as TRANSPARENT, making them invisible to free/ busy time searches. """, ) status_property = single_string_enum_property( "STATUS", STATUS, "", """STATUS defines the overall status or confirmation for the calendar component. Returns: :class:`icalendar.enums.STATUS` The default value is ``""``. Description: In a group-scheduled calendar component, the property is used by the "Organizer" to provide a confirmation of the event to the "Attendees". For example in a "VEVENT" calendar component, the "Organizer" can indicate that a meeting is tentative, confirmed, or cancelled. In a "VTODO" calendar component, the "Organizer" can indicate that an action item needs action, is completed, is in process or being worked on, or has been cancelled. In a "VJOURNAL" calendar component, the "Organizer" can indicate that a journal entry is draft, final, or has been cancelled or removed. """, ) url_property = single_string_property( "URL", """A Uniform Resource Locator (URL) associated with a calendar component. This property specifies a URI where a more dynamic rendition of the calendar information can be found. It is commonly used to reference related resources or provide additional information about the component. According to :rfc:`5545#section-3.8.4.6`, this property can be specified once in "VEVENT", "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components. Since :rfc:`7986#section-5.5`, this property can also be defined on a "VCALENDAR". :rfc:`7953#section-3.1` allows this property in "VAVAILABILITY" components. This property may be used in a calendar component to convey a location where a more dynamic rendition of the calendar information can be found. If both the URL property and Content-Location MIME header are specified, they MUST point to the same resource. This differs from the SOURCE property, which identifies where calendar data can be refreshed from, whereas URL provides an alternative representation of the current calendar data. Examples: Set a URL for an event that references additional information: .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.add('url', 'http://example.com/events/meeting-2025') >>> print(event.to_ical().decode('utf-8')) BEGIN:VEVENT URL:http://example.com/events/meeting-2025 END:VEVENT Set a URL for a calendar: .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar() >>> calendar.add('url', 'http://example.com/pub/calendars/jsmith/mytime.ics') >>> print(calendar.to_ical().decode('utf-8')) BEGIN:VCALENDAR URL:http://example.com/pub/calendars/jsmith/mytime.ics END:VCALENDAR See also: :attr:`~icalendar.cal.calendar.Calendar.source` for specifying from where calendar data can be refreshed. icalendar implementations: - :attr:`Availability.url ` - :attr:`Calendar.url ` - :attr:`Event.url ` - :attr:`FreeBusy.url ` - :attr:`Journal.url ` - :attr:`Todo.url ` """, ) source_property = single_string_property( "SOURCE", """A URI from where calendar data can be refreshed. Description: This property identifies a location where a client can retrieve updated data for the calendar. Clients SHOULD honor any specified "REFRESH-INTERVAL" value when periodically retrieving data. Note that this property differs from the "URL" property in that "URL" is meant to provide an alternative representation of the calendar data rather than the original location of the data. Conformance: This property can be specified once in an iCalendar object. Example: The following is an example of this property: .. code-block:: ics SOURCE;VALUE=URI:https://example.com/holidays.ics """, ) location_property = multi_language_text_property( "LOCATION", None, """The intended venue for the activity defined by a calendar component. Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. Conformance: Since :rfc:`5545`, this property can be specified in "VEVENT" or "VTODO" calendar component. :rfc:`7953` adds this property to "VAVAILABILITY" and "VAVAILABLE". Description: Specific venues such as conference or meeting rooms may be explicitly specified using this property. An alternate representation may be specified that is a URI that points to directory information with more structured specification of the location. For example, the alternate representation may specify either an LDAP URL :rfc:`4516` pointing to an LDAP server entry or a CID URL :rfc:`2392` pointing to a MIME body part containing a Virtual-Information Card (vCard) :rfc:`2426` for the location. """, ) contacts_property = multi_text_property( "CONTACT", """Contact information associated with the calendar component. Purpose: This property is used to represent contact information or alternately a reference to contact information associated with the calendar component. Property Parameters: IANA, non-standard, alternate text representation, and language property parameters can be specified on this property. Conformance: In :rfc:`5545`, this property can be specified in a "VEVENT", "VTODO", "VJOURNAL", or "VFREEBUSY" calendar component. In :rfc:`7953`, this property can be specified in a "VAVAILABILITY" amd "VAVAILABLE" calendar component. Description: The property value consists of textual contact information. An alternative representation for the property value can also be specified that refers to a URI pointing to an alternate form, such as a vCard :rfc:`2426`, for the contact information. Example: The following is an example of this property referencing textual contact information: .. code-block:: ics CONTACT:Jim Dolittle\\, ABC Industries\\, +1-919-555-1234 The following is an example of this property with an alternate representation of an LDAP URI to a directory entry containing the contact information: .. code-block:: ics CONTACT;ALTREP="ldap://example.com:6666/o=ABC%20Industries\\, c=US???(cn=Jim%20Dolittle)":Jim Dolittle\\, ABC Industries\\, +1-919-555-1234 The following is an example of this property with an alternate representation of a MIME body part containing the contact information, such as a vCard :rfc:`2426` embedded in a text/ directory media type :rfc:`2425`: .. code-block:: ics CONTACT;ALTREP="CID:part3.msg970930T083000SILVER@example.com": Jim Dolittle\\, ABC Industries\\, +1-919-555-1234 The following is an example of this property referencing a network resource, such as a vCard :rfc:`2426` object containing the contact information: .. code-block:: ics CONTACT;ALTREP="http://example.com/pdi/jdoe.vcf":Jim Dolittle\\, ABC Industries\\, +1-919-555-1234 """, ) def _timezone_datetime_property(name: str, docs: str): """Create a property to access the values with a proper timezone.""" return single_utc_property(name, docs) rfc_7953_dtstart_property = _timezone_datetime_property( "DTSTART", """Start of the component. This is almost the same as :attr:`Event.DTSTART ` with one exception: The values MUST have a timezone and DATE is not allowed. Description: :rfc:`7953`: If specified, the "DTSTART" and "DTEND" properties in "VAVAILABILITY" components and "AVAILABLE" subcomponents MUST be "DATE-TIME" values specified as either the date with UTC time or the date with local time and a time zone reference. """, ) rfc_7953_dtend_property = _timezone_datetime_property( "DTEND", """Start of the component. This is almost the same as :attr:`Event.DTEND ` with one exception: The values MUST have a timezone and DATE is not allowed. Description: :rfc:`7953`: If specified, the "DTSTART" and "DTEND" properties in "VAVAILABILITY" components and "AVAILABLE" subcomponents MUST be "DATE-TIME" values specified as either the date with UTC time or the date with local time and a time zone reference. """, ) @property def rfc_7953_duration_property(self) -> timedelta | None: """Compute the duration of this component. If there is no :attr:`DTEND` or :attr:`DURATION` set, this is None. Otherwise, the duration is calculated from :attr:`DTSTART` and :attr:`DTEND`/:attr:`DURATION`. This is in accordance with :rfc:`7953`: If "DTEND" or "DURATION" are not present, then the end time is unbounded. """ duration = self.DURATION if duration: return duration end = self.DTEND if end is None: return None start = self.DTSTART if start is None: raise IncompleteComponent("Cannot compute duration without start.") return end - start @property def rfc_7953_end_property(self) -> timedelta | None: """Compute the duration of this component. If there is no :attr:`DTEND` or :attr:`DURATION` set, this is None. Otherwise, the duration is calculated from :attr:`DTSTART` and :attr:`DTEND`/:attr:`DURATION`. This is in accordance with :rfc:`7953`: If "DTEND" or "DURATION" are not present, then the end time is unbounded. """ duration = self.DURATION if duration: start = self.DTSTART if start is None: raise IncompleteComponent("Cannot compute end without start.") return start + duration end = self.DTEND if end is None: return None return end @rfc_7953_end_property.setter def rfc_7953_end_property(self, value: datetime): self.DTEND = value @rfc_7953_end_property.deleter def rfc_7953_end_property(self): del self.DTEND def get_start_end_duration_with_validation( component: Component, start_property: str, end_property: str, component_name: str, ) -> tuple[date | datetime | None, date | datetime | None, timedelta | None]: """ Validate the component and return start, end, and duration. This tests validity according to :rfc:`5545` rules for ``Event`` and ``Todo`` components. Parameters: component: The component to validate, either ``Event`` or ``Todo``. start_property: The start property name, ``DTSTART``. end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE`` for ``Todo``. component_name: The component name for error messages, either ``VEVENT`` or ``VTODO``. Returns: tuple: (start, end, duration) values from the component. Raises: ~error.InvalidCalendar: If the component violates RFC 5545 constraints. """ start = getattr(component, start_property, None) end = getattr(component, end_property, None) duration = component.DURATION # RFC 5545: Only one of end property and DURATION may be present if duration is not None and end is not None: end_name = "DTEND" if end_property == "DTEND" else "DUE" msg = ( f"Only one of {end_name} and DURATION " f"may be in a {component_name}, not both." ) raise InvalidCalendar(msg) # RFC 5545: When DTSTART is a date, DURATION must be of days or weeks if ( start is not None and is_date(start) and duration is not None and duration.seconds != 0 ): msg = "When DTSTART is a date, DURATION must be of days or weeks." raise InvalidCalendar(msg) # RFC 5545: DTSTART and end property must be of the same type if start is not None and end is not None and is_date(start) != is_date(end): end_name = "DTEND" if end_property == "DTEND" else "DUE" msg = ( f"DTSTART and {end_name} must be of the same type, either date or datetime." ) raise InvalidCalendar(msg) return start, end, duration def get_start_property(component: Component) -> date | datetime: """ Get the start property with validation. Parameters: component: The component from which to get its start property. Returns: The ``DTSTART`` value. Raises: ~error.IncompleteComponent: If no ``DTSTART`` is present. """ # Trigger validation by calling _get_start_end_duration start, _end, _duration = component._get_start_end_duration() # noqa: SLF001 if start is None: msg = "No DTSTART given." raise IncompleteComponent(msg) return start def get_end_property(component: Component, end_property: str) -> date | datetime: """ Get the end property with fallback logic for ``Event`` and ``Todo`` components. Parameters: component: The component to get end from end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE`` for ``Todo``. Returns: The computed end value. Raises: ~error.IncompleteComponent: If the provided information is incomplete to compute the end property. """ # Trigger validation by calling _get_start_end_duration start, end, duration = component._get_start_end_duration() # noqa: SLF001 if end is None and duration is None: if start is None: end_name = "DTEND" if end_property == "DTEND" else "DUE" msg = f"No {end_name} or DURATION+DTSTART given." raise IncompleteComponent(msg) # Default behavior differs for Event vs Todo: # Event: date gets +1 day, datetime gets same time # Todo: both date and datetime get same time (issue #898) if end_property == "DTEND" and is_date(start): return start + timedelta(days=1) return start if duration is not None: if start is not None: if component.name == "VEVENT" and duration.total_seconds() <= 0: return start return start + duration end_name = "DTEND" if end_property == "DTEND" else "DUE" msg = f"No {end_name} or DURATION+DTSTART given." raise IncompleteComponent(msg) return end def get_duration_property(component: Component) -> timedelta: """ Get the duration property with fallback calculation from start and end. Parameters: component: The component from which to get its duration property. Returns: The duration as a timedelta. """ # First check if DURATION property is explicitly set if "DURATION" in component: return component["DURATION"].dt # Fall back to calculated duration from start and end return component.end - component.start def set_duration_with_locking( component: Component, duration: timedelta | None, locked: Literal["start", "end"], end_property: str, ) -> None: """ Set the duration with explicit locking behavior for ``Event`` and ``Todo``. Parameters: component: The component to modify, either ``Event`` or ``Todo``. duration: The duration to set, or ``None`` to convert to ``DURATION`` property. locked: Which property to keep unchanged, either ``start`` or ``end``. end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE`` for ``Todo``. """ # Convert to DURATION property if duration is None if duration is None: if "DURATION" in component: return # Already has DURATION property current_duration = component.duration component.DURATION = current_duration return if not isinstance(duration, timedelta): msg = f"Use timedelta, not {type(duration).__name__}." raise TypeError(msg) # Validate date/duration compatibility start = component.DTSTART if start is not None and is_date(start) and duration.seconds != 0: msg = "When DTSTART is a date, DURATION must be of days or weeks." raise InvalidCalendar(msg) if locked == "start": # Keep start locked, adjust end if start is None: msg = "Cannot set duration without DTSTART. Set start time first." raise IncompleteComponent(msg) component.pop(end_property, None) # Remove end property component.DURATION = duration elif locked == "end": # Keep end locked, adjust start current_end = component.end component.DTSTART = current_end - duration component.pop(end_property, None) # Remove end property component.DURATION = duration else: msg = f"locked must be 'start' or 'end', not {locked!r}" raise ValueError(msg) def set_start_with_locking( component: Component, start: date | datetime, locked: Literal["duration", "end"] | None, end_property: str, ) -> None: """ Set the start with explicit locking behavior for ``Event`` and ``Todo`` components. Parameters: component: The component to modify, either ``Event`` or ``Todo``. start: The start time to set. locked: Which property to keep unchanged, either ``duration``, ``end``, or ``None`` for auto-detect. end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE`` for ``Todo``. """ if locked is None: # Auto-detect based on existing properties if "DURATION" in component: locked = "duration" elif end_property in component: locked = "end" else: # Default to duration if no existing properties locked = "duration" if locked == "duration": # Keep duration locked, adjust end current_duration = ( component.duration if "DURATION" in component or end_property in component else None ) component.DTSTART = start if current_duration is not None: component.pop(end_property, None) # Remove end property component.DURATION = current_duration elif locked == "end": # Keep end locked, adjust duration current_end = component.end component.DTSTART = start component.pop("DURATION", None) # Remove duration property setattr(component, end_property, current_end) else: msg = f"locked must be 'duration', 'end', or None, not {locked!r}" raise ValueError(msg) def set_end_with_locking( component: Component, end: date | datetime, locked: Literal["start", "duration"], end_property: str, ) -> None: """ Set the end with explicit locking behavior for Event and Todo components. Parameters: component: The component to modify, either ``Event`` or ``Todo``. end: The end time to set. locked: Which property to keep unchanged, either ``start`` or ``duration``. end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE`` for ``Todo``. """ if locked == "start": # Keep start locked, adjust duration component.pop("DURATION", None) # Remove duration property setattr(component, end_property, end) elif locked == "duration": # Keep duration locked, adjust start current_duration = component.duration component.DTSTART = end - current_duration component.pop(end_property, None) # Remove end property component.DURATION = current_duration else: msg = f"locked must be 'start' or 'duration', not {locked!r}" raise ValueError(msg) def _get_images(self: Component) -> list[Image]: """IMAGE specifies an image associated with the calendar or a calendar component. Description: This property specifies an image for an iCalendar object or a calendar component via a URI or directly with inline data that can be used by calendar user agents when presenting the calendar data to a user. Multiple properties MAY be used to specify alternative sets of images with, for example, varying media subtypes, resolutions, or sizes. When multiple properties are present, calendar user agents SHOULD display only one of them, picking one that provides the most appropriate image quality, or display none. The "DISPLAY" parameter is used to indicate the intended display mode for the image. The "ALTREP" parameter, defined in :rfc:`5545`, can be used to provide a "clickable" image where the URI in the parameter value can be "launched" by a click on the image in the calendar user agent. Conformance: This property can be specified multiple times in an iCalendar object or in "VEVENT", "VTODO", or "VJOURNAL" calendar components. .. note:: At the present moment, this property is read-only. If you require a setter, please open an issue or a pull request. """ images = self.get("IMAGE", []) if not isinstance(images, SEQUENCE_TYPES): images = [images] return [Image.from_property_value(img) for img in images] images_property = property(_get_images) def _get_conferences(self: Component) -> list[Conference]: """Return the CONFERENCE properties as a list. Purpose: This property specifies information for accessing a conferencing system. Conformance: This property can be specified multiple times in a "VEVENT" or "VTODO" calendar component. Description: This property specifies information for accessing a conferencing system for attendees of a meeting or task. This might be for a telephone-based conference number dial-in with access codes included (such as a tel: URI :rfc:`3966` or a sip: or sips: URI :rfc:`3261`), for a web-based video chat (such as an http: or https: URI :rfc:`7230`), or for an instant messaging group chat room (such as an xmpp: URI :rfc:`5122`). If a specific URI for a conferencing system is not available, a data: URI :rfc:`2397` containing a text description can be used. A conference system can be a bidirectional communication channel or a uni-directional "broadcast feed". The "FEATURE" property parameter is used to describe the key capabilities of the conference system to allow a client to choose the ones that give the required level of interaction from a set of multiple properties. The "LABEL" property parameter is used to convey additional details on the use of the URI. For example, the URIs or access codes for the moderator and attendee of a teleconference system could be different, and the "LABEL" property parameter could be used to "tag" each "CONFERENCE" property to indicate which is which. The "LANGUAGE" property parameter can be used to specify the language used for text values used with this property (as per Section 3.2.10 of :rfc:`5545`). Example: The following are examples of this property: .. code-block:: ics CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR; LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123 CONFERENCE;VALUE=URI;FEATURE=CHAT; LABEL=Chat room:xmpp:chat-123@conference.example.com CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO; LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456 Get all conferences: .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.conferences [] Set a conference: .. code-block:: pycon >>> from icalendar import Event, Conference >>> event = Event() >>> event.conferences = [ ... Conference( ... "tel:+1-412-555-0123,,,654321", ... feature="PHONE,MODERATOR", ... label="Moderator dial-in", ... language="EN", ... ) ... ] >>> print(event.to_ical()) BEGIN:VEVENT CONFERENCE;FEATURE="PHONE,MODERATOR";LABEL=Moderator dial-in;LANGUAGE=EN;V ALUE=URI:tel:+1-412-555-0123,,,654321 END:VEVENT """ conferences = self.get("CONFERENCE", []) if not isinstance(conferences, SEQUENCE_TYPES): conferences = [conferences] return [Conference.from_uri(conference) for conference in conferences] def _set_conferences(self: Component, conferences: list[Conference] | None): """Set the conferences.""" _del_conferences(self) for conference in conferences or []: self.add("CONFERENCE", conference.to_uri()) def _del_conferences(self: Component): """Delete all conferences.""" self.pop("CONFERENCE") conferences_property = property(_get_conferences, _set_conferences, _del_conferences) def _get_links(self: Component) -> list[vUri | vUid | vXmlReference]: """LINK properties as a list. Purpose: LINK provides a reference to external information related to a component. Property Parameters: The VALUE parameter is required. Non-standard, link relation type, format type, label, and language parameters can also be specified on this property. The LABEL parameter is defined in :rfc:`7986`. Conformance: This property can be specified zero or more times in any iCalendar component. LINK is specified in :rfc:`9253`. The LINKREL parameter is required. Description: When used in a component, the value of this property points to additional information related to the component. For example, it may reference the originating web server. This property is a serialization of the model in :rfc:`8288`, where the link target is carried in the property value, the link context is the containing calendar entity, and the link relation type and any target attributes are carried in iCalendar property parameters. The LINK property parameters map to :rfc:`8288` attributes as follows: LABEL This parameter maps to the "title" attribute defined in Section 3.4.1 of :rfc:`8288`. LABEL is used to label the destination of a link such that it can be used as a human-readable identifier (e.g., a menu entry) in the language indicated by the LANGUAGE (if present). LANGUAGE This parameter maps to the "hreflang" attribute defined in Section 3.4.1 of :rfc:`8288`. See :rfc:`5646`. Example: ``en``, ``de-ch``. LINKREL This parameter maps to the link relation type defined in Section 2.1 of :rfc:`8288`. See `Registered Link Relation Types `_. FMTTYPE This parameter maps to the "type" attribute defined in Section 3.4.1 of :rfc:`8288`. There is no mapping for "title*", "anchor", "rev", or "media" :rfc:`8288`. Examples: The following is an example of this property, which provides a reference to the source for the calendar object. .. code-block:: ics LINK;LINKREL=SOURCE;LABEL=Venue;VALUE=URI: https://example.com/events The following is an example of this property, which provides a reference to an entity from which this one was derived. The link relation is a vendor-defined value. .. code-block:: ics LINK;LINKREL="https://example.com/linkrel/derivedFrom"; VALUE=URI: https://example.com/tasks/01234567-abcd1234.ics The following is an example of this property, which provides a reference to a fragment of an XML document. The link relation is a vendor-defined value. .. code-block:: ics LINK;LINKREL="https://example.com/linkrel/costStructure"; VALUE=XML-REFERENCE: https://example.com/xmlDocs/bidFramework.xml #xpointer(descendant::CostStruc/range-to( following::CostStrucEND[1])) Set a link :class:`icalendar.prop.uri.vUri` to the event page: .. code-block:: pycon >>> from icalendar import Event, vUri >>> from datetime import datetime >>> link = vUri( ... "http://example.com/event-page", ... params={"LINKREL":"SOURCE"} ... ) >>> event = Event.new( ... start=datetime(2025, 9, 17, 12, 0), ... summary="An Example Event with a page" ... ) >>> event.links = [link] >>> print(event.to_ical()) BEGIN:VEVENT SUMMARY:An Example Event with a page DTSTART:20250917T120000 DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 LINK;LINKREL="SOURCE":http://example.com/event-page END:VEVENT """ links = self.get("LINK", []) if not isinstance(links, list): links = [links] return links LINKS_TYPE_SETTER: TypeAlias = ( str | vUri | vUid | vXmlReference | None | list[str | vUri | vUid | vXmlReference] ) def _set_links(self: Component, links: LINKS_TYPE_SETTER) -> None: """Set the LINKs.""" _del_links(self) if links is None: return if isinstance(links, (str, vUri, vUid, vXmlReference)): links = [links] for link in links: if type(link) is str: link = vUri(link, params={"VALUE": "URI"}) # noqa: PLW2901 self.add("LINK", link) def _del_links(self: Component) -> None: """Delete all links.""" self.pop("LINK") links_property = property(_get_links, _set_links, _del_links) RELATED_TO_TYPE_SETTER: TypeAlias = ( None | str | vText | vUri | vUid | list[str | vText | vUri | vUid] ) def _get_related_to(self: Component) -> list[vText | vUri | vUid]: """RELATED-TO properties as a list. Purpose: This property is used to represent a relationship or reference between one calendar component and another. :rfc:`9523` allows URI or UID values and a GAP parameter. Value Type: :rfc:`5545`: TEXT :rfc:`9253`: URI, UID Conformance: Since :rfc:`5545`. this property can be specified in the "VEVENT", "VTODO", and "VJOURNAL" calendar components. Since :rfc:`9523`, this property MAY be specified in any iCalendar component. Description (:rfc:`5545`): The property value consists of the persistent, globally unique identifier of another calendar component. This value would be represented in a calendar component by the "UID" property. By default, the property value points to another calendar component that has a PARENT relationship to the referencing object. The "RELTYPE" property parameter is used to either explicitly state the default PARENT relationship type to the referenced calendar component or to override the default PARENT relationship type and specify either a CHILD or SIBLING relationship. The PARENT relationship indicates that the calendar component is a subordinate of the referenced calendar component. The CHILD relationship indicates that the calendar component is a superior of the referenced calendar component. The SIBLING relationship indicates that the calendar component is a peer of the referenced calendar component. Changes to a calendar component referenced by this property can have an implicit impact on the related calendar component. For example, if a group event changes its start or end date or time, then the related, dependent events will need to have their start and end dates changed in a corresponding way. Similarly, if a PARENT calendar component is cancelled or deleted, then there is an implied impact to the related CHILD calendar components. This property is intended only to provide information on the relationship of calendar components. It is up to the target calendar system to maintain any property implications of this relationship. Description (:rfc:`9253`): By default or when VALUE=UID is specified, the property value consists of the persistent, globally unique identifier of another calendar component. This value would be represented in a calendar component by the UID property. By default, the property value points to another calendar component that has a PARENT relationship to the referencing object. The RELTYPE property parameter is used to either explicitly state the default PARENT relationship type to the referenced calendar component or to override the default PARENT relationship type and specify either a CHILD or SIBLING relationship or a temporal relationship. The PARENT relationship indicates that the calendar component is a subordinate of the referenced calendar component. The CHILD relationship indicates that the calendar component is a superior of the referenced calendar component. The SIBLING relationship indicates that the calendar component is a peer of the referenced calendar component. To preserve backwards compatibility, the value type MUST be UID when the PARENT, SIBLING, or CHILD relationships are specified. The FINISHTOSTART, FINISHTOFINISH, STARTTOFINISH, or STARTTOSTART relationships define temporal relationships, as specified in the RELTYPE parameter definition. The FIRST and NEXT define ordering relationships between calendar components. The DEPENDS-ON relationship indicates that the current calendar component depends on the referenced calendar component in some manner. For example, a task may be blocked waiting on the other, referenced, task. The REFID and CONCEPT relationships establish a reference from the current component to the referenced component. Changes to a calendar component referenced by this property can have an implicit impact on the related calendar component. For example, if a group event changes its start or end date or time, then the related, dependent events will need to have their start and end dates and times changed in a corresponding way. Similarly, if a PARENT calendar component is canceled or deleted, then there is an implied impact to the related CHILD calendar components. This property is intended only to provide information on the relationship of calendar components. Deletion of the target component, for example, the target of a FIRST, NEXT, or temporal relationship, can result in broken links. It is up to the target calendar system to maintain any property implications of these relationships. Examples: :rfc:`5545` examples of this property: .. code-block:: ics RELATED-TO:jsmith.part7.19960817T083000.xyzMail@example.com .. code-block:: ics RELATED-TO:19960401-080045-4000F192713-0052@example.com :rfc:`9253` examples of this property: .. code-block:: ics RELATED-TO;VALUE=URI;RELTYPE=STARTTOFINISH: https://example.com/caldav/user/jb/cal/ 19960401-080045-4000F192713.ics See also :class:`icalendar.enums.RELTYPE`. """ result = self.get("RELATED-TO", []) if not isinstance(result, list): return [result] return result def _set_related_to(self: Component, values: RELATED_TO_TYPE_SETTER) -> None: """Set the RELATED-TO properties.""" _del_related_to(self) if values is None: return if not isinstance(values, list): values = [values] for value in values: self.add("RELATED-TO", value) def _del_related_to(self: Component): """Delete the RELATED-TO properties.""" self.pop("RELATED-TO", None) related_to_property = property(_get_related_to, _set_related_to, _del_related_to) def _get_concepts(self: Component) -> list[vUri]: """CONCEPT Purpose: CONCEPT defines the formal categories for a calendar component. Conformance: Since :rfc:`9253`, this property can be specified zero or more times in any iCalendar component. Description: This property is used to specify formal categories or classifications of the calendar component. The values are useful in searching for a calendar component of a particular type and category. This categorization is distinct from the more informal "tagging" of components provided by the existing CATEGORIES property. It is expected that the value of the CONCEPT property will reference an external resource that provides information about the categorization. In addition, a structured URI value allows for hierarchical categorization of events. Possible category resources are the various proprietary systems, for example, the Library of Congress, or an open source of categorization data. Examples: The following is an example of this property. It points to a server acting as the source for the calendar object. .. code-block:: ics CONCEPT:https://example.com/event-types/arts/music .. seealso:: :attr:`icalendar.prop.categories.vCategory` """ concepts = self.get("CONCEPT", []) if not isinstance(concepts, list): concepts = [concepts] return concepts CONCEPTS_TYPE_SETTER: TypeAlias = list[vUri | str] | str | vUri | None def _set_concepts(self: Component, concepts: CONCEPTS_TYPE_SETTER): """Set the concepts.""" _del_concepts(self) if concepts is None: return if not isinstance(concepts, list): concepts = [concepts] for value in concepts: self.add("CONCEPT", value) def _del_concepts(self: Component): """Delete the concepts.""" self.pop("CONCEPT", None) concepts_property = property(_get_concepts, _set_concepts, _del_concepts) def multi_string_property(name: str, doc: str): """A property for an iCalendar Property that can occur multiple times.""" def fget(self: Component) -> list[str]: """Get the values of a multi-string property.""" value = self.get(name, []) if not isinstance(value, list): value = [value] return value def fset(self: Component, value: list[str] | str | None) -> None: """Set the values of a multi-string property.""" fdel(self) if value is None: return if not isinstance(value, list): value = [value] for value in value: self.add(name, value) def fdel(self: Component): """Delete the values of a multi-string property.""" self.pop(name, None) return property(fget, fset, fdel, doc=doc) refids_property = multi_string_property( "REFID", """REFID Purpose: REFID acts as a key for associated iCalendar entities. Conformance: Since :rfc:`9253`, this property can be specified zero or more times in any iCalendar component. Description: The value of this property is free-form text that creates an identifier for associated components. All components that use the same REFID value are associated through that value and can be located or retrieved as a group. For example, all of the events in a travel itinerary would have the same REFID value, so as to be grouped together. Examples: The following is an example of this property. .. code-block:: ics REFID:itinerary-2014-11-17 Use a REFID to associate several VTODOs: .. code-block:: pycon >>> from icalendar import Todo >>> todo_1 = Todo.new( ... summary="turn off stove", ... refids=["travel", "alps"] ... ) >>> todo_2 = Todo.new( ... summary="pack backpack", ... refids=["travel", "alps"] ... ) >>> todo_1.refids == todo_2.refids True .. note:: List modifications do not modify the component. """, ) REQUEST_STATUS_property = multi_string_property( "REQUEST-STATUS", """This property defines the status code returned for a scheduling request. Setting this property replaces all existing REQUEST-STATUS values. A :class:`str` is stored as-is. Setting ``None`` or an empty list removes all REQUEST-STATUS values, as does deleting the property. The value consists of a short return status component, a longer return status description component, and optionally a status-specific data component, separated by semicolons (statcode;statdesc[;extdata]). The return status components are defined in :rfc:`5545#section-3.8.8.3`. Note: List modifications do not modify the component. Methods such as ``append()``, ``extend()``, and ``remove()``, as well as item assignment, act on a copy. Assign the list back to the property, or use :meth:`Component.add ` with a typed value instead. Parameters: request_status(str | list[str] | None): A single status string, or a list of status strings to set. Example: Add a request status to an event: .. code-block:: pycon >>> from icalendar import Event >>> event = Event.new(request_status="2.0;Success") >>> event.REQUEST_STATUS == ["2.0;Success"] True """, ) __all__ = [ "CONCEPTS_TYPE_SETTER", "LINKS_TYPE_SETTER", "RECURRENCE_ID", "RELATED_TO_TYPE_SETTER", "REQUEST_STATUS_property", "attendees_property", "busy_type_property", "categories_property", "class_property", "color_property", "comments_property", "concepts_property", "conferences_property", "contacts_property", "create_single_property", "description_property", "descriptions_property", "duration_property", "exdates_property", "get_duration_property", "get_end_property", "get_start_end_duration_with_validation", "get_start_property", "images_property", "links_property", "location_property", "multi_language_text_property", "multi_string_property", "organizer_property", "priority_property", "property_del_duration", "property_doc_duration_template", "property_get_duration", "property_set_duration", "rdates_property", "refids_property", "related_to_property", "repeat_property", "rfc_7953_dtend_property", "rfc_7953_dtstart_property", "rfc_7953_duration_property", "rfc_7953_end_property", "rrules_property", "sequence_property", "set_duration_with_locking", "set_end_with_locking", "set_start_with_locking", "single_int_property", "single_utc_property", "source_property", "status_property", "summary_property", "transparency_property", "uid_property", "url_property", ] collective-icalendar-138c845/src/icalendar/cal/000077500000000000000000000000001524134033600213155ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/cal/__init__.py000066400000000000000000000014711524134033600234310ustar00rootroot00000000000000"""Calendar is a dictionary like Python object that can render itself as VCAL files according to RFC 5545. These are the defined components. """ from .alarm import Alarm from .availability import Availability from .available import Available from .calendar import Calendar from .component import Component from .component_factory import ComponentFactory from .event import Event from .free_busy import FreeBusy from .journal import Journal from .lazy import LazyCalendar from .timezone import Timezone, TimezoneDaylight, TimezoneStandard from .todo import Todo __all__ = [ "Alarm", "Availability", "Available", "Calendar", "Component", "ComponentFactory", "Event", "FreeBusy", "Journal", "LazyCalendar", "Timezone", "TimezoneDaylight", "TimezoneStandard", "Todo", ] collective-icalendar-138c845/src/icalendar/cal/alarm.py000066400000000000000000000637221524134033600227750ustar00rootroot00000000000000""":rfc:`5545` VALARM component.""" from __future__ import annotations from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, NamedTuple from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, attendees_property, create_single_property, description_property, property_del_duration, property_get_duration, property_set_duration, repeat_property, single_int_property, single_string_property, single_utc_property, summary_property, uid_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example from icalendar.error import InvalidCalendar from icalendar.prop.binary import vBinary if TYPE_CHECKING: import uuid from collections.abc import Sequence from icalendar.compatibility import Self from icalendar.prop import vCalAddress class Alarm(Component): """ A "VALARM" calendar component is a grouping of component properties that defines an alarm or reminder for an event or a to-do. For example, it may be used to define a reminder for a pending event or an overdue to-do. Example: The following example creates an alarm which uses an audio file from an FTP server. .. code-block:: pycon >>> from icalendar import Alarm >>> alarm = Alarm.example() >>> print(alarm.to_ical().decode()) BEGIN:VALARM ACTION:AUDIO ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud DURATION:PT15M REPEAT:4 TRIGGER;VALUE=DATE-TIME:19970317T133000Z END:VALARM """ name = "VALARM" # some properties MAY/MUST/MUST NOT appear depending on ACTION value required = ( "ACTION", "TRIGGER", ) singletons = ( "ACTION", "DESCRIPTION", "SUMMARY", "TRIGGER", "DURATION", "REPEAT", "UID", "PROXIMITY", "ACKNOWLEDGED", ) inclusive = ( ( "DURATION", "REPEAT", ), ( "SUMMARY", "ATTENDEE", ), ) multiple = ("ATTENDEE", "ATTACH", "RELATED-TO") REPEAT = single_int_property( "REPEAT", 0, """The number of additional times the alarm is triggered after the initial trigger. Defaults to ``0``, meaning the alarm fires once. To repeat the alarm, set both :attr:`REPEAT` and :attr:`DURATION`. The :attr:`DURATION` sets the gap between repetitions. :attr:`REPEAT` is the count of *additional* triggers, so a :attr:`REPEAT` of ``2`` produces three alarms in total (the initial trigger plus two repeats). Conforming with :rfc:`5545#section-3.8.6.2`, this property can appear once in an :class:`~icalendar.cal.alarm.Alarm` component and must be paired with :attr:`DURATION`. Example: Build an alarm that fires once and then repeats twice at five-minute intervals. .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Alarm >>> alarm = Alarm() >>> alarm.TRIGGER = timedelta(minutes=-15) >>> alarm.DURATION = timedelta(minutes=5) >>> alarm.REPEAT = 2 >>> alarm.REPEAT 2 Raises: TypeError: If the value is not an ``int``. Booleans are rejected, too, even though ``bool`` subclasses ``int``. ~icalendar.error.InvalidCalendar: If the value is negative. .. versionchanged:: 7.2.3 Negative values are no longer accepted. """, min_value=0, ) DURATION = property( property_get_duration, property_set_duration, property_del_duration, """The delay between repeated triggers of a repeating alarm. Returns a :class:`datetime.timedelta` or ``None`` when the alarm has no :attr:`DURATION` set. Setting this attribute accepts a :class:`~datetime.timedelta`; deleting it removes the property from the component. :attr:`DURATION` is meaningful only for repeating alarms and must be paired with :attr:`REPEAT`. The two together produce :attr:`REPEAT` additional triggers, each spaced by :attr:`DURATION` after the initial trigger. Conforming with :rfc:`5545#section-3.8.2.5`, the :attr:`DURATION` property can appear once in an :class:`~icalendar.cal.alarm.Alarm` component. Example: Pair :attr:`DURATION` with :attr:`REPEAT` to produce three triggers spaced ten minutes apart. .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Alarm >>> alarm = Alarm() >>> alarm.TRIGGER = timedelta(minutes=-30) >>> alarm.DURATION = timedelta(minutes=10) >>> alarm.REPEAT = 2 >>> alarm.DURATION datetime.timedelta(seconds=600) """, ) ACKNOWLEDGED = single_utc_property( "ACKNOWLEDGED", """This property is the UTC datetime at which this alarm was last sent or acknowledged as defined in :rfc:`9074`. Setting this property allows calendar clients to dismiss or suppress an alarm across multiple devices. Once set to a value greater than or equal to the alarm's computed trigger time, conforming clients will not refire the alarm. Returns ``None`` when no acknowledgment has been recorded. Example: Mark an alarm as acknowledged. Note that the example uses an arbitrary time for the purpose of passing doctests. In actual practice, clients should use the current time in UTC, such as ``datetime.now(UTC)``. .. code-block:: pycon >>> from datetime import timezone, datetime >>> from icalendar import Alarm >>> UTC = timezone.utc >>> alarm = Alarm() >>> alarm.ACKNOWLEDGED = datetime(2024, 1, 15, 10, 0, tzinfo=UTC) >>> alarm.ACKNOWLEDGED datetime.datetime(2024, 1, 15, 10, 0, tzinfo=ZoneInfo(key='UTC')) See also: :attr:`TRIGGER`, the time at which the alarm fires. """, ) TRIGGER = create_single_property( "TRIGGER", "dt", (datetime, timedelta), timedelta | datetime | None, """The time at which this alarm fires, per :rfc:`5545#section-3.8.6.3`. The value is either a :class:`~datetime.timedelta` (relative trigger) or a UTC :class:`~datetime.datetime` (absolute trigger). A negative :class:`~datetime.timedelta` fires *before* the related component boundary (start or end); a positive one fires *after* it. Use :attr:`TRIGGER_RELATED` to choose whether the offset is measured from the start or the end of the parent event or to-do. An absolute trigger fires at an exact UTC point in time regardless of the parent component's dates. Examples: Set an alarm to fire 15 minutes before the start of an event. .. code-block:: pycon >>> from datetime import datetime, timedelta, timezone >>> from icalendar import Alarm, Event >>> UTC = timezone.utc >>> event = Event() >>> event.start = datetime(2024, 1, 15, 10, 0, tzinfo=UTC) >>> alarm = Alarm() >>> alarm.TRIGGER = timedelta(minutes=-15) >>> event.add_component(alarm) >>> event.alarms.times[0].trigger datetime.datetime(2024, 1, 15, 9, 45, tzinfo=datetime.timezone.utc) Set an absolute trigger to fire at a specific UTC time. .. code-block:: pycon >>> absolute_alarm = Alarm() >>> absolute_alarm.TRIGGER = datetime(2024, 1, 15, 9, 45, tzinfo=UTC) >>> absolute_alarm.TRIGGER datetime.datetime(2024, 1, 15, 9, 45, tzinfo=datetime.timezone.utc) See also: :attr:`TRIGGER_RELATED`, :attr:`DURATION`, :attr:`REPEAT` """, ) @property def TRIGGER_RELATED(self) -> str: """The RELATED parameter of the TRIGGER property. Values are either "START" (default) or "END". A value of START will set the alarm to trigger off the start of the associated event or to-do. A value of END will set the alarm to trigger off the end of the associated event or to-do. In this example, we create an alarm that triggers two hours after the end of its parent component. >>> from icalendar import Alarm >>> from datetime import timedelta >>> alarm = Alarm() >>> alarm.TRIGGER = timedelta(hours=2) >>> alarm.TRIGGER_RELATED = "END" """ trigger = self.get("TRIGGER") if trigger is None: return "START" return trigger.params.get("RELATED", "START") @TRIGGER_RELATED.setter def TRIGGER_RELATED(self, value: str): """Set "START" or "END".""" trigger = self.get("TRIGGER") if trigger is None: raise ValueError( "You must set a TRIGGER before setting the RELATED parameter." ) trigger.params["RELATED"] = value class Triggers(NamedTuple): """The computed times of alarm triggers. start - triggers relative to the start of the Event or Todo (timedelta) end - triggers relative to the end of the Event or Todo (timedelta) absolute - triggers at a datetime in UTC """ start: tuple[timedelta] end: tuple[timedelta] absolute: tuple[datetime] @property def triggers(self): """The computed triggers of an Alarm. This takes the TRIGGER, DURATION and REPEAT properties into account. Here, we create an alarm that triggers 3 times before the start of the parent component. >>> from icalendar import Alarm >>> from datetime import timedelta >>> alarm = Alarm() >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours before START >>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again >>> alarm.REPEAT = 2 # trigger 2 more times >>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2)) True >>> alarm.triggers.end () >>> alarm.triggers.absolute () """ start = [] end = [] absolute = [] trigger = self.TRIGGER if trigger is not None: if isinstance(trigger, date): absolute.append(trigger) add = absolute elif self.TRIGGER_RELATED == "START": start.append(trigger) add = start else: end.append(trigger) add = end duration = self.DURATION if duration is not None: for _ in range(self.repeat): add.append(add[-1] + duration) return self.Triggers( start=tuple(start), end=tuple(end), absolute=tuple(absolute) ) repeat = repeat_property ACTION = single_string_property( "ACTION", """The action invoked when the alarm triggers. Typical values defined by :rfc:`5545#section-3.8.6.1` are ``AUDIO``, ``DISPLAY``, and ``EMAIL``. The empty string is returned when no ``ACTION`` property is present. """, ) uid = single_string_property( "UID", uid_property.__doc__, ["X-ALARMUID", "X-EVOLUTION-ALARM-UID"], ) summary = summary_property description = description_property attendees = attendees_property @classmethod def new( cls, /, attendees: list[vCalAddress] | None = None, concepts: CONCEPTS_TYPE_SETTER = None, description: str | None = None, links: LINKS_TYPE_SETTER = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, summary: str | None = None, uid: str | uuid.UUID | None = None, ) -> Self: """Create a new alarm with all required properties. This creates a new Alarm in accordance with :rfc:`5545`. Parameters: attendees: The :attr:`attendees` of the alarm. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. description: The :attr:`description` of the alarm. links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. refids: :attr:`~icalendar.cal.component.Component.refids` of the alarm. related_to: :attr:`~icalendar.cal.component.Component.related_to` of the alarm. summary: The :attr:`summary` of the alarm. uid: The :attr:`uid` of the alarm. Returns: :class:`Alarm` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ alarm: Self = super().new( links=links, related_to=related_to, refids=refids, concepts=concepts, ) alarm.summary = summary alarm.description = description alarm.uid = uid alarm.attendees = attendees return alarm def _apply_duration_repeat( self, duration: timedelta | None, repeat: int | None, ) -> None: if duration is not None or repeat is not None: if duration is None or repeat is None: raise InvalidCalendar( "DURATION and REPEAT must be set together or not at all" ) self.DURATION = duration self.REPEAT = repeat @classmethod def new_display( cls, description: str, trigger: timedelta | datetime, duration: timedelta | None = None, repeat: int | None = None, uid: str | uuid.UUID | None = None, links: LINKS_TYPE_SETTER = None, related_to: RELATED_TO_TYPE_SETTER = None, refids: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, ) -> Alarm: """Create a new DISPLAY alarm that shows a text reminder. A DISPLAY alarm pops up a text notification at the trigger time. This is the most common alarm type used by calendar clients. Conforms to :rfc:`5545#section-3.6.6`. Parameters: description: Required. The text to display when the alarm fires. Corresponds to the :attr:`description` property. trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` relative to the event start (negative means before) or as an absolute :class:`~datetime.datetime` (recommend UTC-aware). concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. duration: Gap between repeated triggers. Must be paired with ``repeat``. Corresponds to the :attr:`DURATION` property. links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. repeat: Number of *additional* times to fire after the initial trigger. Must be paired with ``duration``. Corresponds to the :attr:`REPEAT` property. uid: Unique identifier for the alarm or ``None``. Returns: :class:`Alarm` with ``ACTION:DISPLAY`` set. Raises: ~icalendar.error.InvalidCalendar: If required fields are missing or ``duration`` and ``repeat`` are not both provided together. Example: Create a display alarm that fires 15 minutes before the event: .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Alarm >>> alarm = Alarm.new_display( ... description="Team meeting in 15 minutes", ... trigger=timedelta(minutes=-15), ... ) >>> print(alarm.to_ical().decode()) BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Team meeting in 15 minutes TRIGGER:-PT15M END:VALARM Attach the alarm to an event: .. code-block:: python from datetime import datetime, timedelta, timezone from icalendar import Alarm, Event event = Event.new( summary="Team meeting", start=datetime(2025, 6, 1, 10, 0, tzinfo=timezone.utc), end=datetime(2025, 6, 1, 11, 0, tzinfo=timezone.utc), ) event.add_component(Alarm.new_display( description="Team meeting in 15 minutes", trigger=timedelta(minutes=-15), )) """ if not description: raise InvalidCalendar("DISPLAY alarm requires a description") if trigger is None: raise InvalidCalendar("DISPLAY alarm requires a trigger") alarm: Alarm = cls.new( description=description, uid=uid, links=links, related_to=related_to, refids=refids, concepts=concepts, ) alarm.add("ACTION", "DISPLAY") alarm.TRIGGER = trigger alarm._apply_duration_repeat(duration, repeat) return alarm @classmethod def new_audio( cls, trigger: timedelta | datetime, attach: str | bytes | None = None, duration: timedelta | None = None, repeat: int | None = None, uid: str | uuid.UUID | None = None, links: LINKS_TYPE_SETTER = None, related_to: RELATED_TO_TYPE_SETTER = None, refids: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, ) -> Alarm: """Create a new AUDIO alarm that plays a sound. An AUDIO alarm plays a sound at the trigger time. An optional ``attach`` URI points to the audio file to play; when omitted, the client uses its default alert sound. Conforms to :rfc:`5545#section-3.6.6`. Parameters: trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` relative to the event start (negative means before) or as an absolute :class:`~datetime.datetime` (recommend UTC-aware). attach: Optional audio attachment. Pass a URI string such as ``"ftp://example.com/pub/sounds/bell.aud"`` for a linked sound file, or :class:`bytes` for inline binary audio data (stored as ``VALUE=BINARY``). When ``None`` the client uses its default sound. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. duration: Gap between repeated triggers. Must be paired with ``repeat``. Corresponds to the :attr:`DURATION` property. links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. repeat: Number of *additional* times to fire after the initial trigger. Must be paired with ``duration``. Corresponds to the :attr:`REPEAT` property. uid: Unique identifier for the alarm or ``None``. Returns: :class:`Alarm` with ``ACTION:AUDIO`` set. Raises: ~icalendar.error.InvalidCalendar: If required fields are missing or ``duration`` and ``repeat`` are not both provided together. Example: Create an audio alarm using a custom sound file: .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Alarm >>> alarm = Alarm.new_audio( ... trigger=timedelta(minutes=-5), ... attach="ftp://example.com/pub/sounds/bell-01.aud", ... ) >>> print(alarm.to_ical().decode()) BEGIN:VALARM ACTION:AUDIO ATTACH:ftp://example.com/pub/sounds/bell-01.aud TRIGGER:-PT5M END:VALARM """ if trigger is None: raise InvalidCalendar("AUDIO alarm requires a trigger") alarm: Alarm = cls.new( uid=uid, links=links, related_to=related_to, refids=refids, concepts=concepts, ) alarm.add("ACTION", "AUDIO") alarm.TRIGGER = trigger if attach: alarm.add( "ATTACH", vBinary(attach) if isinstance(attach, bytes) else attach ) alarm._apply_duration_repeat(duration, repeat) return alarm @classmethod def new_email( cls, summary: str, description: str, trigger: timedelta | datetime, attendees: Sequence[vCalAddress] | vCalAddress, attachments: Sequence[str] | str | None = None, duration: timedelta | None = None, repeat: int | None = None, uid: str | uuid.UUID | None = None, links: LINKS_TYPE_SETTER = None, related_to: RELATED_TO_TYPE_SETTER = None, refids: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, ) -> Alarm: """Create a new EMAIL alarm that sends an email notification. An EMAIL alarm sends an email to each address in ``attendees`` when the alarm fires. Conforms to :rfc:`5545#section-3.6.6`. Parameters: attendees: Required. One or more recipient addresses as :class:`~icalendar.prop.cal_address.vCalAddress` instances. A single address or a sequence of addresses. At least one is required. description: Required. Body of the email. Corresponds to the :attr:`description` property. summary: Required. Subject line of the email. Corresponds to the :attr:`summary` property. trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` relative to the event start (negative means before) or as an absolute :class:`~datetime.datetime` (recommend UTC-aware). attachments: Optional URI or sequence of URIs to attach to the email. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. duration: Gap between repeated triggers. Must be paired with ``repeat``. Corresponds to the :attr:`DURATION` property. links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. repeat: Number of *additional* times to fire after the initial trigger. Must be paired with ``duration``. Corresponds to the :attr:`REPEAT` property. uid: Unique identifier for the alarm or ``None``. Returns: :class:`Alarm` with ``ACTION:EMAIL`` set. Raises: ~icalendar.error.InvalidCalendar: If required fields are missing, ``attendees`` is empty, or ``duration`` and ``repeat`` are not both provided together. Example: Create an email alarm sent to two recipients: .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Alarm, vCalAddress >>> alarm = Alarm.new_email( ... summary="Meeting reminder", ... description="Your meeting starts in 30 minutes.", ... trigger=timedelta(minutes=-30), ... attendees=[vCalAddress("mailto:user@example.com")], ... ) >>> print(alarm.to_ical().decode()) BEGIN:VALARM ACTION:EMAIL ATTENDEE:mailto:user@example.com DESCRIPTION:Your meeting starts in 30 minutes. SUMMARY:Meeting reminder TRIGGER:-PT30M END:VALARM """ if isinstance(attendees, str): attendees = [attendees] if isinstance(attachments, str): attachments = [attachments] if not summary: raise InvalidCalendar("EMAIL alarm requires a summary") if not description: raise InvalidCalendar("EMAIL alarm requires a description") if trigger is None: raise InvalidCalendar("EMAIL alarm requires a trigger") if not attendees: raise InvalidCalendar("EMAIL alarm requires at least one attendee") alarm: Alarm = cls.new( summary=summary, description=description, uid=uid, attendees=attendees, links=links, related_to=related_to, refids=refids, concepts=concepts, ) alarm.add("ACTION", "EMAIL") alarm.TRIGGER = trigger if attachments: for attachment in attachments: alarm.add("ATTACH", attachment) alarm._apply_duration_repeat(duration, repeat) return alarm @classmethod def example(cls, name: str = "example") -> Alarm: """Return the alarm example with the given name.""" return cls.from_ical(get_example("alarms", name)) __all__ = ["Alarm"] collective-icalendar-138c845/src/icalendar/cal/availability.py000066400000000000000000000317411524134033600243470ustar00rootroot00000000000000"""This implementes the VAVAILABILITY component. This is specified in :rfc:`7953`. """ from __future__ import annotations import uuid from datetime import datetime from typing import TYPE_CHECKING from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, busy_type_property, categories_property, class_property, contacts_property, description_property, duration_property, location_property, organizer_property, priority_property, rfc_7953_dtend_property, rfc_7953_dtstart_property, rfc_7953_duration_property, rfc_7953_end_property, sequence_property, summary_property, url_property, ) from icalendar.cal.examples import get_example from icalendar.error import InvalidCalendar from .component import Component if TYPE_CHECKING: from collections.abc import Iterable, Sequence from datetime import date from icalendar.cal.available import Available from icalendar.compatibility import Self from icalendar.enums import BUSYTYPE, CLASS from icalendar.prop import vCalAddress class Availability(Component): """VAVAILABILITY component from :rfc:`7953`. This provides a grouping of component properties and subcomponents that describe the availability associated with a calendar user. Description: A "VAVAILABILITY" component indicates a period of time within which availability information is provided. A "VAVAILABILITY" component can specify a start time and an end time or duration. If "DTSTART" is not present, then the start time is unbounded. If "DTEND" or "DURATION" are not present, then the end time is unbounded. Within the specified time period, availability defaults to a free-busy type of "BUSY-UNAVAILABLE" (see Section 3.2), except for any time periods corresponding to "AVAILABLE" subcomponents. "AVAILABLE" subcomponents are used to indicate periods of free time within the time range of the enclosing "VAVAILABILITY" component. "AVAILABLE" subcomponents MAY include recurrence properties to specify recurring periods of time, which can be overridden using normal iCalendar recurrence behavior (i.e., use of the "RECURRENCE-ID" property). If specified, the "DTSTART" and "DTEND" properties in "VAVAILABILITY" components and "AVAILABLE" subcomponents MUST be "DATE-TIME" values specified as either the date with UTC time or the date with local time and a time zone reference. The iCalendar object containing the "VAVAILABILITY" component MUST contain appropriate "VTIMEZONE" components corresponding to each unique "TZID" parameter value used in any DATE-TIME properties in all components, unless [RFC7809] is in effect. When used to publish available time, the "ORGANIZER" property specifies the calendar user associated with the published available time. If the "PRIORITY" property is specified in "VAVAILABILITY" components, it is used to determine how that component is combined with other "VAVAILABILITY" components. See Section 4. Other calendar properties MAY be specified in "VAVAILABILITY" or "AVAILABLE" components and are considered attributes of the marked block of time. Their usage is application specific. For example, the "LOCATION" property might be used to indicate that a person is available in one location for part of the week and a different location for another part of the week (but see Section 9 for when it is appropriate to add additional data like this). Example: The following is an example of a "VAVAILABILITY" calendar component used to represent the availability of a user, always available Monday through Friday, 9:00 am to 5:00 pm in the America/Montreal time zone: .. code-block:: ics BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:0428C7D2-688E-4D2E-AC52-CD112E2469DF DTSTAMP:20111005T133225Z BEGIN:AVAILABLE UID:34EDA59B-6BB1-4E94-A66C-64999089C0AF SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111002T090000 DTEND;TZID=America/Montreal:20111002T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR END:AVAILABLE END:VAVAILABILITY You can get the same example from :meth:`example`: .. code-block: pycon >>> from icalendar import Availability >>> a = Availability.example() >>> a.organizer vCalAddress('mailto:bernard@example.com') The following is an example of a "VAVAILABILITY" calendar component used to represent the availability of a user available Monday through Thursday, 9:00 am to 5:00 pm, at the main office, and Friday, 9:00 am to 12:00 pm, in the branch office in the America/Montreal time zone between October 2nd and December 2nd 2011: .. code-block:: ics BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:84D0F948-7FC6-4C1D-BBF3-BA9827B424B5 DTSTAMP:20111005T133225Z DTSTART;TZID=America/Montreal:20111002T000000 DTEND;TZID=America/Montreal:20111202T000000 BEGIN:AVAILABLE UID:7B33093A-7F98-4EED-B381-A5652530F04D SUMMARY:Monday to Thursday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111002T090000 DTEND;TZID=America/Montreal:20111002T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH LOCATION:Main Office END:AVAILABLE BEGIN:AVAILABLE UID:DF39DC9E-D8C3-492F-9101-0434E8FC1896 SUMMARY:Friday from 9:00 to 12:00 DTSTART;TZID=America/Montreal:20111006T090000 DTEND;TZID=America/Montreal:20111006T120000 RRULE:FREQ=WEEKLY LOCATION:Branch Office END:AVAILABLE END:VAVAILABILITY For more examples, have a look at :rfc:`5545`. """ name = "VAVAILABILITY" canonical_order = ( "DTSTART", "DTEND", "DURATION", "DTSTAMP", "UID", "SEQUENCE", "SUMMARY", "DESCRIPTION", "ORGANIZER", ) required = ( "DTSTART", "DTSTAMP", "UID", ) singletons = ( "DTSTAMP", "UID", "BUSYTYPE", "CLASS", "CREATED", "DESCRIPTION", "DTSTART", "LAST-MODIFIED", "LOCATION", "ORGANIZER", "PRIORITY", "SEQUENCE", "SUMMARY", "URL", "DTEND", "DURATION", ) exclusive = ( "DTEND", "DURATION", ) organizer = organizer_property busy_type = busy_type_property summary = summary_property description = description_property sequence = sequence_property classification = class_property url = url_property location = location_property categories = categories_property priority = priority_property contacts = contacts_property start = DTSTART = rfc_7953_dtstart_property DTEND = rfc_7953_dtend_property DURATION = duration_property("Availability") duration = rfc_7953_duration_property end = rfc_7953_end_property @property def available(self) -> list[Available]: """All VAVAILABLE sub-components. This is a shortcut to get all VAVAILABLE sub-components. Modifications do not change the calendar. Use :meth:`~icalendar.cal.component.Component.add_component`. """ return self.walk("AVAILABLE") @classmethod def new( cls, /, busy_type: BUSYTYPE | None = None, categories: Sequence[str] = (), comments: list[str] | str | None = None, components: Sequence[Available] | None = (), concepts: CONCEPTS_TYPE_SETTER = None, contacts: list[str] | str | None = None, created: date | None = None, classification: CLASS | None = None, description: str | None = None, end: datetime | None = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, location: str | None = None, organizer: vCalAddress | str | None = None, priority: int | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, sequence: int | None = None, stamp: date | None = None, start: datetime | None = None, subcomponents: Iterable[Component] | None = None, summary: str | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, ) -> Self: """Create a new event with all required properties. This creates a new Availability in accordance with :rfc:`7953`. Parameters: busy_type: The :attr:`busy_type` of the availability. categories: The :attr:`categories` of the availability. classification: The :attr:`classification` of the availability. comments: The :attr:`~icalendar.cal.component.Component.comments` of the availability. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the availability. contacts: The :attr:`contacts` of the availability. created: The :attr:`~icalendar.cal.component.Component.created` of the availability. description: The :attr:`description` of the availability. end: The :attr:`end` of the availability. last_modified: The :attr:`~icalendar.cal.component.Component.last_modified` of the availability. links: The :attr:`~icalendar.cal.component.Component.links` of the availability. location: The :attr:`location` of the availability. organizer: The :attr:`organizer` of the availability. refids: :attr:`~icalendar.cal.component.Component.refids` of the availability. related_to: :attr:`~icalendar.cal.component.Component.related_to` of the availability. sequence: The :attr:`sequence` of the availability. stamp: The :attr:`~icalendar.cal.component.Component.stamp` of the availability. If None, this is set to the current time. start: The :attr:`start` of the availability. subcomponents: The :attr:`~icalendar.cal.component.Component.subcomponents` of the availability. summary: The :attr:`summary` of the availability. uid: The :attr:`~icalendar.cal.component.Component.uid` of the availability. If ``None``, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the availability. Returns: :class:`Availability` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`7953`. .. warning:: As time progresses, we will be stricter with the validation. """ availability: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), created=created, comments=comments, last_modified=last_modified, links=links, related_to=related_to, refids=refids, concepts=concepts, subcomponents=subcomponents, ) availability.summary = summary availability.description = description availability.uid = uid if uid is not None else uuid.uuid4() availability.sequence = sequence availability.categories = categories availability.classification = classification availability.url = url availability.busy_type = busy_type availability.organizer = organizer availability.location = location availability.priority = priority availability.contacts = contacts for subcomponent in components: availability.add_component(subcomponent) if cls._validate_new: if start is not None and ( not isinstance(start, datetime) or start.tzinfo is None ): raise InvalidCalendar( "Availability start must be a datetime with a timezone" ) if end is not None and ( not isinstance(end, datetime) or end.tzinfo is None ): raise InvalidCalendar( "Availability end must be a datetime with a timezone" ) availability._validate_start_and_end(start, end) availability.start = start availability.end = end return availability @classmethod def example(cls, name: str = "rfc_7953_1") -> Availability: """Return the calendar example with the given name.""" return cls.from_ical(get_example("availabilities", name)) __all__ = ["Availability"] collective-icalendar-138c845/src/icalendar/cal/available.py000066400000000000000000000160721524134033600236150ustar00rootroot00000000000000"""This implements the sub-component "AVAILABLE" of "VAVAILABILITY". This is specified in :rfc:`7953`. """ from __future__ import annotations import uuid from datetime import datetime from typing import TYPE_CHECKING from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, categories_property, contacts_property, description_property, duration_property, exdates_property, location_property, rdates_property, rfc_7953_dtend_property, rfc_7953_dtstart_property, rfc_7953_duration_property, rfc_7953_end_property, rrules_property, sequence_property, summary_property, uid_property, ) from icalendar.cal.examples import get_example from icalendar.error import InvalidCalendar from .component import Component if TYPE_CHECKING: from collections.abc import Sequence from datetime import date from icalendar.compatibility import Self class Available(Component): """Sub-component of "VAVAILABILITY from :rfc:`7953`. Description: "AVAILABLE" subcomponents are used to indicate periods of free time within the time range of the enclosing "VAVAILABILITY" component. "AVAILABLE" subcomponents MAY include recurrence properties to specify recurring periods of time, which can be overridden using normal iCalendar recurrence behavior (i.e., use of the "RECURRENCE-ID" property). Examples: This is a recurring "AVAILABLE" subcomponent: .. code-block:: ics BEGIN:AVAILABLE UID:57DD4AAF-3835-46B5-8A39-B3B253157F01 SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Denver:20111023T090000 DTEND;TZID=America/Denver:20111023T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR LOCATION:Denver END:AVAILABLE You can get the same example from :meth:`example`: .. code-block: pycon >>> from icalendar import Available >>> a = Available.example() >>> str(a.summary) 'Monday to Friday from 9:00 to 17:00' """ name = "AVAILABLE" summary = summary_property description = description_property sequence = sequence_property categories = categories_property uid = uid_property location = location_property contacts = contacts_property exdates = exdates_property rdates = rdates_property rrules = rrules_property from icalendar.attr import RECURRENCE_ID start = DTSTART = rfc_7953_dtstart_property DTEND = rfc_7953_dtend_property DURATION = duration_property("Available") duration = rfc_7953_duration_property end = rfc_7953_end_property @classmethod def new( cls, /, categories: Sequence[str] = (), comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, contacts: list[str] | str | None = None, created: date | None = None, description: str | None = None, end: datetime | None = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, location: str | None = None, recurrence_id: date | datetime | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, sequence: int | None = None, stamp: date | None = None, start: datetime | None = None, summary: str | None = None, uid: str | uuid.UUID | None = None, ) -> Self: """Create a new Available component with all required properties. This creates a new Available component in accordance with :rfc:`7953`. Parameters: categories: The :attr:`categories` of the Available component. comments: The :attr:`~icalendar.Component.comments` of the Available component. concepts: The :attr:`~icalendar.Component.concepts` of the Available component. contacts: The :attr:`contacts` of the Available component. created: The :attr:`~icalendar.Component.created` of the Available component. description: The :attr:`description` of the Available component. end: The :attr:`end` of the Available component. last_modified: The :attr:`~icalendar.Component.last_modified` of the Available component. links: The :attr:`~icalendar.Component.links` of the Available component. location: The :attr:`location` of the Available component. recurrence_id: The :attr:`RECURRENCE_ID` of the Available component. refids: :attr:`~icalendar.Component.refids` of the Available component. related_to: :attr:`~icalendar.Component.related_to` of the Available component. sequence: The :attr:`sequence` of the Available component. stamp: The :attr:`~icalendar.Component.stamp` of the Available component. If None, this is set to the current time. start: The :attr:`start` of the Available component. summary: The :attr:`summary` of the Available component. uid: The :attr:`uid` of the Available component. If None, this is set to a new :func:`uuid.uuid4`. Returns: :class:`Available` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`7953`. .. warning:: As time progresses, we will be stricter with the validation. """ available: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), created=created, last_modified=last_modified, comments=comments, links=links, related_to=related_to, refids=refids, concepts=concepts, ) available.summary = summary available.description = description available.uid = uid if uid is not None else uuid.uuid4() available.sequence = sequence available.categories = categories available.location = location available.contacts = contacts available.RECURRENCE_ID = recurrence_id if cls._validate_new: if end is not None and ( not isinstance(end, datetime) or end.tzinfo is None ): raise InvalidCalendar( "Available end must be a datetime with a timezone" ) if not isinstance(start, datetime) or start.tzinfo is None: raise InvalidCalendar( "Available start must be a datetime with a timezone" ) available._validate_start_and_end(start, end) available.start = start available.end = end return available @classmethod def example(cls, name: str = "rfc_7953_1") -> Available: """Return the calendar example with the given name.""" return cls.from_ical(get_example("availabilities", name)).available[0] __all__ = ["Available"] collective-icalendar-138c845/src/icalendar/cal/calendar.py000066400000000000000000000524611524134033600234500ustar00rootroot00000000000000""":rfc:`5545` iCalendar component.""" from __future__ import annotations import uuid from datetime import timedelta from typing import TYPE_CHECKING, Literal, cast, overload from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, categories_property, images_property, multi_language_text_property, single_string_property, source_property, uid_property, url_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example from icalendar.cal.timezone import Timezone from icalendar.error import IncompleteComponent from icalendar.parser.ical.calendar import CalendarIcalParser from icalendar.version import __version__ if TYPE_CHECKING: from collections.abc import Iterable, Sequence from datetime import date, datetime from pathlib import Path from icalendar.cal.availability import Availability from icalendar.cal.event import Event from icalendar.cal.free_busy import FreeBusy from icalendar.cal.journal import Journal from icalendar.cal.todo import Todo from icalendar.compatibility import Self from icalendar.parser.ical.component import ComponentIcalParser DEFAULT_PRODID = f"-//collective//icalendar//{__version__}//EN" class Calendar(Component): """ The "VCALENDAR" object is a collection of calendar information. This information can include a variety of components, such as "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE", or any other type of calendar component. Examples: Create a new Calendar: >>> from icalendar import Calendar >>> calendar = Calendar.new(name="My Calendar") >>> print(calendar.calendar_name) My Calendar """ name = "VCALENDAR" canonical_order = ( "VERSION", "PRODID", "CALSCALE", "METHOD", "DESCRIPTION", "X-WR-CALDESC", "NAME", "X-WR-CALNAME", ) required = ( "PRODID", "VERSION", ) singletons = ( "PRODID", "VERSION", "CALSCALE", "METHOD", "COLOR", # RFC 7986 ) multiple = ( "CATEGORIES", # RFC 7986 "DESCRIPTION", # RFC 7986 "NAME", # RFC 7986 ) @classmethod def example(cls, name: str = "example") -> Calendar: """Return the calendar example with the given name.""" return cls.from_ical(get_example("calendars", name)) @classmethod def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: """Get the iCal parser for the given input string.""" return CalendarIcalParser(st, cls._get_component_factory(), cls.types_factory) @overload @classmethod def from_ical( cls, st: str | bytes | Path, multiple: Literal[False] = False ) -> Calendar: ... @overload @classmethod def from_ical( cls, st: str | bytes | Path, multiple: Literal[True] ) -> list[Calendar]: ... @classmethod def from_ical( cls, st: str | bytes | Path, multiple: bool = False ) -> Calendar | list[Calendar]: """Parse iCalendar data into calendar instances. Parameters: st: iCalendar data as bytes or string, or a path to an iCalendar file. multiple: If ``True``, returns a list of calendars. If ``False``, returns a single calendar. Returns: Calendar or list of calendars. """ return cast( "Calendar | list[Calendar]", super().from_ical(st, multiple=multiple) ) @property def events(self) -> list[Event]: """All event components in the calendar. This is a shortcut to get all events. Modifications do not change the calendar. Use :meth:`Component.add_component `. >>> from icalendar import Calendar >>> calendar = Calendar.example() >>> event = calendar.events[0] >>> event.start datetime.date(2022, 1, 1) >>> print(event["SUMMARY"]) New Year's Day """ return self.walk("VEVENT") @property def todos(self) -> list[Todo]: """All todo components in the calendar. This is a shortcut to get all todos. Modifications do not change the calendar. Use :meth:`Component.add_component `. """ return self.walk("VTODO") @property def journals(self) -> list[Journal]: """All journal components in the calendar. This is a shortcut to get all journals. Modifications do not change the calendar. Use :meth:`Component.add_component `. """ return self.walk("VJOURNAL") @property def availabilities(self) -> list[Availability]: """All :class:`Availability` components in the calendar. This is a shortcut to get all availabilities. Modifications do not change the calendar. Use :meth:`Component.add_component `. """ return self.walk("VAVAILABILITY") @property def freebusy(self) -> list[FreeBusy]: """All FreeBusy components in the calendar. This is a shortcut to get all FreeBusy. Modifications do not change the calendar. Use :meth:`Component.add_component `. """ return self.walk("VFREEBUSY") def get_used_tzids(self) -> set[str]: """The set of TZIDs in use. This goes through the whole calendar to find all occurrences of timezone information like the TZID parameter in all attributes. >>> from icalendar import Calendar >>> calendar = Calendar.example("timezone_rdate") >>> calendar.get_used_tzids() {'posix/Europe/Vaduz'} Even if you use UTC, this will not show up. """ result = set() for _name, value in self.property_items(sorted=False): if hasattr(value, "params"): result.add(value.params.get("TZID")) return result - {None} def get_missing_tzids(self) -> set[str]: """The set of missing timezone component tzids. To create a :rfc:`5545` compatible calendar, all of these timezones should be added. UTC is excluded: per :rfc:`5545#section-3.2.19`, UTC datetimes use the ``Z`` suffix and never require a VTIMEZONE component. """ tzids = self.get_used_tzids() - {"UTC"} for timezone in self.timezones: # discard (not remove) — a VTIMEZONE may exist for a timezone not # referenced by any event TZID (e.g. added by x-wr-timezone conversion) tzids.discard(timezone.tz_name) return tzids @property def timezones(self) -> list[Timezone]: """Return the timezones components in this calendar. >>> from icalendar import Calendar >>> calendar = Calendar.example("pacific_fiji") >>> [timezone.tz_name for timezone in calendar.timezones] ['custom_Pacific/Fiji'] .. note:: This is a read-only property. """ return self.walk("VTIMEZONE") def add_missing_timezones( self, first_date: date = Timezone.DEFAULT_FIRST_DATE, last_date: date = Timezone.DEFAULT_LAST_DATE, ): """Add all missing VTIMEZONE components. This adds all the timezone components that are required. VTIMEZONE components are inserted at the beginning of the calendar to ensure they appear before other components that reference them. .. note:: Timezones that are not known will not be added. Parameters: first_date: Earlier than anything that happens in the calendar. last_date: Later than anything happening in the calendar. >>> from icalendar import Calendar, Event >>> from datetime import datetime >>> from zoneinfo import ZoneInfo >>> calendar = Calendar() >>> event = Event() >>> calendar.add_component(event) >>> event.start = datetime(1990, 10, 11, 12, tzinfo=ZoneInfo("Europe/Berlin")) >>> calendar.timezones [] >>> calendar.add_missing_timezones() >>> calendar.timezones[0].tz_name 'Europe/Berlin' >>> calendar.get_missing_tzids() # check that all are added set() """ missing_tzids = self.get_missing_tzids() if not missing_tzids: return existing_timezone_count = len(self.timezones) for tzid in missing_tzids: try: timezone = Timezone.from_tzid( tzid, first_date=first_date, last_date=last_date ) except ValueError: continue self.subcomponents.insert(existing_timezone_count, timezone) existing_timezone_count += 1 calendar_name = multi_language_text_property( "NAME", "X-WR-CALNAME", """The display name of this calendar, per :rfc:`7986#section-5.1`. Implements both the ``NAME`` property from :rfc:`7986#section-5.1` and the widely used ``X-WR-CALNAME`` extension for broader client compatibility. Multiple language variants can be stored by setting this property more than once, each with a different ``LANGUAGE`` parameter value. Example: Set the name of the calendar. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar() >>> calendar.calendar_name = "My Calendar" >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR NAME:My Calendar X-WR-CALNAME:My Calendar END:VCALENDAR """, ) description = multi_language_text_property( "DESCRIPTION", "X-WR-CALDESC", """A description of the calendar's content. Implements both ``DESCRIPTION`` from :rfc:`5545#section-3.8.1.5` and :rfc:`7986#section-5.2` and ``X-WR-CALDESC`` for broader calendar client compatibility. Multiple language variants can be stored by setting this property more than once with different ``LANGUAGE`` parameter values. Example: Add a description to a calendar. .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar() >>> calendar.description = "This is a calendar" >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR DESCRIPTION:This is a calendar X-WR-CALDESC:This is a calendar END:VCALENDAR """, ) color = single_string_property( "COLOR", """A CSS3 color name or value used to visually distinguish this calendar, per :rfc:`7986#section-5.9`. Implements both ``COLOR`` from :rfc:`7986#section-5.9` and ``X-APPLE-CALENDAR-COLOR``. The value is a case-insensitive CSS3 color name, for example, ``"turquoise"``, or a hex code, for example, ``"#ffffff"``, drawn from the `CSS3 color specification `_. Since :rfc:`7986`, individual ``VEVENT``, ``VTODO``, and ``VJOURNAL`` subcomponents may also carry their own color. Example: .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar() >>> calendar.color = "black" >>> print(calendar.to_ical().decode()) BEGIN:VCALENDAR COLOR:black END:VCALENDAR """, "X-APPLE-CALENDAR-COLOR", ) categories = categories_property uid = uid_property prodid = single_string_property( "PRODID", """The product identifier for the software that created this iCalendar object. This property is defined in :rfc:`5545#section-3.7.3`. It's required exactly once per iCalendar object. The value should be a globally unique string. The conventional format is a Formal Public Identifier (FPI), for example, ``-//My Company//My Product//EN``, but any unique string is acceptable. Example: Set a custom product identifier on a new calendar. .. code-block:: pycon >>> from icalendar import Calendar >>> cal = Calendar() >>> cal.prodid = "-//MyApp//MyCalendar//EN" >>> str(cal.prodid) '-//MyApp//MyCalendar//EN' See also: :attr:`version` """, ) version = single_string_property( "VERSION", """The iCalendar specification version required to interpret this object. This property is defined in :rfc:`5545#section-3.7.4`. It's required exactly once per calendar object. The value ``"2.0"`` indicates :rfc:`5545` compliance, which is the default used by this library. A range such as ``"1.0;2.0"`` may indicate minimum and maximum supported versions. Example: .. code-block:: pycon >>> from icalendar import Calendar >>> cal = Calendar() >>> cal.version = "2.0" >>> str(cal.version) '2.0' See also: :attr:`prodid` """, ) calscale = single_string_property( "CALSCALE", """The calendar scale for date and time values in this iCalendar object. This property is defined in :rfc:`5545#section-3.7.1`. The only value currently defined is ``"GREGORIAN"`` (the default). When this property is absent, Gregorian is assumed. Per :rfc:`7529`, non-Gregorian calendar systems are expressed via ``RRULE`` transformations rather than a different ``CALSCALE`` value. However, icalendar currently implements only the parsing of this value, not a transformation. Example: .. code-block:: pycon >>> from icalendar import Calendar >>> cal = Calendar() >>> cal.calscale 'GREGORIAN' """, default="GREGORIAN", ) method = single_string_property( "METHOD", """The iTIP scheduling method associated with this calendar object, per :rfc:`5545#section-3.7.2`. When present, ``METHOD`` indicates that this object is part of a scheduling transaction, such as a meeting invitation or cancellation. Scheduling methods are defined by :rfc:`5546#section-1.4` (iTIP), with values such as ``"REQUEST"``, ``"REPLY"``, ``"CANCEL"``, and ``"PUBLISH"``. When used inside a MIME message, this value must match the ``method`` parameter of the ``Content-Type`` header. If absent, the calendar is treated as a plain data snapshot with no scheduling semantics. Example: .. code-block:: pycon >>> from icalendar import Calendar >>> cal = Calendar() >>> cal.method = "REQUEST" >>> str(cal.method) 'REQUEST' """, ) url = url_property source = source_property @property def refresh_interval(self) -> timedelta | None: """A suggested minimum polling interval for fetching updates to this calendar, per :rfc:`7986#section-5.7`. Calendar clients should not poll more frequently than this interval. The value must be a positive duration. Returns: A :class:`~datetime.timedelta`, or ``None`` when not set. Raises: ValueError: When setting a non-positive (zero or negative) duration. TypeError: When setting a value that is not a :class:`~datetime.timedelta` or ``None``. Example: .. code-block:: pycon >>> from datetime import timedelta >>> from icalendar import Calendar >>> cal = Calendar() >>> cal.refresh_interval = timedelta(hours=1) >>> cal.refresh_interval datetime.timedelta(seconds=3600) """ refresh_interval = self.get("REFRESH-INTERVAL") return refresh_interval.dt if refresh_interval else None @refresh_interval.setter def refresh_interval(self, value: timedelta | None): """Set the REFRESH-INTERVAL.""" if not isinstance(value, timedelta) and value is not None: raise TypeError( "REFRESH-INTERVAL must be either a positive timedelta," " or None to delete it." ) if value is not None and value.total_seconds() <= 0: raise ValueError("REFRESH-INTERVAL must be a positive timedelta.") if value is not None: del self.refresh_interval self.add("REFRESH-INTERVAL", value) else: del self.refresh_interval @refresh_interval.deleter def refresh_interval(self): """Delete REFRESH-INTERVAL.""" self.pop("REFRESH-INTERVAL") images = images_property @classmethod def new( cls, /, calscale: str | None = None, categories: Sequence[str] = (), color: str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, description: str | None = None, language: str | None = None, last_modified: date | datetime | None = None, links: LINKS_TYPE_SETTER = None, method: str | None = None, name: str | None = None, organization: str | None = None, prodid: str | None = None, refresh_interval: timedelta | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, source: str | None = None, subcomponents: Iterable[Component] | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, version: str = "2.0", ) -> Self: """Create a new Calendar. This creates a new Calendar in accordance with :rfc:`5545` and :rfc:`7986`. Parameters: calscale: The :attr:`calscale` of the calendar. categories: The :attr:`categories` of the calendar. color: The :attr:`color` of the calendar. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the calendar. description: The :attr:`description` of the calendar. language: The language for the calendar. Used to generate localized `prodid`. last_modified: The :attr:`~icalendar.cal.component.Component.last_modified` of the calendar. links: The :attr:`~icalendar.cal.component.Component.links` of the calendar. method: The :attr:`method` of the calendar. name: The :attr:`calendar_name` of the calendar. organization: The organization name. Used to generate `prodid` if not provided. prodid: The :attr:`prodid` of the component. If ``None`` and ``organization`` is provided, generates a `prodid` in the format of "-//organization//name//language". If ``None`` and ``organization`` is not provided, sets it to :attr:`~icalendar.cal.calendar.DEFAULT_PRODID`. refresh_interval: The :attr:`refresh_interval` of the calendar. refids: :attr:`~icalendar.cal.component.Component.refids` of the calendar. related_to: :attr:`~icalendar.cal.component.Component.related_to` of the calendar. source: The :attr:`source` of the calendar. subcomponents: The subcomponents of the calendar. uid: The :attr:`uid` of the calendar. If None, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the calendar. version: The :attr:`version` of the calendar. Returns: :class:`Calendar` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ calendar: Self = super().new( last_modified=last_modified, links=links, related_to=related_to, refids=refids, concepts=concepts, subcomponents=subcomponents, ) # Generate prodid if not provided but organization is given if prodid is None and organization: app_name = name or "Calendar" lang = language.upper() if language else "EN" prodid = f"-//{organization}//{app_name}//{lang}" elif prodid is None: prodid = DEFAULT_PRODID calendar.prodid = prodid calendar.version = version calendar.calendar_name = name calendar.color = color calendar.description = description calendar.method = method calendar.calscale = calscale calendar.categories = categories calendar.uid = uid if uid is not None else uuid.uuid4() calendar.url = url calendar.refresh_interval = refresh_interval calendar.source = source return calendar def validate(self): """Validate that the calendar has required properties and components. This method can be called explicitly to validate a calendar before output. Raises: ~error.IncompleteComponent: If the calendar lacks required properties or components. """ if not self.get("PRODID"): raise IncompleteComponent("Calendar must have a PRODID") if not self.get("VERSION"): raise IncompleteComponent("Calendar must have a VERSION") if not self.subcomponents: raise IncompleteComponent( "Calendar must contain at least one component (event, todo, etc.)" ) __all__ = ["Calendar"] collective-icalendar-138c845/src/icalendar/cal/component.py000066400000000000000000001277031524134033600237030ustar00rootroot00000000000000"""The base for :rfc:`5545` components.""" from __future__ import annotations import json from copy import deepcopy from dataclasses import dataclass from datetime import date, datetime, time, timedelta, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, comments_property, concepts_property, links_property, refids_property, related_to_property, single_utc_property, uid_property, ) from icalendar.cal.component_factory import ComponentFactory from icalendar.caselessdict import CaselessDict from icalendar.error import InvalidCalendar, JCalParsingError from icalendar.parser import ( Contentline, Contentlines, Parameters, q_join, q_split, ) from icalendar.parser.ical.component import ComponentIcalParser from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText, vUnknown from icalendar.timezone import tzp from icalendar.tools import is_date if TYPE_CHECKING: from collections.abc import Iterable from icalendar.compatibility import Self _marker = [] @dataclass class _ComponentEqFrame: """A pending component-equality comparison on the iterative stack. See ``Component.__eq__`` for how the fields are used. """ #: the two components being compared a: Component b: Component #: ``b``'s subcomponents not yet matched against one of ``a``'s. ``None`` #: until ``a`` and ``b``'s own properties have been compared and found equal. unmatched: list | None = None #: index of the ``a`` subcomponent we are currently trying to match a_index: int = 0 #: index of the unmatched ``b`` subcomponent we are currently testing candidate_index: int = 0 class Component(CaselessDict): """Base class for calendar components. Component is the base object for calendar, Event and the other components defined in :rfc:`5545`. Normally you will not use this class directly, but rather one of the subclasses. """ name: ClassVar[str | None] = None """The name of the component. This is defined in each component class. Example: .. code-block:: pycon >>> from icalendar import Calendar >>> cal = Calendar.new() >>> cal.name 'VCALENDAR' """ required: ClassVar[tuple[()]] = () """These properties are required.""" singletons: ClassVar[tuple[()]] = () """These properties must appear only once.""" multiple: ClassVar[tuple[()]] = () """These properties may occur more than once.""" exclusive: ClassVar[tuple[()]] = () """These properties are mutually exclusive.""" inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = () """These properties are inclusive. In other words, if the first property in the tuple occurs, then the second one must also occur. Example: .. code-block:: python ('duration', 'repeat') """ ignore_exceptions: ClassVar[bool] = False """Whether or not to ignore exceptions when parsing. If ``True``, and this component can't be parsed, then it will silently ignore it, rather than let the exception propagate upwards. """ types_factory: ClassVar[TypesFactory] = TypesFactory.instance() _components_factory: ClassVar[ComponentFactory | None] = None subcomponents: list[Component] """All subcomponents of this component.""" @classmethod def _get_component_factory(cls) -> ComponentFactory: """Get the component factory.""" if cls._components_factory is None: cls._components_factory = ComponentFactory() return cls._components_factory @classmethod def get_component_class(cls, name: str) -> type[Component]: """Return a component with this name. Parameters: name: Name of the component, i.e. ``VCALENDAR`` """ return cls._get_component_factory().get_component_class(name) @classmethod def register(cls, component_class: type[Component]) -> None: """Register a custom component class. Parameters: component_class: Component subclass to register. Must have a ``name`` attribute. Raises: ValueError: If ``component_class`` has no ``name`` attribute. ValueError: If a component with this name is already registered. Examples: Create a custom icalendar component with the name ``X-EXAMPLE``: .. code-block:: pycon >>> from icalendar import Component >>> class XExample(Component): ... name = "X-EXAMPLE" ... def custom_method(self): ... return "custom" >>> Component.register(XExample) """ if not hasattr(component_class, "name") or component_class.name is None: raise ValueError(f"{component_class} must have a 'name' attribute") # Check if already registered component_factory = cls._get_component_factory() existing = component_factory.get(component_class.name) if existing is not None and existing is not component_class: raise ValueError( f"Component '{component_class.name}' is already registered" f" as {existing}" ) component_factory.add_component_class(component_class) @staticmethod def _infer_value_type( value: date | datetime | timedelta | time | tuple | list, ) -> str | None: """Infer the ``VALUE`` parameter from a Python type. Parameters: value: Python native type, one of :class:`datetime.date`, :class:`datetime.datetime`, :class:`datetime.timedelta`, :class:`datetime.time`, :class:`tuple`, or :class:`list`. Returns: str or None: The ``VALUE`` parameter string, for example, "DATE", "TIME", or other string, or ``None`` if no specific ``VALUE`` is needed. """ if isinstance(value, list): if not value: return None # Check if ALL items are date (but not datetime) if all(is_date(item) for item in value): return "DATE" # Check if ALL items are time if all(isinstance(item, time) for item in value): return "TIME" # Mixed types or other types - don't infer return None if is_date(value): return "DATE" if isinstance(value, time): return "TIME" # Don't infer PERIOD - it's too risky and vPeriod already handles it return None def __init__(self, *args: Any, **kwargs: Any) -> None: """Set keys to upper for initial dict.""" super().__init__(*args, **kwargs) # set parameters here for properties that use non-default values self.subcomponents: list[Component] = [] # Components can be nested. self.errors: list[ tuple[str | None, str] ] = [] # If we ignored exception(s) while # parsing a property, contains error strings def __bool__(self) -> bool: """Returns True, CaselessDict would return False if it had no items.""" return True def __getitem__(self, key) -> VPROPERTY: """Get property value from the component dictionary.""" return super().__getitem__(key) def get(self, key, default=None) -> Any: """Get property value with default.""" try: return self[key] except KeyError: return default def is_empty(self) -> bool: """Returns True if Component has no items or subcomponents, else False.""" return bool(not list(self.values()) + self.subcomponents) ############################# # handling of property values @classmethod def _encode(cls, name, value, parameters=None, encode=1): """Encode values to icalendar property values. :param name: Name of the property. :type name: string :param value: Value of the property. Either of a basic Python type of any of the icalendar's own property types. :type value: Python native type or icalendar property type. :param parameters: Property parameter dictionary for the value. Only available, if encode is set to True. :type parameters: Dictionary :param encode: True, if the value should be encoded to one of icalendar's own property types (Fallback is "vText") or False, if not. :type encode: Boolean :returns: icalendar property value """ if not encode: return value if isinstance(value, cls.types_factory.all_types): # Don't encode already encoded values. obj = value else: # Extract VALUE parameter if present, or infer it from the Python type value_param = None if parameters and "VALUE" in parameters: value_param = parameters["VALUE"] elif not isinstance(value, cls.types_factory.all_types): inferred = cls._infer_value_type(value) if inferred: value_param = inferred # Auto-set the VALUE parameter if parameters is None: parameters = {} if "VALUE" not in parameters: parameters["VALUE"] = inferred klass = cls.types_factory.for_property(name, value_param) obj = klass(value) if parameters: if not hasattr(obj, "params"): obj.params = Parameters() for key, item in parameters.items(): if item is None: if key in obj.params: del obj.params[key] else: obj.params[key] = item return obj def add( self, name: str, value, parameters: dict[str, str] | Parameters = None, encode: bool = True, ) -> None: """Add a property to this component. If the property already exists, the new value is appended so the property carries a list of values rather than replacing the previous one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED`` and ``value`` is a ``datetime``, the value is converted to UTC as the RFC requires. Parameters: name: Name of the property. value: Value of the property. Either a basic Python type or any of icalendar's own property types. parameters: Property parameter dictionary for the value. Only consulted when ``encode`` is ``True``. encode: ``True`` if the value should be encoded to one of icalendar's own property types (fallback is ``vText``); ``False`` to store the value as-is. Returns: ``None`` Example: >>> from icalendar import Event >>> event = Event() >>> event.add("summary", "Team sync") >>> event["summary"] vText(b'Team sync') """ if isinstance(value, datetime) and name.lower() in ( "dtstamp", "created", "last-modified", ): # RFC expects UTC for those... force value conversion. value = tzp.localize_utc(value) # encode value if ( encode and isinstance(value, list) and name.lower() not in ["rdate", "exdate", "categories"] ): # Individually convert each value to an ical type except rdate and # exdate, where lists of dates might be passed to vDDDLists. value = [self._encode(name, v, parameters, encode) for v in value] else: value = self._encode(name, value, parameters, encode) # set value if name in self: # If property already exists, append it. oldval = self[name] if isinstance(oldval, list): if isinstance(value, list): value = oldval + value else: oldval.append(value) value = oldval else: value = [oldval, value] self[name] = value def _decode(self, name: str, value: VPROPERTY): """Internal for decoding property values.""" # TODO: Currently the decoded method calls the icalendar.prop instances # from_ical. We probably want to decode properties into Python native # types here. But when parsing from an ical string with from_ical, we # want to encode the string into a real icalendar.prop property. if hasattr(value, "ical_value"): return value.ical_value if isinstance(value, vDDDLists): # TODO: Workaround unfinished decoding return value decoded = self.types_factory.from_ical(name, value) # TODO: remove when proper decoded is implemented in every prop.* class # Workaround to decode vText properly. vUnknown is not a vText # subclass (RFC 7265), but its value is decoded the same way here. if isinstance(decoded, (vText, vUnknown)): decoded = decoded.encode(DEFAULT_ENCODING) return decoded def decoded(self, name: str, default: Any = _marker) -> Any: """Returns decoded value of property. A component maps keys to icalendar property value types. This function returns values compatible to native Python types. """ if name in self: value = self[name] if isinstance(value, list): return [self._decode(name, v) for v in value] return self._decode(name, value) if default is _marker: raise KeyError(name) return default ######################################################################## # Inline values. A few properties have multiple values inlined in in one # property line. These methods are used for splitting and joining these. def get_inline(self, name, decode=1): """Returns a list of values (split on comma).""" vals = [v.strip('" ') for v in q_split(self[name])] if decode: return [self._decode(name, val) for val in vals] return vals def set_inline(self, name, values, encode=1): """Converts a list of values into comma separated string and sets value to that. """ if encode: values = [self._encode(name, value, encode=1) for value in values] self[name] = self.types_factory["inline"](q_join(values)) ######################### # Handling of components def add_component(self, component: Component) -> None: """Add a subcomponent to this component.""" self.subcomponents.append(component) def _walk( self, name: str | None, select: callable[[Component], bool] ) -> list[Component]: """Walk to given component.""" result = [] stack = [self] while stack: component = stack.pop() if (name is None or component.name == name) and select(component): result.append(component) stack.extend(reversed(component.subcomponents)) return result def walk( self, name: str | None = None, select: callable[[Component], bool] = lambda _: True, ) -> list[Component]: """Recursively traverses component and subcomponents. Returns sequence of same. If name is passed, only components with name will be returned. :param name: The name of the component or None such as ``VEVENT``. :param select: A function that takes the component as first argument and returns True/False. :returns: A list of components that match. :rtype: list[Component] """ if name is not None: name = name.upper() return self._walk(name, select) def with_uid(self, uid: str) -> list[Component]: """Return a list of components with the given UID. Parameters: uid: The UID of the component. Returns: list[Component]: List of components with the given UID. """ return self.walk(select=lambda c: c.uid == uid) ##################### # Generation def property_items( self, recursive: bool = True, sorted: bool = True, ) -> list[tuple[str, object]]: """Returns properties in this component and subcomponents as a list. The list contains ``(name, value)`` tuples. """ # Iterative implementation to avoid RecursionError result = [] v_text = self.types_factory["text"] # Stack stores (component, state) # state: True means we are processing the END of the component # state: False means we are processing the BEGIN and properties of the component stack = [(self, False)] while stack: comp, is_end = stack.pop() if is_end: result.append(("END", v_text(comp.name).to_ical())) else: result.append(("BEGIN", v_text(comp.name).to_ical())) property_names = comp.sorted_keys() if sorted else comp.keys() for name in property_names: values = comp[name] if isinstance(values, list): # normally one property is one line for value in values: result.append((name, value)) else: result.append((name, values)) # Push the END marker for this component stack.append((comp, True)) # Push subcomponents if recursion is enabled if recursive: # Push in reverse order to maintain original order in result for subcomponent in reversed(comp.subcomponents): stack.append((subcomponent, False)) return result @overload @classmethod def from_ical( cls, st: str | bytes, multiple: Literal[False] = False ) -> Component: ... @overload @classmethod def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ... @classmethod def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: """Get the iCal parser for the given input string.""" return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory) @classmethod def from_ical( cls, st: str | bytes | Path, multiple: bool = False ) -> Component | list[Component]: """Parse iCalendar data into component instances. Handles standard and custom components (``X-*``, IANA-registered). Parameters: st: iCalendar data as bytes or string, or a path to an iCalendar file as :class:`pathlib.Path` or string. multiple: If ``True``, returns list. If ``False``, returns single component. Returns: Component or list of components See Also: :doc:`/how-to/custom-components` for examples of parsing custom components """ if isinstance(st, Path): st = st.read_bytes() elif isinstance(st, str) and "\n" not in st and "\r" not in st: # A string is only probed as a file path when it contains no line # breaks. Valid iCalendar data is always folded with CRLF line # endings (RFC 5545), so real calendar content never reaches this # branch and is never read from disk. File paths, conversely, do # not contain line breaks on the platforms we support. try: is_file = Path(st).is_file() except (OSError, ValueError): # The string is not usable as a path on this platform (e.g. it # is too long, or contains characters the OS rejects such as an # embedded null byte). Treat it as calendar data, not a file, so # the parser raises a consistent ValueError across platforms. is_file = False if is_file: st = Path(st).read_bytes() parser = cls._get_ical_parser(st) components = parser.parse() if multiple: return components if len(components) > 1: raise ValueError( cls._format_error( "Found multiple components where only one is allowed", st ) ) if len(components) < 1: raise ValueError( cls._format_error( "Found no components where exactly one is required", st ) ) return components[0] @staticmethod def _format_error(error_description, bad_input, elipsis="[...]"): # there's three character more in the error, ie. ' ' x2 and a ':' max_error_length = 100 - 3 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length: truncate_to = max_error_length - len(error_description) - len(elipsis) return f"{error_description}: {bad_input[:truncate_to]} {elipsis}" return f"{error_description}: {bad_input}" def content_line(self, name, value, sorted: bool = True): """Returns property as content line.""" params = getattr(value, "params", Parameters()) return Contentline.from_parts(name, params, value, sorted=sorted) def content_lines(self, sorted: bool = True): """Converts the Component and subcomponents into content lines.""" contentlines = Contentlines() for name, value in self.property_items(sorted=sorted): cl = self.content_line(name, value, sorted=sorted) contentlines.append(cl) contentlines.append("") # remember the empty string in the end return contentlines def to_ical(self, sorted: bool = True): """ :param sorted: Whether parameters and properties should be lexicographically sorted. """ content_lines = self.content_lines(sorted=sorted) return content_lines.to_ical() def __repr__(self) -> str: """String representation of class with all of its subcomponents. Implemented iteratively rather than recursively so that calendars with deeply nested subcomponents do not raise ``RecursionError``. A pathological ``.ics`` payload of only ~13 KB can otherwise nest ``BEGIN:VEVENT`` ~500 levels and crash any caller that performs ``repr()``/``str()``/``f"{cal}"`` on the parsed calendar (e.g. logging, error reporting, debug pages). """ # Stack-based traversal. Each frame is one of: # ("open", component) -> emit "Name({props}" and schedule children # ("close",) -> emit ")" # ("comma",) -> emit ", " out: list[str] = [] stack: list[tuple] = [("open", self)] while stack: frame = stack.pop() kind = frame[0] if kind == "comma": out.append(", ") elif kind == "close": out.append(")") else: # "open" node = frame[1] if isinstance(node, Component): out.append(f"{node.name or type(node).__name__}({dict(node)}") subs = node.subcomponents if subs: # Defer ")" then push children in reverse so that # popping yields original order, with ", " separators # (the first popped comma serves as the separator # between the component's dict and its first child). stack.append(("close",)) for sub in reversed(subs): stack.append(("open", sub)) stack.append(("comma",)) else: out.append(")") else: # Should not normally occur (subcomponents are Components), # but be safe and fall back to non-recursive str(). out.append(str(node)) return "".join(out) def __eq__(self, other: Component) -> bool: if not isinstance(other, Component): return NotImplemented # Two components are equal when their own properties are equal and their # subcomponents are equal as a multiset: order does not matter, and each # nested pair is compared the same way recursively. Subcomponents are # neither sortable nor hashable, so we can't use a set; we have to match # each one by searching. Done recursively that search is exponential for # deeply nested components (GHSA-cv84-9p8j-fj68), so we walk an explicit # stack instead of recursing. # # Each frame holds the pair being compared plus b's subcomponents # still unmatched. child_result carries the outcome of the comparison # that just finished back up to its parent frame: a successful child # match removes that subcomponent from unmatched and advances to the # next a subcomponent, while a failure tries the next candidate. # Exhausting a's subcomponents means every one found a partner -> # equal; running out of candidates for some subcomponent -> not equal. # (Greedy matching is sufficient because equality is transitive, so equal # candidates are interchangeable.) stack = [_ComponentEqFrame(self, other)] child_result = None while stack: frame = stack[-1] if frame.unmatched is None: if len(frame.a.subcomponents) != len(frame.b.subcomponents) or not ( CaselessDict.__eq__(frame.a, frame.b) ): stack.pop() child_result = False continue frame.unmatched = list(frame.b.subcomponents) elif child_result is not None: if child_result: del frame.unmatched[frame.candidate_index] frame.a_index += 1 frame.candidate_index = 0 else: frame.candidate_index += 1 child_result = None if frame.a_index >= len(frame.a.subcomponents): stack.pop() child_result = True elif frame.candidate_index >= len(frame.unmatched): stack.pop() child_result = False else: stack.append( _ComponentEqFrame( frame.a.subcomponents[frame.a_index], frame.unmatched[frame.candidate_index], ) ) return child_result DTSTAMP = stamp = single_utc_property( "DTSTAMP", """The UTC datetime stamp recording when this component instance was created or last revised. This property is defined in :rfc:`5545#section-3.8.7.2`. It's required in ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VFREEBUSY`` components. When the calendar object carries a ``METHOD`` property, such as for scheduling, this value is the creation time of *this particular revision*. Without a ``METHOD`` property, it's equivalent to :attr:`LAST_MODIFIED`. The value is always in UTC. It's also accessible as :attr:`stamp`. Example: .. code-block:: pycon >>> from datetime import timezone, datetime >>> from icalendar import Event >>> event = Event() >>> event.DTSTAMP = datetime(2024, 6, 1, 12, 0, 0, tzinfo=timezone.utc) >>> event.DTSTAMP datetime.datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(key='UTC')) See also: :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, :attr:`created`, :attr:`stamp`, :attr:`last_modified` """, ) LAST_MODIFIED = single_utc_property( "LAST-MODIFIED", """The UTC datetime when this component's information was last revised, per :rfc:`5545#section-3.8.7.3`. It's analogous to a file's modification timestamp. This property is optional. When it's absent, :attr:`last_modified` falls back to :attr:`DTSTAMP`. This property is applicable to ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VTIMEZONE`` components. The value is always in UTC. Example: .. code-block:: pycon >>> from datetime import timezone, datetime >>> from icalendar import Event >>> event = Event() >>> event.LAST_MODIFIED = datetime(2024, 6, 1, 9, 0, 0, tzinfo=timezone.utc) >>> event.LAST_MODIFIED datetime.datetime(2024, 6, 1, 9, 0, tzinfo=ZoneInfo(key='UTC')) See also: :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, :attr:`created`, :attr:`stamp`, :attr:`last_modified` """, ) @property def last_modified(self) -> datetime: """Datetime when the information associated with the component was last revised. Since :attr:`LAST_MODIFIED` is an optional property, this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set. """ return self.LAST_MODIFIED or self.DTSTAMP @last_modified.setter def last_modified(self, value): self.LAST_MODIFIED = value @last_modified.deleter def last_modified(self): del self.LAST_MODIFIED @property def created(self) -> datetime: """Datetime when the information associated with the component was created. Since :attr:`CREATED` is an optional property, this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set. """ return self.CREATED or self.DTSTAMP @created.setter def created(self, value): self.CREATED = value @created.deleter def created(self): del self.CREATED def is_thunderbird(self) -> bool: """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" return any(attr.startswith("X-MOZ-") for attr in self.keys()) @staticmethod def _utc_now() -> datetime: """Return now as UTC value.""" return datetime.now(timezone.utc) uid = uid_property comments = comments_property links = links_property related_to = related_to_property concepts = concepts_property refids = refids_property CREATED = single_utc_property( "CREATED", """The UTC datetime when this calendar component was first created, per :rfc:`5545#section-3.8.7.1`. This property records when the calendar user agent originally stored the component. This property is optional. When it's absent, :attr:`created` falls back to :attr:`DTSTAMP`. This property is applicable to ``VEVENT``, ``VTODO``, and ``VJOURNAL`` components. The value is always in UTC. Example: .. code-block:: pycon >>> from datetime import timezone, datetime >>> from icalendar import Event >>> event = Event() >>> event.CREATED = datetime(2024, 1, 1, 8, 0, 0, tzinfo=timezone.utc) >>> event.CREATED datetime.datetime(2024, 1, 1, 8, 0, tzinfo=ZoneInfo(key='UTC')) See also: :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, :attr:`created`, :attr:`stamp`, :attr:`last_modified` """, ) _validate_new = True @staticmethod def _validate_start_and_end(start, end): """This validates start and end. Raises: ~error.InvalidCalendar: If the information is not valid """ if start is None or end is None: return if start > end: raise InvalidCalendar("end must be after start") @classmethod def new( cls, created: date | None = None, comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, stamp: date | None = None, subcomponents: Iterable[Component] | None = None, ) -> Self: """Create a new component. Parameters: comments: The :attr:`comments` of the component. concepts: The :attr:`concepts` of the component. created: The :attr:`created` of the component. last_modified: The :attr:`last_modified` of the component. links: The :attr:`links` of the component. related_to: The :attr:`related_to` of the component. stamp: The :attr:`DTSTAMP` of the component. subcomponents: The subcomponents of the component. Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ component = cls() component.DTSTAMP = stamp component.created = created component.last_modified = last_modified component.comments = comments component.links = links component.related_to = related_to component.concepts = concepts component.refids = refids if subcomponents is not None: component.subcomponents = ( subcomponents if isinstance(subcomponents, list) else list(subcomponents) ) return component def to_jcal(self) -> list: """Convert this component to a jCal object. Returns: jCal object See also :attr:`to_json`. In this example, we create a simple VEVENT component and convert it to jCal: .. code-block:: pycon >>> from icalendar import Event >>> from datetime import date >>> from pprint import pprint >>> event = Event.new(summary="My Event", start=date(2025, 11, 22)) >>> pprint(event.to_jcal()) ['vevent', [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'], ['summary', {}, 'text', 'My Event'], ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'], ['dtstart', {}, 'date', '2025-11-22']], []] """ # Iterative tree walk to avoid RecursionError on deeply nested # components, mirroring the iterative iCal parser/serializer (GH #1370). def make_node(comp: Component) -> list: properties = [ item.to_jcal(key.lower()) for key, value in comp.items() for item in (value if isinstance(value, list) else [value]) ] return [comp.name.lower(), properties, []] root_node = make_node(self) # stack of (component, jCal node) pairs still to expand stack: list[tuple[Component, list]] = [(self, root_node)] while stack: comp, node = stack.pop() children = node[2] for subcomponent in comp.subcomponents: child_node = make_node(subcomponent) children.append(child_node) stack.append((subcomponent, child_node)) return root_node def to_json(self) -> str: """Return this component as a jCal JSON string. Returns: JSON string See also :attr:`to_jcal`. """ return json.dumps(self.to_jcal()) @classmethod def from_jcal(cls, jcal: str | list) -> Component: """Create a component from a jCal list. Parameters: jcal: jCal list or JSON string according to :rfc:`7265`. Raises: ~error.JCalParsingError: If the jCal provided is invalid. ~json.JSONDecodeError: If the provided string is not valid JSON. This reverses :func:`to_json` and :func:`to_jcal`. The following code parses an example from :rfc:`7265`: .. code-block:: pycon >>> from icalendar import Component >>> jcal = ["vcalendar", ... [ ... ["calscale", {}, "text", "GREGORIAN"], ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], ... ["version", {}, "text", "2.0"] ... ], ... [ ... ["vevent", ... [ ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], ... ["dtstart", {}, "date", "2008-10-06"], ... ["summary", {}, "text", "Planning meeting"], ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"] ... ], ... [] ... ] ... ] ... ] >>> calendar = Component.from_jcal(jcal) >>> print(calendar.name) VCALENDAR >>> print(calendar.prodid) -//Example Inc.//Example Calendar//EN >>> event = calendar.events[0] >>> print(event.summary) Planning meeting """ if isinstance(jcal, str): jcal = json.loads(jcal) # Iterative tree build to avoid RecursionError on deeply nested jCal, # mirroring the iterative iCal parser (GH #1370). ``_node_from_jcal`` # parses a single component (without its subcomponents); the stack walks # the subcomponent tree, accumulating the jCal error path ([2, i] per # nesting level) so error messages match the recursive implementation. root, root_subcomponents = _node_from_jcal(jcal, cls) stack: list[tuple[Component, list, list]] = [(root, root_subcomponents, [])] while stack: parent, subcomponents, prefix = stack.pop() for i, subcomponent in enumerate(subcomponents): child_prefix = [*prefix, 2, i] # Prepend the full nesting path so errors match the recursive # implementation. This also preserves the error value and # traceback, like the nested context managers did before. with JCalParsingError.reraise_with_path_added(*child_prefix): child, child_subcomponents = _node_from_jcal( subcomponent, type(parent) ) parent.subcomponents.append(child) stack.append((child, child_subcomponents, child_prefix)) return root def copy(self, recursive: bool = False) -> Self: """Copy the component. Parameters: recursive: If ``True``, this creates copies of the component, its subcomponents, and all its properties. If ``False``, this only creates a shallow copy of the component. Returns: A copy of the component. Examples: Create a shallow copy of a component: .. code-block:: pycon >>> from icalendar import Event >>> event = Event.new(description="Event to be copied") >>> event_copy = event.copy() >>> str(event_copy.description) 'Event to be copied' Shallow copies lose their subcomponents: .. code-block:: pycon >>> from icalendar import Calendar >>> calendar = Calendar.example() >>> len(calendar.subcomponents) 3 >>> calendar_copy = calendar.copy() >>> len(calendar_copy.subcomponents) 0 A recursive copy also copies all the subcomponents: .. code-block:: pycon >>> full_calendar_copy = calendar.copy(recursive=True) >>> len(full_calendar_copy.subcomponents) 3 >>> full_calendar_copy.events[0] == calendar.events[0] True >>> full_calendar_copy.events[0] is calendar.events[0] False """ if recursive: return deepcopy(self) return super().copy() def is_lazy(self) -> bool: """This component is fully parsed.""" return False def parse(self) -> Self: """Return the fully parsed component. For non-lazy components, this returns self. For lazy components, this parses the component and returns the result. """ return self def _node_from_jcal(jcal, starting_cls: type[Component]) -> tuple[Component, list]: """Parse a single jCal component without recursing into subcomponents. Module-level helper for :meth:`Component.from_jcal`: it has no ties to a class or instance (the relevant class is passed in as ``starting_cls``), so it is a plain function rather than a (static) method. Parameters: jcal: The jCal list for one component. starting_cls: The class used as the parser for structural validation before the component type is resolved from its name (the entry class for the root, the parent's resolved class for a child). Returns: A ``(component, raw_subcomponents)`` tuple. The raw subcomponents are returned for the caller to walk iteratively. Raises: ~error.JCalParsingError: If this component node is invalid. The path is relative to this node; callers prepend the nesting path. """ if not isinstance(jcal, list) or len(jcal) != 3: raise JCalParsingError( "A component must be a list with 3 items.", starting_cls, value=jcal ) name, properties, subcomponents = jcal if not isinstance(name, str): raise JCalParsingError( "The name must be a string.", starting_cls, path=[0], value=name ) if name.upper() != starting_cls.name: # delegate to correct component class component_cls = starting_cls.get_component_class(name.upper()) else: component_cls = starting_cls component = component_cls() if not isinstance(properties, list): raise JCalParsingError( "The properties must be a list.", component_cls, path=1, value=properties, ) for i, prop in enumerate(properties): JCalParsingError.validate_property(prop, component_cls, path=[1, i]) prop_name = prop[0] prop_value = prop[2] prop_cls: type[VPROPERTY] = component_cls.types_factory.for_property( prop_name, prop_value ) with JCalParsingError.reraise_with_path_added(1, i): v_prop = prop_cls.from_jcal(prop) # jCal encodes the value type in the type field (``prop[2]``) # instead of as a ``VALUE`` parameter (RFC 7265). Restore that # parameter when the type differs from the property's default, so # explicit value types such as ``RDATE;VALUE=PERIOD`` or # ``TRIGGER;VALUE=DATE-TIME`` survive the round-trip (GH #1426). # A type equal to the default needs no VALUE parameter, and the # reserved ``unknown`` type must never become ``VALUE=UNKNOWN`` # (RFC 7265, section 5.2). default_type = component_cls.types_factory.default_value_type(prop_name) if isinstance(prop_value, str) and prop_value.lower() not in ( "unknown", default_type, ): v_prop.VALUE = prop_value.upper() elif "VALUE" in v_prop.params: del v_prop.VALUE component.add(prop_name, v_prop) if not isinstance(subcomponents, list): raise JCalParsingError( "The subcomponents must be a list.", component_cls, 2, value=subcomponents, ) return component, subcomponents __all__ = ["Component"] collective-icalendar-138c845/src/icalendar/cal/component_factory.py000066400000000000000000000064511524134033600254260ustar00rootroot00000000000000"""A factory to create components.""" from __future__ import annotations import re from typing import TYPE_CHECKING, Any from icalendar.caselessdict import CaselessDict if TYPE_CHECKING: from icalendar import Component class ComponentFactory(CaselessDict): """Registered components from :rfc:`7953` and :rfc:`5545`. To get a component, use this class as shown below. .. code-block:: pycon >>> from icalendar import ComponentFactory >>> factory = ComponentFactory() >>> event_class = factory.get_component_class('VEVENT') >>> event_class() VEVENT({}) Automatically creates custom component classes for unknown names (X-components, IANA-components). Custom components are never dropped per :rfc:`5545`. .. code-block:: pycon >>> factory = ComponentFactory() >>> custom_class = factory.get_component_class('X-VENDOR') >>> custom_class() X-VENDOR({}) If a component class is not yet supported, it can be either created using :meth:`get_component_class` or added manually as a subclass of :class:`~icalendar.cal.component.Component`. See :doc:`/how-to/custom-components` for details. """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Set keys to upper for initial dict.""" super().__init__(*args, **kwargs) from icalendar.cal.alarm import Alarm from icalendar.cal.availability import Availability from icalendar.cal.available import Available from icalendar.cal.calendar import Calendar from icalendar.cal.event import Event from icalendar.cal.free_busy import FreeBusy from icalendar.cal.journal import Journal from icalendar.cal.timezone import ( Timezone, TimezoneDaylight, TimezoneStandard, ) from icalendar.cal.todo import Todo self.add_component_class(Calendar) self.add_component_class(Event) self.add_component_class(Todo) self.add_component_class(Journal) self.add_component_class(FreeBusy) self.add_component_class(Timezone) self.add_component_class(TimezoneStandard) self.add_component_class(TimezoneDaylight) self.add_component_class(Alarm) self.add_component_class(Available) self.add_component_class(Availability) def add_component_class(self, cls: type[Component]) -> None: """Add a component class to the factory. Parameters: cls: The component class to add. """ self[cls.name] = cls def get_component_class(self, name: str) -> type[Component]: """Get the component class from the factory. This also creates and adds the component class if it does not exist. Parameters: name: The name of the component, for example, ``"VCALENDAR"``. Returns: The registered component class. """ component_class = self.get(name) if component_class is None: from icalendar.cal.component import Component component_class = type( re.sub(r"[^\w]+", "", name), (Component,), {"name": name.upper()} ) self.add_component_class(component_class) return component_class __all__ = ["ComponentFactory"] collective-icalendar-138c845/src/icalendar/cal/event.py000066400000000000000000000452641524134033600230230ustar00rootroot00000000000000""":rfc:`5545` VEVENT component.""" from __future__ import annotations import uuid from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Literal from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, REQUEST_STATUS_property, X_MOZ_LASTACK_property, X_MOZ_SNOOZE_TIME_property, attendees_property, categories_property, class_property, color_property, conferences_property, contacts_property, create_single_property, description_property, exdates_property, get_duration_property, get_end_property, get_start_end_duration_with_validation, get_start_property, images_property, location_property, organizer_property, priority_property, property_del_duration, property_doc_duration_template, property_get_duration, property_set_duration, rdates_property, rrules_property, sequence_property, set_duration_with_locking, set_end_with_locking, set_start_with_locking, status_property, summary_property, transparency_property, uid_property, url_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example if TYPE_CHECKING: from collections.abc import Iterable, Sequence from icalendar.alarms import Alarms from icalendar.compatibility import Self from icalendar.enums import CLASS, STATUS, TRANSP from icalendar.prop import vCalAddress from icalendar.prop.conference import Conference class Event(Component): """A grouping of component properties that describe an event. Description: A "VEVENT" calendar component is a grouping of component properties, possibly including "VALARM" calendar components, that represents a scheduled amount of time on a calendar. For example, it can be an activity; such as a one-hour long, department meeting from 8:00 AM to 9:00 AM, tomorrow. Generally, an event will take up time on an individual calendar. Hence, the event will appear as an opaque interval in a search for busy time. Alternately, the event can have its Time Transparency set to "TRANSPARENT" in order to prevent blocking of the event in searches for busy time. The "VEVENT" is also the calendar component used to specify an anniversary or daily reminder within a calendar. These events have a DATE value type for the "DTSTART" property instead of the default value type of DATE-TIME. If such a "VEVENT" has a "DTEND" property, it MUST be specified as a DATE value also. The anniversary type of "VEVENT" can span more than one date (i.e., "DTEND" property value is set to a calendar date after the "DTSTART" property value). If such a "VEVENT" has a "DURATION" property, it MUST be specified as a "dur-day" or "dur-week" value. The "DTSTART" property for a "VEVENT" specifies the inclusive start of the event. For recurring events, it also specifies the very first instance in the recurrence set. The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event. For cases where a "VEVENT" calendar component specifies a "DTSTART" property with a DATE value type but no "DTEND" nor "DURATION" property, the event's duration is taken to be one day. For cases where a "VEVENT" calendar component specifies a "DTSTART" property with a DATE-TIME value type but no "DTEND" property, the event ends on the same calendar date and time of day specified by the "DTSTART" property. The "VEVENT" calendar component cannot be nested within another calendar component. However, "VEVENT" calendar components can be related to each other or to a "VTODO" or to a "VJOURNAL" calendar component with the "RELATED-TO" property. Examples: The following is an example of the "VEVENT" calendar component used to represent a meeting that will also be opaque to searches for busy time: .. code-block:: ics BEGIN:VEVENT UID:19970901T130000Z-123401@example.com DTSTAMP:19970901T130000Z DTSTART:19970903T163000Z DTEND:19970903T190000Z SUMMARY:Annual Employee Review CLASS:PRIVATE CATEGORIES:BUSINESS,HUMAN RESOURCES END:VEVENT The following is an example of the "VEVENT" calendar component used to represent a reminder that will not be opaque, but rather transparent, to searches for busy time: .. code-block:: ics BEGIN:VEVENT UID:19970901T130000Z-123402@example.com DTSTAMP:19970901T130000Z DTSTART:19970401T163000Z DTEND:19970402T010000Z SUMMARY:Laurel is in sensitivity awareness class. CLASS:PUBLIC CATEGORIES:BUSINESS,HUMAN RESOURCES TRANSP:TRANSPARENT END:VEVENT The following is an example of the "VEVENT" calendar component used to represent an anniversary that will occur annually: .. code-block:: ics BEGIN:VEVENT UID:19970901T130000Z-123403@example.com DTSTAMP:19970901T130000Z DTSTART;VALUE=DATE:19971102 SUMMARY:Our Blissful Anniversary TRANSP:TRANSPARENT CLASS:CONFIDENTIAL CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION RRULE:FREQ=YEARLY END:VEVENT The following is an example of the "VEVENT" calendar component used to represent a multi-day event scheduled from June 28th, 2007 to July 8th, 2007 inclusively. Note that the "DTEND" property is set to July 9th, 2007, since the "DTEND" property specifies the non-inclusive end of the event. .. code-block:: ics BEGIN:VEVENT UID:20070423T123432Z-541111@example.com DTSTAMP:20070423T123432Z DTSTART;VALUE=DATE:20070628 DTEND;VALUE=DATE:20070709 SUMMARY:Festival International de Jazz de Montreal TRANSP:TRANSPARENT END:VEVENT Create a new Event: .. code-block:: python >>> from icalendar import Event >>> from datetime import datetime >>> event = Event.new(start=datetime(2021, 1, 1, 12, 30, 0)) >>> print(event.to_ical()) BEGIN:VEVENT DTSTART:20210101T123000 DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VEVENT """ name = "VEVENT" canonical_order = ( "SUMMARY", "DTSTART", "DTEND", "DURATION", "DTSTAMP", "UID", "RECURRENCE-ID", "SEQUENCE", "RRULE", "RDATE", "EXDATE", ) required = ( "UID", "DTSTAMP", ) singletons = ( "CLASS", "CREATED", "COLOR", "DESCRIPTION", "DTSTART", "GEO", "LAST-MODIFIED", "LOCATION", "ORGANIZER", "PRIORITY", "DTSTAMP", "SEQUENCE", "STATUS", "SUMMARY", "TRANSP", "URL", "RECURRENCE-ID", "DTEND", "DURATION", "UID", ) exclusive = ( "DTEND", "DURATION", ) multiple = ( "ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE", "REQUEST-STATUS", "RELATED", "RESOURCES", "RDATE", "RRULE", ) ignore_exceptions = True @property def alarms(self) -> Alarms: """Compute the alarm times for this component. >>> from icalendar import Event >>> event = Event.example("rfc_9074_example_1") >>> len(event.alarms.times) 1 >>> alarm_time = event.alarms.times[0] >>> alarm_time.trigger # The time when the alarm pops up datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) >>> alarm_time.is_active() # This alarm has not been acknowledged True Note that this only uses DTSTART and DTEND, but ignores RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms return Alarms(self) @classmethod def example(cls, name: str = "rfc_9074_example_3") -> Event: """Return the calendar example with the given name.""" return cls.from_ical(get_example("events", name)) DTSTART = create_single_property( "DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VEVENT" specifies the inclusive start of the event.', ) DTEND = create_single_property( "DTEND", "dt", (datetime, date), date, 'The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event.', ) def _get_start_end_duration(self): """Verify the calendar validity and return the right attributes.""" return get_start_end_duration_with_validation( self, "DTSTART", "DTEND", "VEVENT" ) DURATION = property( property_get_duration, property_set_duration, property_del_duration, property_doc_duration_template.format(component="VEVENT"), ) @property def duration(self) -> timedelta: """The duration of the VEVENT. Returns the DURATION property if set, otherwise calculated from start and end. When setting duration, the end time is automatically calculated from start + duration. You can set the duration to automatically adjust the end time while keeping start locked. Setting the duration will do the following. 1. Keep the start time locked (unchanged) 2. Adjust the end time to start + duration 3. Remove any existing DTEND property 4. Set the DURATION property """ return get_duration_property(self) @duration.setter def duration(self, value: timedelta): if not isinstance(value, timedelta): raise TypeError(f"Use timedelta, not {type(value).__name__}.") # Use the set_duration method with default start-locked behavior self.set_duration(value, locked="start") @property def start(self) -> date | datetime: """The start of the event. Invalid values raise an :exc:`~icalendar.error.InvalidCalendar`. If there is no start, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. You can get the start, end and duration of an event as follows: >>> from datetime import datetime >>> from icalendar import Event >>> event = Event() >>> event.start = datetime(2021, 1, 1, 12) >>> event.end = datetime(2021, 1, 1, 12, 30) # 30 minutes >>> event.duration # 1800 seconds == 30 minutes datetime.timedelta(seconds=1800) >>> print(event.to_ical()) BEGIN:VEVENT DTSTART:20210101T120000 DTEND:20210101T123000 END:VEVENT """ return get_start_property(self) @start.setter def start(self, start: date | datetime | None): """Set the start.""" self.DTSTART = start @property def end(self) -> date | datetime: """The end of the event. Invalid values raise an :exc:`~icalendar.error.InvalidCalendar` error. If there is no end, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. """ return get_end_property(self, "DTEND") @end.setter def end(self, end: date | datetime | None): """Set the end.""" self.DTEND = end def set_duration( self, duration: timedelta | None, locked: Literal["start", "end"] = "start" ): """Set the duration of the event relative to either start or end. Parameters: duration: The duration to set, or None to convert to DURATION property locked: Which property to keep unchanged ('start' or 'end') """ set_duration_with_locking(self, duration, locked, "DTEND") def set_start( self, start: date | datetime, locked: Literal["duration", "end"] | None = None ): """Set the start and keep the duration or end of the event. Parameters: start: The start time to set locked: Which property to keep unchanged ('duration', 'end', or None for auto-detect) """ set_start_with_locking(self, start, locked, "DTEND") def set_end( self, end: date | datetime, locked: Literal["start", "duration"] = "start" ): """Set the end of the component, keeping either the start or the duration same. Parameters: end: The end time to set locked: Which property to keep unchanged ('start' or 'duration') """ set_end_with_locking(self, end, locked, "DTEND") X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property X_MOZ_LASTACK = X_MOZ_LASTACK_property color = color_property sequence = sequence_property categories = categories_property rdates = rdates_property exdates = exdates_property rrules = rrules_property REQUEST_STATUS = REQUEST_STATUS_property uid = uid_property summary = summary_property description = description_property classification = class_property url = url_property organizer = organizer_property location = location_property priority = priority_property contacts = contacts_property transparency = transparency_property status = status_property attendees = attendees_property images = images_property conferences = conferences_property from icalendar.attr import RECURRENCE_ID @classmethod def new( cls, /, attendees: list[vCalAddress] | None = None, categories: Sequence[str] = (), classification: CLASS | None = None, color: str | None = None, comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, conferences: list[Conference] | None = None, contacts: list[str] | str | None = None, created: date | None = None, description: str | None = None, end: date | datetime | None = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, location: str | None = None, organizer: vCalAddress | str | None = None, priority: int | None = None, recurrence_id: date | datetime | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, request_status: list[str] | str | None = None, sequence: int | None = None, stamp: date | None = None, start: date | datetime | None = None, status: STATUS | None = None, subcomponents: Iterable[Component] | None = None, transparency: TRANSP | None = None, summary: str | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, ) -> Self: """Create a new event with the required properties of ``stamp`` and ``uid``. This creates a new ``Event`` in accordance with :rfc:`5545#section-3.6.1`. Parameters: attendees: The :attr:`attendees` of the event. categories: The :attr:`categories` of the event. classification: The :attr:`classification` of the event. color: The :attr:`color` of the event. comments: The :attr:`~icalendar.Component.comments` of the event. concepts: The :attr:`~icalendar.Component.concepts` of the event. conferences: The :attr:`conferences` of the event. created: The :attr:`~icalendar.Component.created` of the event. description: The :attr:`description` of the event. end: The :attr:`end` of the event. last_modified: The :attr:`~icalendar.Component.last_modified` of the event. links: The :attr:`~icalendar.Component.links` of the event. location: The :attr:`location` of the event. organizer: The :attr:`organizer` of the event. priority: The :attr:`priority` of the event. recurrence_id: The :attr:`RECURRENCE_ID` of the event. refids: :attr:`~icalendar.Component.refids` of the event. related_to: :attr:`~icalendar.Component.related_to` of the event. request_status: The :attr:`REQUEST_STATUS` of the event. sequence: The :attr:`sequence` of the event. stamp: The :attr:`~icalendar.Component.stamp` of the event. If ``None``, this is set to the current UTC time. start: The :attr:`start` of the event. status: The :attr:`status` of the event. subcomponents: The subcomponents of the event. summary: The :attr:`summary` of the event. transparency: The :attr:`transparency` of the event. uid: The :attr:`uid` of the event. If ``None``, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the event. Returns: :class:`Event` Raises: :exc:`~icalendar.error.InvalidCalendar`: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ event: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), created=created, last_modified=last_modified, comments=comments, links=links, related_to=related_to, refids=refids, concepts=concepts, subcomponents=subcomponents, ) event.summary = summary event.description = description event.uid = uid if uid is not None else uuid.uuid4() event.start = start event.end = end event.color = color event.categories = categories event.sequence = sequence event.classification = classification event.url = url event.organizer = organizer event.location = location event.priority = priority event.transparency = transparency event.contacts = contacts event.status = status event.REQUEST_STATUS = request_status event.attendees = attendees event.conferences = conferences event.RECURRENCE_ID = recurrence_id if cls._validate_new: cls._validate_start_and_end(start, end) return event __all__ = ["Event"] collective-icalendar-138c845/src/icalendar/cal/examples.py000066400000000000000000000013711524134033600235070ustar00rootroot00000000000000"""Access to examples of components.""" from __future__ import annotations from pathlib import Path def get_example(component_directory: str, example_name: str) -> bytes: """Return an example and raise an error if it is absent.""" here = Path(__file__).parent.parent examples = here / "tests" / component_directory if not example_name.endswith(".ics"): example_name = example_name + ".ics" example_file = examples / example_name if not example_file.is_file(): raise ValueError( f"Example {example_name} for {component_directory} not found. " f"You can use one of {', '.join(p.name for p in examples.iterdir())}" ) return Path(example_file).read_bytes() __all__ = ["get_example"] collective-icalendar-138c845/src/icalendar/cal/free_busy.py000066400000000000000000000143351524134033600236600ustar00rootroot00000000000000""":rfc:`5545` VFREEBUSY component.""" from __future__ import annotations import uuid from datetime import date, datetime, timedelta from typing import TYPE_CHECKING from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, REQUEST_STATUS_property, contacts_property, create_single_property, organizer_property, uid_property, url_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example if TYPE_CHECKING: from icalendar.compatibility import Self from icalendar.prop import vCalAddress class FreeBusy(Component): """ A "VFREEBUSY" calendar component is a grouping of component properties that represents either a request for free or busy time information, a reply to a request for free or busy time information, or a published set of busy time information. Examples: Create a new FreeBusy: >>> from icalendar import FreeBusy >>> free_busy = FreeBusy.new() >>> print(free_busy.to_ical()) BEGIN:VFREEBUSY DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VFREEBUSY Get the example FreeBusy. .. code-block:: pycon >>> from icalendar import FreeBusy >>> free_busy = FreeBusy.example() >>> print(free_busy.to_ical().decode()) BEGIN:VFREEBUSY DTEND:19980410T234500Z DTSTAMP:19970901T120000Z DTSTART:19980313T141711Z FREEBUSY:19980314T233000Z/19980315T003000Z FREEBUSY:19980316T153000Z/19980316T163000Z FREEBUSY:19980318T030000Z/19980318T040000Z ORGANIZER:jsmith@example.com UID:19970901T115957Z-76A912@example.com URL:http://www.example.com/calendar/busytime/jsmith.ifb END:VFREEBUSY """ name = "VFREEBUSY" required = ( "UID", "DTSTAMP", ) singletons = ( "CONTACT", "DTSTART", "DTEND", "DTSTAMP", "ORGANIZER", "UID", "URL", ) multiple = ( "ATTENDEE", "COMMENT", "FREEBUSY", "REQUEST-STATUS", ) uid = uid_property url = url_property organizer = organizer_property contacts = contacts_property REQUEST_STATUS = REQUEST_STATUS_property start = DTSTART = create_single_property( "DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VFREEBUSY" specifies the inclusive start of the component.', ) end = DTEND = create_single_property( "DTEND", "dt", (datetime, date), date, 'The "DTEND" property for a "VFREEBUSY" calendar component specifies the non-inclusive end of the component.', ) @property def duration(self) -> timedelta | None: """The duration computed from start and end.""" if self.DTSTART is None or self.DTEND is None: return None return self.DTEND - self.DTSTART @classmethod def new( cls, /, comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, contacts: list[str] | str | None = None, end: date | datetime | None = None, links: LINKS_TYPE_SETTER = None, organizer: vCalAddress | str | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, request_status: list[str] | str | None = None, stamp: date | None = None, start: date | datetime | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, ) -> Self: """Create a new FreeBusy component with all required properties, in accordance with :rfc:`5545#section-3.6.4`. The FreeBusy component has the required properties of UID and DTSTAMP, which may be set with the parameters of ``uid`` and ``stamp``. Parameters: comments: The :attr:`~icalendar.cal.component.Component.comments` of the component. concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the component. contacts: The :attr:`contacts` of the component. end: The :attr:`end` of the component. links: The :attr:`~icalendar.cal.component.Component.links` of the component. organizer: The :attr:`organizer` of the component. refids: :attr:`~icalendar.cal.component.Component.refids` of the component. related_to: :attr:`~icalendar.cal.component.Component.related_to` of the component. request_status: The :attr:`REQUEST_STATUS` of the component. stamp: The :attr:`~icalendar.cal.component.Component.DTSTAMP` of the component. If None, this is set to the current time. start: The :attr:`start` of the component. uid: The :attr:`uid` of the component. If None, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the component. Returns: :class:`FreeBusy` Raises: :exc:`~icalendar.error.InvalidCalendar`: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ free_busy: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), comments=comments, links=links, related_to=related_to, refids=refids, concepts=concepts, ) free_busy.uid = uid if uid is not None else uuid.uuid4() free_busy.url = url free_busy.organizer = organizer free_busy.contacts = contacts free_busy.REQUEST_STATUS = request_status free_busy.end = end free_busy.start = start if cls._validate_new: cls._validate_start_and_end(start, end) return free_busy @classmethod def example(cls, name: str = "example") -> FreeBusy: """Return the FreeBusy example with the given name.""" return cls.from_ical(get_example("freebusy", name)) __all__ = ["FreeBusy"] collective-icalendar-138c845/src/icalendar/cal/journal.py000066400000000000000000000227701524134033600233510ustar00rootroot00000000000000""":rfc:`5545` VJOURNAL component.""" from __future__ import annotations import uuid from datetime import date, datetime, timedelta from typing import TYPE_CHECKING from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, REQUEST_STATUS_property, attendees_property, categories_property, class_property, color_property, contacts_property, create_single_property, descriptions_property, exdates_property, images_property, organizer_property, rdates_property, rrules_property, sequence_property, status_property, summary_property, uid_property, url_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example from icalendar.error import IncompleteComponent if TYPE_CHECKING: from collections.abc import Sequence from icalendar.compatibility import Self from icalendar.enums import CLASS, STATUS from icalendar.prop import vCalAddress class Journal(Component): """A descriptive text at a certain time or associated with a component. Description: A "VJOURNAL" calendar component is a grouping of component properties that represent one or more descriptive text notes associated with a particular calendar date. The "DTSTART" property is used to specify the calendar date with which the journal entry is associated. Generally, it will have a DATE value data type, but it can also be used to specify a DATE-TIME value data type. Examples of a journal entry include a daily record of a legislative body or a journal entry of individual telephone contacts for the day or an ordered list of accomplishments for the day. Examples: Create a new Journal: >>> from icalendar import Journal >>> journal = Journal.new() >>> print(journal.to_ical()) BEGIN:VJOURNAL DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VJOURNAL Get the example Journal. .. code-block:: pycon >>> from icalendar import Journal >>> journal = Journal.example() >>> print(journal.to_ical().decode()) BEGIN:VJOURNAL DESCRIPTION:Aurora project plans were reviewed. DTSTAMP:19970901T130000Z DTSTART;VALUE=DATE:19970317 SUMMARY:Staff meeting minutes UID:19970901T130000Z-123405@example.com END:VJOURNAL """ name = "VJOURNAL" required = ( "UID", "DTSTAMP", ) singletons = ( "CLASS", "COLOR", "CREATED", "DTSTART", "DTSTAMP", "LAST-MODIFIED", "ORGANIZER", "RECURRENCE-ID", "SEQUENCE", "STATUS", "SUMMARY", "UID", "URL", ) multiple = ( "ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE", "RELATED", "RDATE", "RRULE", "REQUEST-STATUS", "DESCRIPTION", ) DTSTART = create_single_property( "DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VJOURNAL" that specifies the exact date at which the journal entry was made.', ) @property def start(self) -> date: """The start of the Journal. The "DTSTART" property is used to specify the calendar date with which the journal entry is associated. """ start = self.DTSTART if start is None: raise IncompleteComponent("No DTSTART given.") return start @start.setter def start(self, value: datetime | date) -> None: """Set the start of the journal.""" self.DTSTART = value end = start @property def duration(self) -> timedelta: """The journal has no duration: timedelta(0).""" return timedelta(0) color = color_property sequence = sequence_property categories = categories_property rdates = rdates_property exdates = exdates_property rrules = rrules_property REQUEST_STATUS = REQUEST_STATUS_property uid = uid_property summary = summary_property descriptions = descriptions_property classification = class_property url = url_property organizer = organizer_property contacts = contacts_property status = status_property attendees = attendees_property from icalendar.attr import RECURRENCE_ID @property def description(self) -> str: """The concatenated descriptions of the journal. A Journal can have several descriptions. This is a compatibility method. """ descriptions = self.descriptions if not descriptions: return None return "\r\n\r\n".join(descriptions) @description.setter def description(self, description: str | None): """Set the description""" self.descriptions = description @description.deleter def description(self): """Delete all descriptions.""" del self.descriptions images = images_property @classmethod def new( cls, /, attendees: list[vCalAddress] | None = None, categories: Sequence[str] = (), classification: CLASS | None = None, color: str | None = None, comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, contacts: list[str] | str | None = None, created: date | None = None, description: str | Sequence[str] | None = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, organizer: vCalAddress | str | None = None, recurrence_id: date | datetime | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, request_status: list[str] | str | None = None, sequence: int | None = None, stamp: date | None = None, start: date | datetime | None = None, status: STATUS | None = None, summary: str | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, ) -> Self: """Create a new journal entry with all required properties. This creates a new Journal in accordance with :rfc:`5545`. Parameters: attendees: The :attr:`attendees` of the journal. categories: The :attr:`categories` of the journal. classification: The :attr:`classification` of the journal. color: The :attr:`color` of the journal. comments: The :attr:`~icalendar.Component.comments` of the journal. concepts: The :attr:`~icalendar.Component.concepts` of the journal. contacts: The :attr:`contacts` of the journal. created: The :attr:`~icalendar.Component.created` of the journal. description: The :attr:`description` of the journal. last_modified: The :attr:`~icalendar.Component.last_modified` of the journal. links: The :attr:`~icalendar.Component.links` of the journal. organizer: The :attr:`organizer` of the journal. recurrence_id: The :attr:`RECURRENCE_ID` of the journal. refids: :attr:`~icalendar.Component.refids` of the journal. related_to: :attr:`~icalendar.Component.related_to` of the journal. request_status: The :attr:`REQUEST_STATUS` of the journal. sequence: The :attr:`sequence` of the journal. stamp: The :attr:`~icalendar.Component.stamp` of the journal. If None, this is set to the current time. start: The :attr:`start` of the journal. status: The :attr:`status` of the journal. summary: The :attr:`summary` of the journal. uid: The :attr:`uid` of the journal. If None, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the journal. Returns: :class:`Journal` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ journal: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), created=created, last_modified=last_modified, comments=comments, links=links, related_to=related_to, refids=refids, concepts=concepts, ) journal.summary = summary journal.descriptions = description journal.uid = uid if uid is not None else uuid.uuid4() journal.start = start journal.color = color journal.categories = categories journal.sequence = sequence journal.classification = classification journal.url = url journal.organizer = organizer journal.contacts = contacts journal.start = start journal.status = status journal.REQUEST_STATUS = request_status journal.attendees = attendees journal.RECURRENCE_ID = recurrence_id return journal @classmethod def example(cls, name: str = "example") -> Journal: """Return the journal example with the given name.""" return cls.from_ical(get_example("journals", name)) __all__ = ["Journal"] collective-icalendar-138c845/src/icalendar/cal/lazy.py000066400000000000000000000243071524134033600226540ustar00rootroot00000000000000"""Components for lazy parsing of components.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal from icalendar.cal.component_factory import ComponentFactory from icalendar.parser.ical.lazy import LazyCalendarIcalParser from .calendar import Calendar if TYPE_CHECKING: from collections.abc import Callable from icalendar.parser.ical.component import ComponentIcalParser from icalendar.parser.ical.lazy import LazySubcomponent from .component import Component class ParsedSubcomponentsStrategy: """All the subcomponents are parsed and available as a list.""" def __init__(self) -> None: self._components: list[Component] = [] def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar.""" return self, self._components def set_components( self, components: list[Component] ) -> ParsedSubcomponentsStrategy: """Set the subcomponents of the calendar.""" self._components = components return self def add_component(self, component: Component) -> ParsedSubcomponentsStrategy: """Add a component to the calendar.""" self._components.append(component.parse()) return self def is_lazy(self) -> Literal[False]: """Returns ``False`` because subcomponents are not lazily parsed.""" return False def walk(self, name: str) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar with the given name.""" result = [] for component in self._components: result += component.walk(name) return self, result def with_uid( self, name: str ) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar with the given uid.""" result = [] for component in self._components: result += component.with_uid(name) return self, result class LazySubcomponentsStrategy: """Parse subcomponents only when accessed.""" initial_components_to_parse: tuple[str, ...] = ("VTIMEZONE",) """Parse these subcomponents before any others.""" def __init__(self) -> None: self._components: list[LazySubcomponent | Component] = [] self._initial_parsed: bool = False @property def as_parsed(self) -> ParsedSubcomponentsStrategy: """Return the parsed components.""" return ParsedSubcomponentsStrategy().set_components( [component.parse() for component in self._components] ) def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar. Parse all subcomponents. """ self.parse_initial_components() return self.as_parsed.get_all_components() def set_components( self, components: list[Component] ) -> ParsedSubcomponentsStrategy: """Set the subcomponents of the calendar.""" return ParsedSubcomponentsStrategy().set_components(components) def add_component( self, component: Component | LazySubcomponent ) -> LazySubcomponentsStrategy: """Add a component to the calendar.""" self._components.append(component) return self def is_lazy(self) -> bool: """Return whether the subcomponents may be lazily parsed.""" return True def parse_initial_components(self) -> None: """Parse the components that are required by other components. This mainly concerns the timezone components. They are required by other components that have a TZID parameter. """ if self._initial_parsed: return self._initial_parsed = True for component in self._components: if component.name in self.initial_components_to_parse: component.parse() def walk( self, name: str | None ) -> tuple[LazySubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar with the given name. Parse only the minimal number of subcomponents. """ if name is None: return self.as_parsed.walk(name) self.parse_initial_components() result = [] for component in self._components: result += component.walk(name) return self, result def with_uid(self, uid: str) -> tuple[LazySubcomponentsStrategy, list[Component]]: """Get the subcomponents of the calendar with the given ``uid``. Parse only the minimal number of subcomponents. """ self.parse_initial_components() result = [] for component in self._components: result += component.with_uid(uid) return self, result class InitialSubcomponentsStrategy: """Initial strategy for the calendar. No subcomponents. """ def set_components(self, components: list[Component]) -> LazySubcomponentsStrategy: if components: raise ValueError( "Cannot set subcomponents on an uninitialised LazyCalendar. " "Parse it first or add components via add_component()." ) return LazySubcomponentsStrategy() class LazyCalendar(Calendar): """A calendar that can handle big files. Subcomponents of this calendar are evaluated lazily, meaning that they are not parsed until they are accessed. This allows the calendar to handle large files without consuming too much memory or time. All properties of the calendar component are parsed immediately. Subcomponents and their properties are parsed lazily. Examples: By accessing the :attr:`~icalendar.cal.calendar.Calendar.events` of the calendar, only :class:`~icalendar.cal.event.Event` and :class:`~icalendar.cal.timezone.Timezone` are immediately parsed. .. code-block:: pycon >>> from icalendar import LazyCalendar >>> calendar = LazyCalendar.example("issue_1050_all_components") >>> len(calendar.events) == 1 True The calendar's subcomponents were not parsed because they were not accessed. The calendar is still lazy. >>> calendar.is_lazy() True When you access all :attr:`~icalendar.cal.component.Component.subcomponents` of the calendar, for example by getting their count, the entire calendar is parsed and becomes not lazy. >>> len(calendar.subcomponents) 5 >>> calendar.is_lazy() False """ _subcomponents: ( LazySubcomponentsStrategy | ParsedSubcomponentsStrategy | InitialSubcomponentsStrategy ) """The strategy pattern for subcomponents of the calendar.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the calendar.""" self._subcomponents = InitialSubcomponentsStrategy() super().__init__(*args, **kwargs) @property def subcomponents(self) -> list[Component]: """The subcomponents of the calendar. Parse all subcomponents of the calendar and return them as a list. You can manipulate this list or set it. It has the same behavior as in :class:`~icalendar.cal.calendar.Calendar`. """ self._subcomponents, result = self._subcomponents.get_all_components() return result @subcomponents.setter def subcomponents(self, value: list[Component]) -> None: """Set the subcomponents of the calendar.""" self._subcomponents = self._subcomponents.set_components(value) @classmethod def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: """Get the iCal parser for the given input string.""" return LazyCalendarIcalParser( st, cls._get_component_factory(), cls.types_factory ) @classmethod def _get_component_factory(cls) -> ComponentFactory: """Get the component factory for this calendar.""" factory = ComponentFactory() factory.add_component_class(cls) return factory def add_component(self, component: Component) -> None: """Add a component to the calendar. Use this instead of appending to :attr:`~icalendar.cal.lazy.LazyCalendar.subcomponents`, as the latter does not parse the whole calendar. """ self._subcomponents = self._subcomponents.add_component(component) def is_lazy(self) -> bool: """Whether the subcomponents will be parsed lazily. .. note:: If you believe that the calendar parses more than it should, please `open an issue `_. Returns: ``True`` if subcomponents are deferred and not yet parsed. ``False`` if all subcomponents have been parsed. """ return self._subcomponents.is_lazy() def _walk( self, name: str | None, select: Callable[[Component], bool] ) -> list[Component]: self._subcomponents, result = self._subcomponents.walk(name) result = [component for component in result if select(component)] if (name is None or self.name == name) and select(self): result.insert(0, self) return result def with_uid(self, uid: str) -> list[Component]: self._subcomponents, result = self._subcomponents.with_uid(uid) if self.uid == uid: result.insert(0, self) return result with_uid.__doc__ = Calendar.with_uid.__doc__ __all__ = ["LazyCalendar"] if __name__ == "__main__": import timeit calendar = Calendar.example("issue_1050_all_components") COUNT = 10000 calendar.subcomponents *= COUNT ics = calendar.to_ical() def _benchmark(cal: type[Calendar]): """Check out how fast this is.""" cal = cal.from_ical(ics) assert len(cal.events) == COUNT for cal in [Calendar, LazyCalendar]: print("Benchmarking:", cal.__name__) # noqa: T201 print(timeit.timeit("_benchmark(cal)", globals=locals(), number=1)) # noqa: T201 # Benchmarking: Calendar # 12.277852076000272 # Benchmarking: LazyCalendar # 5.738950790999297 collective-icalendar-138c845/src/icalendar/cal/timezone.py000066400000000000000000000416501524134033600235270ustar00rootroot00000000000000""":rfc:`5545` components for timezone information.""" from __future__ import annotations from collections import defaultdict from datetime import date, datetime, timedelta, tzinfo from typing import TYPE_CHECKING import dateutil.rrule import dateutil.tz from icalendar.attr import ( create_single_property, exdates_property, rdates_property, rrules_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example from icalendar.prop import vUTCOffset from icalendar.timezone import TZP, tzp from icalendar.timezone.tzid import tzid_from_tzinfo from icalendar.tools import to_datetime if TYPE_CHECKING: from icalendar.cal.calendar import Calendar class Timezone(Component): """ A "VTIMEZONE" calendar component is a grouping of component properties that defines a time zone. It is used to describe the way in which a time zone changes its offset from UTC over time. """ subcomponents: list[TimezoneStandard | TimezoneDaylight] name = "VTIMEZONE" canonical_order = ("TZID",) required = ("TZID",) # it also requires one of components DAYLIGHT and STANDARD singletons = ( "TZID", "LAST-MODIFIED", "TZURL", ) DEFAULT_FIRST_DATE = date(1970, 1, 1) DEFAULT_LAST_DATE = date(2038, 1, 1) @classmethod def example(cls, name: str = "pacific_fiji") -> Calendar: """Return the timezone example with the given name.""" return cls.from_ical(get_example("timezones", name)) @staticmethod def _extract_offsets(component: TimezoneDaylight | TimezoneStandard, tzname: str): """extract offsets and transition times from a VTIMEZONE component :param component: a STANDARD or DAYLIGHT component :param tzname: the name of the zone """ offsetfrom = component.TZOFFSETFROM offsetto = component.TZOFFSETTO dtstart = component.DTSTART # offsets need to be rounded to the next minute, we might loose up # to 30 seconds accuracy, but it can't be helped (datetime # supposedly cannot handle smaller offsets) offsetto_s = int((offsetto.seconds + 30) / 60) * 60 offsetto = timedelta(days=offsetto.days, seconds=offsetto_s) offsetfrom_s = int((offsetfrom.seconds + 30) / 60) * 60 offsetfrom = timedelta(days=offsetfrom.days, seconds=offsetfrom_s) # expand recurrences if "RRULE" in component: # to be paranoid about correct weekdays # evaluate the rrule with the current offset tzi = dateutil.tz.tzoffset("(offsetfrom)", offsetfrom) rrstart = dtstart.replace(tzinfo=tzi) rrulestr = component["RRULE"].to_ical().decode("utf-8") rrule = dateutil.rrule.rrulestr(rrulestr, dtstart=rrstart) tzp.fix_rrule_until(rrule, component["RRULE"]) # constructing the timezone requires UTC transition times. # here we construct local times without tzinfo, the offset to UTC # gets subtracted in to_tz(). transtimes = [dt.replace(tzinfo=None) for dt in rrule] # or rdates elif "RDATE" in component: if not isinstance(component["RDATE"], list): rdates = [component["RDATE"]] else: rdates = component["RDATE"] transtimes = [dtstart] + [leaf.dt for tree in rdates for leaf in tree.dts] else: transtimes = [dtstart] transitions = [ (transtime, offsetfrom, offsetto, tzname) for transtime in set(transtimes) ] if component.name == "STANDARD": is_dst = 0 elif component.name == "DAYLIGHT": is_dst = 1 return is_dst, transitions @staticmethod def _make_unique_tzname(tzname, tznames): """ :param tzname: Candidate tzname :param tznames: Other tznames """ # TODO better way of making sure tznames are unique while tzname in tznames: tzname += "_1" tznames.add(tzname) return tzname def to_tz(self, tzp: TZP = tzp, lookup_tzid: bool = True): """convert this VTIMEZONE component to a timezone object :param tzp: timezone provider to use :param lookup_tzid: whether to use the TZID property to look up existing timezone definitions with tzp. If it is False, a new timezone will be created. If it is True, the existing timezone will be used if it exists, otherwise a new timezone will be created. """ if lookup_tzid: tz = tzp.timezone(self.tz_name) if tz is not None: return tz return tzp.create_timezone(self) @property def tz_name(self) -> str: """Return the name of the timezone component. Please note that the names of the timezone are different from this name and may change with winter/summer time. """ try: return str(self["TZID"]) except UnicodeEncodeError: return self["TZID"].encode("ascii", "replace") def get_transitions( self, ) -> tuple[list[datetime], list[tuple[timedelta, timedelta, str]]]: """Return a tuple of (transition_times, transition_info) - transition_times = [datetime, ...] - transition_info = [(TZOFFSETTO, dts_offset, tzname)] """ zone = self.tz_name transitions = [] dst = {} tznames = set() for component in self.walk(): if isinstance(component, Timezone): continue try: tzname = str(component["TZNAME"]) except UnicodeEncodeError: tzname = component["TZNAME"].encode("ascii", "replace") tzname = self._make_unique_tzname(tzname, tznames) except KeyError: # for whatever reason this is str/unicode tzname = ( f"{zone}_{component['DTSTART'].to_ical().decode('utf-8')}_" f"{component['TZOFFSETFROM'].to_ical()}_" f"{component['TZOFFSETTO'].to_ical()}" ) tzname = self._make_unique_tzname(tzname, tznames) dst[tzname], component_transitions = self._extract_offsets( component, tzname ) transitions.extend(component_transitions) transitions.sort() transition_times = [ transtime - osfrom for transtime, osfrom, _, _ in transitions ] # transition_info is a list with tuples in the format # (utcoffset, dstoffset, name) # dstoffset = 0, if current transition is to standard time # = this_utcoffset - prev_standard_utcoffset, otherwise transition_info = [] for num, (_transtime, osfrom, osto, name) in enumerate(transitions): dst_offset = False if not dst[name]: dst_offset = timedelta(seconds=0) else: # go back in time until we find a transition to dst for index in range(num - 1, -1, -1): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto break # when the first transition is to dst, we didn't find anything # in the past, so we have to look into the future if not dst_offset: for index in range(num, len(transitions)): if not dst[transitions[index][3]]: # [3] is the name dst_offset = osto - transitions[index][2] # [2] is osto break # If we still haven't found a STANDARD transition # (only DAYLIGHT exists), calculate dst_offset as the # difference from TZOFFSETFROM. Handles Issue #321. if dst_offset is False: dst_offset = osto - osfrom transition_info.append((osto, dst_offset, name)) return transition_times, transition_info # binary search _from_tzinfo_skip_search = [ timedelta(days=days) for days in (64, 32, 16, 8, 4, 2, 1) ] + [ # we know it happens in the night usually around 1am timedelta(hours=4), timedelta(hours=1), # adding some minutes and seconds for faster search timedelta(minutes=20), timedelta(minutes=5), timedelta(minutes=1), timedelta(seconds=20), timedelta(seconds=5), timedelta(seconds=1), ] @classmethod def from_tzinfo( cls, timezone: tzinfo, tzid: str | None = None, first_date: date = DEFAULT_FIRST_DATE, last_date: date = DEFAULT_LAST_DATE, ) -> Timezone: """Return a VTIMEZONE component from a timezone object. This works with pytz and zoneinfo and any other timezone. The offsets are calculated from the tzinfo object. Parameters: :param tzinfo: the timezone object :param tzid: the tzid for this timezone. If None, it will be extracted from the tzinfo. :param first_date: a datetime that is earlier than anything that happens in the calendar :param last_date: a datetime that is later than anything that happens in the calendar :raises ValueError: If we have no tzid and cannot extract one. .. note:: This can take some time. Please cache the results. """ if tzid is None: tzid = tzid_from_tzinfo(timezone) if tzid is None: raise ValueError( f"Cannot get TZID from {timezone}. Please set the tzid parameter." ) normalize = getattr(timezone, "normalize", lambda dt: dt) # pytz compatibility first_datetime = datetime(first_date.year, first_date.month, first_date.day) last_datetime = datetime(last_date.year, last_date.month, last_date.day) if hasattr(timezone, "localize"): # pytz compatibility first_datetime = timezone.localize(first_datetime) last_datetime = timezone.localize(last_datetime) else: first_datetime = first_datetime.replace(tzinfo=timezone) last_datetime = last_datetime.replace(tzinfo=timezone) # from, to, tzname, is_standard -> start offsets: dict[tuple[timedelta | None, timedelta, str, bool], list[datetime]] = ( defaultdict(list) ) start = first_datetime offset_to = None while start < last_datetime: offset_from = offset_to end = start offset_to = end.utcoffset() for add_offset in cls._from_tzinfo_skip_search: last_end = end # we need to save this as we might be left and right of the time change end = normalize(end + add_offset) try: while end.utcoffset() == offset_to: last_end = end end = normalize(end + add_offset) except OverflowError: # zoninfo does not go all the way break # retract if we overshoot end = last_end # Now, start (inclusive) -> end (exclusive) are one timezone is_standard = start.dst() == timedelta() name = start.tzname() if name is None: name = str(offset_to) key = (offset_from, offset_to, name, is_standard) # first_key = (None,) + key[1:] # if first_key in offsets: # # remove the first one and claim it changes at that day # offsets[first_key] = offsets.pop(first_key) offsets[key].append(start.replace(tzinfo=None)) start = normalize(end + cls._from_tzinfo_skip_search[-1]) tz = cls() tz.add("TZID", tzid) tz.add("COMMENT", f"This timezone only works from {first_date} to {last_date}.") for (offset_from, offset_to, tzname, is_standard), starts in offsets.items(): first_start = min(starts) starts.remove(first_start) if first_start.date() == last_date: first_start = datetime(last_date.year, last_date.month, last_date.day) subcomponent = TimezoneStandard() if is_standard else TimezoneDaylight() if offset_from is None: offset_from = offset_to subcomponent.TZOFFSETFROM = offset_from subcomponent.TZOFFSETTO = offset_to subcomponent.add("TZNAME", tzname) subcomponent.DTSTART = first_start if starts: subcomponent.add("RDATE", starts) tz.add_component(subcomponent) return tz @classmethod def from_tzid( cls, tzid: str, tzp: TZP = tzp, first_date: date = DEFAULT_FIRST_DATE, last_date: date = DEFAULT_LAST_DATE, ) -> Timezone: """Create a VTIMEZONE from a tzid like ``"Europe/Berlin"``. :param tzid: the id of the timezone :param tzp: the timezone provider :param first_date: a datetime that is earlier than anything that happens in the calendar :param last_date: a datetime that is later than anything that happens in the calendar :raises ValueError: If the tzid is unknown. >>> from icalendar import Timezone >>> tz = Timezone.from_tzid("Europe/Berlin") >>> print(tz.to_ical()[:36]) BEGIN:VTIMEZONE TZID:Europe/Berlin .. note:: This can take some time. Please cache the results. """ tz = tzp.timezone(tzid) if tz is None: raise ValueError(f"Unkown timezone {tzid}.") return cls.from_tzinfo(tz, tzid, first_date, last_date) @property def standard(self) -> list[TimezoneStandard]: """The STANDARD subcomponents as a list.""" return self.walk("STANDARD") @property def daylight(self) -> list[TimezoneDaylight]: """The DAYLIGHT subcomponents as a list. These are for the daylight saving time. """ return self.walk("DAYLIGHT") class TimezoneStandard(Component): """ The "STANDARD" sub-component of "VTIMEZONE" defines the standard time offset from UTC for a time zone. It represents a time zone's standard time, typically used during winter months in locations that observe Daylight Saving Time. """ name = "STANDARD" required = ("DTSTART", "TZOFFSETTO", "TZOFFSETFROM") singletons = ( "DTSTART", "TZOFFSETTO", "TZOFFSETFROM", ) multiple = ("COMMENT", "RDATE", "TZNAME", "RRULE", "EXDATE") DTSTART = create_single_property( "DTSTART", "dt", (datetime,), datetime, """The mandatory "DTSTART" property gives the effective onset date and local time for the time zone sub-component definition. "DTSTART" in this usage MUST be specified as a date with a local time value.""", convert=to_datetime, ) TZOFFSETTO = create_single_property( "TZOFFSETTO", "td", (timedelta,), timedelta, """The mandatory "TZOFFSETTO" property gives the UTC offset for the time zone sub-component (Standard Time or Daylight Saving Time) when this observance is in use. """, vUTCOffset, ) TZOFFSETFROM = create_single_property( "TZOFFSETFROM", "td", (timedelta,), timedelta, """The mandatory "TZOFFSETFROM" property gives the UTC offset that is in use when the onset of this time zone observance begins. "TZOFFSETFROM" is combined with "DTSTART" to define the effective onset for the time zone sub-component definition. For example, the following represents the time at which the observance of Standard Time took effect in Fall 1967 for New York City: DTSTART:19671029T020000 TZOFFSETFROM:-0400 """, vUTCOffset, ) rdates = rdates_property exdates = exdates_property rrules = rrules_property class TimezoneDaylight(Component): """ The "DAYLIGHT" sub-component of "VTIMEZONE" defines the daylight saving time offset from UTC for a time zone. It represents a time zone's daylight saving time, typically used during summer months in locations that observe Daylight Saving Time. """ name = "DAYLIGHT" required = TimezoneStandard.required singletons = TimezoneStandard.singletons multiple = TimezoneStandard.multiple DTSTART = TimezoneStandard.DTSTART TZOFFSETTO = TimezoneStandard.TZOFFSETTO TZOFFSETFROM = TimezoneStandard.TZOFFSETFROM rdates = rdates_property exdates = exdates_property rrules = rrules_property __all__ = ["Timezone", "TimezoneDaylight", "TimezoneStandard"] collective-icalendar-138c845/src/icalendar/cal/todo.py000066400000000000000000000340321524134033600226360ustar00rootroot00000000000000""":rfc:`5545` VTODO component.""" from __future__ import annotations import uuid from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Literal from icalendar.attr import ( CONCEPTS_TYPE_SETTER, LINKS_TYPE_SETTER, RELATED_TO_TYPE_SETTER, REQUEST_STATUS_property, X_MOZ_LASTACK_property, X_MOZ_SNOOZE_TIME_property, attendees_property, categories_property, class_property, color_property, conferences_property, contacts_property, create_single_property, description_property, exdates_property, get_duration_property, get_end_property, get_start_end_duration_with_validation, get_start_property, images_property, location_property, organizer_property, priority_property, property_del_duration, property_doc_duration_template, property_get_duration, property_set_duration, rdates_property, rrules_property, sequence_property, set_duration_with_locking, set_end_with_locking, set_start_with_locking, status_property, summary_property, uid_property, url_property, ) from icalendar.cal.component import Component from icalendar.cal.examples import get_example if TYPE_CHECKING: from collections.abc import Iterable, Sequence from icalendar.alarms import Alarms from icalendar.compatibility import Self from icalendar.enums import CLASS, STATUS from icalendar.prop import vCalAddress from icalendar.prop.conference import Conference class Todo(Component): """ A "VTODO" calendar component is a grouping of component properties that represents an action item or assignment. For example, it can be used to represent an item of work assigned to an individual, such as "Prepare for the upcoming conference seminar on Internet Calendaring". Examples: Create a new Todo: >>> from icalendar import Todo >>> todo = Todo.new() >>> print(todo.to_ical()) BEGIN:VTODO DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VTODO Complete the example Todo. .. code-block:: pycon >>> from datetime import datetime, timezone >>> from icalendar import Todo, STATUS >>> todo = Todo.example() >>> todo["PERCENT-COMPLETE"] = 100 >>> todo["COMPLETED"] = datetime(2007, 5, 1, 12, tzinfo=timezone.utc) >>> todo.status = STATUS.COMPLETED >>> print(todo.to_ical().decode()) BEGIN:VTODO CATEGORIES:FAMILY,FINANCE CLASS:CONFIDENTIAL COMPLETED:2007-05-01 12:00:00+00:00 DTSTAMP:20070313T123432Z DUE;VALUE=DATE:20070501 PERCENT-COMPLETE:100 STATUS:COMPLETED SUMMARY:Submit Quebec Income Tax Return for 2006 UID:20070313T123432Z-456553@example.com END:VTODO """ name = "VTODO" required = ( "UID", "DTSTAMP", ) singletons = ( "CLASS", "COLOR", "COMPLETED", "CREATED", "DESCRIPTION", "DTSTAMP", "DTSTART", "GEO", "LAST-MODIFIED", "LOCATION", "ORGANIZER", "PERCENT-COMPLETE", "PRIORITY", "RECURRENCE-ID", "SEQUENCE", "STATUS", "SUMMARY", "UID", "URL", "DUE", "DURATION", ) exclusive = ( "DUE", "DURATION", ) multiple = ( "ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE", "REQUEST-STATUS", "RELATED", "RESOURCES", "RDATE", "RRULE", ) DTSTART = create_single_property( "DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', ) DUE = create_single_property( "DUE", "dt", (datetime, date), date, 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', ) DURATION = property( property_get_duration, property_set_duration, property_del_duration, property_doc_duration_template.format(component="VTODO"), ) def _get_start_end_duration(self): """Verify the calendar validity and return the right attributes.""" return get_start_end_duration_with_validation(self, "DTSTART", "DUE", "VTODO") @property def start(self) -> date | datetime: """The start of the VTODO. Invalid values raise an :exc:`~icalendar.error.InvalidCalendar`. If there is no start, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. You can get the start, end and duration of a Todo as follows: >>> from datetime import datetime >>> from icalendar import Todo >>> todo = Todo() >>> todo.start = datetime(2021, 1, 1, 12) >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes >>> todo.duration # 1800 seconds == 30 minutes datetime.timedelta(seconds=1800) >>> print(todo.to_ical()) BEGIN:VTODO DTSTART:20210101T120000 DUE:20210101T123000 END:VTODO """ return get_start_property(self) @start.setter def start(self, start: date | datetime | None): """Set the start.""" self.DTSTART = start @property def end(self) -> date | datetime: """The end of the todo. Invalid values raise an :exc:`~icalendar.error.InvalidCalendar` error. If there is no end, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. """ return get_end_property(self, "DUE") @end.setter def end(self, end: date | datetime | None): """Set the end.""" self.DUE = end @property def duration(self) -> timedelta: """The duration of the VTODO. Returns the DURATION property if set, otherwise calculated from start and end. You can set the duration to automatically adjust the end time while keeping start locked. Setting the duration will do the following. 1. Keep the start time locked (unchanged) 2. Adjust the end time to start + duration 3. Remove any existing DUE property 4. Set the DURATION property """ return get_duration_property(self) @duration.setter def duration(self, value: timedelta): if not isinstance(value, timedelta): raise TypeError(f"Use timedelta, not {type(value).__name__}.") # Use the set_duration method with default start-locked behavior self.set_duration(value, locked="start") def set_duration( self, duration: timedelta | None, locked: Literal["start", "end"] = "start" ): """Set the duration of the event relative to either start or end. Parameters: duration: The duration to set, or None to convert to DURATION property locked: Which property to keep unchanged ('start' or 'end') """ set_duration_with_locking(self, duration, locked, "DUE") def set_start( self, start: date | datetime, locked: Literal["duration", "end"] | None = None ): """Set the start with explicit locking behavior. Parameters: start: The start time to set locked: Which property to keep unchanged ('duration', 'end', or None for auto-detect) """ set_start_with_locking(self, start, locked, "DUE") def set_end( self, end: date | datetime, locked: Literal["start", "duration"] = "start" ): """Set the end of the component, keeping either the start or the duration same. Parameters: end: The end time to set locked: Which property to keep unchanged ('start' or 'duration') """ set_end_with_locking(self, end, locked, "DUE") X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property X_MOZ_LASTACK = X_MOZ_LASTACK_property @property def alarms(self) -> Alarms: """Compute the alarm times for this component. >>> from datetime import datetime >>> from icalendar import Todo >>> todo = Todo() # empty without alarms >>> todo.start = datetime(2024, 10, 26, 10, 21) >>> len(todo.alarms.times) 0 Note that this only uses DTSTART and DUE, but ignores RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms return Alarms(self) color = color_property sequence = sequence_property categories = categories_property rdates = rdates_property exdates = exdates_property rrules = rrules_property REQUEST_STATUS = REQUEST_STATUS_property uid = uid_property summary = summary_property description = description_property classification = class_property url = url_property organizer = organizer_property location = location_property priority = priority_property contacts = contacts_property status = status_property attendees = attendees_property images = images_property conferences = conferences_property from icalendar.attr import RECURRENCE_ID @classmethod def new( cls, /, attendees: list[vCalAddress] | None = None, categories: Sequence[str] = (), classification: CLASS | None = None, color: str | None = None, comments: list[str] | str | None = None, concepts: CONCEPTS_TYPE_SETTER = None, contacts: list[str] | str | None = None, conferences: list[Conference] | None = None, created: date | None = None, description: str | None = None, end: date | datetime | None = None, last_modified: date | None = None, links: LINKS_TYPE_SETTER = None, location: str | None = None, organizer: vCalAddress | str | None = None, priority: int | None = None, recurrence_id: date | datetime | None = None, refids: list[str] | str | None = None, related_to: RELATED_TO_TYPE_SETTER = None, request_status: list[str] | str | None = None, sequence: int | None = None, stamp: date | None = None, start: date | datetime | None = None, status: STATUS | None = None, subcomponents: Iterable[Component] | None = None, summary: str | None = None, uid: str | uuid.UUID | None = None, url: str | None = None, ) -> Self: """Create a new TODO with all required properties. This creates a new Todo in accordance with :rfc:`5545`. Parameters: attendees: The :attr:`attendees` of the todo. categories: The :attr:`categories` of the todo. classification: The :attr:`classification` of the todo. color: The :attr:`color` of the todo. comments: The :attr:`~icalendar.Component.comments` of the todo. concepts: The :attr:`~icalendar.Component.concepts` of the todo. contacts: The :attr:`contacts` of the todo. conferences: The :attr:`conferences` of the todo. created: The :attr:`~icalendar.Component.created` of the todo. description: The :attr:`description` of the todo. end: The :attr:`end` of the todo. last_modified: The :attr:`~icalendar.Component.last_modified` of the todo. links: The :attr:`~icalendar.Component.links` of the todo. location: The :attr:`location` of the todo. organizer: The :attr:`organizer` of the todo. recurrence_id: The :attr:`RECURRENCE_ID` of the todo. refids: :attr:`~icalendar.Component.refids` of the todo. related_to: :attr:`~icalendar.Component.related_to` of the todo. request_status: The :attr:`REQUEST_STATUS` of the todo. sequence: The :attr:`sequence` of the todo. stamp: The :attr:`~icalendar.Component.DTSTAMP` of the todo. If None, this is set to the current time. start: The :attr:`start` of the todo. status: The :attr:`status` of the todo. subcomponents: The subcomponents of the todo. summary: The :attr:`summary` of the todo. uid: The :attr:`uid` of the todo. If None, this is set to a new :func:`uuid.uuid4`. url: The :attr:`url` of the todo. Returns: :class:`Todo` Raises: ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ todo: Self = super().new( stamp=stamp if stamp is not None else cls._utc_now(), created=created, last_modified=last_modified, comments=comments, links=links, related_to=related_to, refids=refids, concepts=concepts, subcomponents=subcomponents, ) todo.summary = summary todo.description = description todo.uid = uid if uid is not None else uuid.uuid4() todo.start = start todo.end = end todo.color = color todo.categories = categories todo.sequence = sequence todo.classification = classification todo.url = url todo.organizer = organizer todo.location = location todo.priority = priority todo.contacts = contacts todo.status = status todo.REQUEST_STATUS = request_status todo.attendees = attendees todo.conferences = conferences todo.RECURRENCE_ID = recurrence_id if cls._validate_new: cls._validate_start_and_end(start, end) return todo @classmethod def example(cls, name: str = "example") -> Todo: """Return the todo example with the given name.""" return cls.from_ical(get_example("todos", name)) __all__ = ["Todo"] collective-icalendar-138c845/src/icalendar/caselessdict.py000066400000000000000000000237331524134033600236060ustar00rootroot00000000000000from __future__ import annotations from collections import OrderedDict from typing import TYPE_CHECKING, Any, TypeVar from icalendar.parser_tools import to_unicode if TYPE_CHECKING: from collections.abc import Iterable, Mapping try: from typing import Self except ImportError: from typing_extensions import Self KT = TypeVar("KT") VT = TypeVar("VT") def canonsort_keys( keys: Iterable[KT], canonical_order: Iterable[KT] | None = None ) -> list[KT]: """Sort leading keys according to a canonical order. Keys specified in ``canonical_order`` appear first in that order. Remaining keys appear alphabetically at the end. Parameters: keys: The keys to sort. canonical_order: The preferred order for leading keys. Keys not in this sequence are sorted alphabetically after the canonical ones. If ``None``, all keys are sorted alphabetically. Returns: A new list of keys sorted by canonical order first, then alphabetically. Example: .. code-block:: pycon >>> from icalendar.caselessdict import canonsort_keys >>> canonsort_keys(["C", "A", "B"], ["B", "C"]) ['B', 'C', 'A'] """ canonical_map = {k: i for i, k in enumerate(canonical_order or [])} head = [k for k in keys if k in canonical_map] tail = [k for k in keys if k not in canonical_map] return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail) def canonsort_items( dict1: Mapping[KT, VT], canonical_order: Iterable[KT] | None = None ) -> list[tuple[KT, VT]]: """Sort items from a mapping according to a canonical key order. Parameters: dict1: The mapping whose items to sort. canonical_order: The preferred order for leading keys. If ``None``, all keys are sorted alphabetically. Returns: A list of ``(key, value)`` tuples sorted by canonical order. Example: .. code-block:: pycon >>> from icalendar.caselessdict import canonsort_items >>> canonsort_items({"C": 3, "A": 1, "B": 2}, ["B", "C"]) [('B', 2), ('C', 3), ('A', 1)] """ return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)] class CaselessDict(OrderedDict): """A case-insensitive dictionary that uses strings as keys. All keys are stored in uppercase internally, but values retain their original case. Keys can be provided as ``str`` or ``bytes``. They are converted to Unicode via :func:`~icalendar.parser_tools.to_unicode`, then uppercased before storage. """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Parameters: *args: Positional arguments passed to :class:`~collections.OrderedDict`. **kwargs: Keyword arguments passed to :class:`~collections.OrderedDict`. Example: Create a new ``CaselessDict`` and normalize existing keys to uppercase. .. code-block:: pycon >>> from icalendar.caselessdict import CaselessDict >>> d = CaselessDict(summary="Meeting") >>> d["SUMMARY"] 'Meeting' >>> "summary" in d True """ super().__init__(*args, **kwargs) for key, value in self.items(): key_upper = to_unicode(key).upper() if key != key_upper: super().__delitem__(key) self[key_upper] = value __hash__ = None def __getitem__(self, key: Any) -> Any: """Get the item from the ``CaselessDict`` instance by ``key``, case-insensitively. Parameters: key: The key to look up, case-insensitively. Returns: The (key, value) pair associated with the uppercased key. Raises: KeyError: If the key is not found. """ key = to_unicode(key) return super().__getitem__(key.upper()) def __setitem__(self, key: Any, value: Any) -> None: """Set a (key, value) pair, storing the key in uppercase. Parameters: key: The key of the pair, case-insensitive. value: The value to associate with the key. """ key = to_unicode(key) super().__setitem__(key.upper(), value) def __delitem__(self, key: Any) -> None: """Delete a (key, value) pair by its case-insensitive key. Parameters: key: The key to delete, case-insensitively. Raises: KeyError: If the key is not found. """ key = to_unicode(key) super().__delitem__(key.upper()) def __contains__(self, key: Any) -> bool: """Check whether a key exists in the mapping, case-insensitively. Parameters: key: The key to check case-insensitively. Returns: ``True`` if the uppercased key exists, else ``False``. """ key = to_unicode(key) return super().__contains__(key.upper()) def get(self, key: Any, default: Any = None) -> Any: """Return the ``key``, optionally with a ``default`` value. Parameters: key: The key to look up, case-insensitively. default: The value to return if the key is not found. Returns: The value for the key, if present, else the value specified by ``default``. """ key = to_unicode(key) return super().get(key.upper(), default) def setdefault(self, key: Any, value: Any = None) -> Any: """Create the (key, value) pair, optionally with a ``value``. Once set, to change default value use :meth:`update`. Parameters: key: The key to look up or create, case-insensitively. value: The default value to set, if given, else ``None``. Returns: The value for the key. """ key = to_unicode(key) return super().setdefault(key.upper(), value) def pop(self, key: Any, default: Any = None) -> Any: """Remove and return the value for ``key``, or ``default`` if not found. Parameters: key: The key to remove, case-insensitively. default: The value to return if the key is not found. Returns: The removed value, or the value of ``default``. """ key = to_unicode(key) return super().pop(key.upper(), default) def popitem(self) -> tuple[Any, Any]: """Remove and return the last inserted (key, value) pair. Returns: A (key, value) tuple. Raises: KeyError: If the dictionary is empty. """ return super().popitem() def has_key(self, key: Any) -> bool: """Check whether a key exists, case-insensitively. This is a legacy method. Use ``key in dict`` instead. Parameters: key: The key to check, case-insensitively. Returns: ``True`` if the key exists, else ``False``. """ key = to_unicode(key) return super().__contains__(key.upper()) def update(self, *args: Any, **kwargs: Any) -> None: """Update the dictionary with (key, value) pairs, normalizing keys to uppercase. Multiple keys that differ only in case will overwrite each other. Only the last value is retained. Parameters: *args: Mappings or iterables of (key, value) pairs. **kwargs: Additional (key, value) pairs. """ # Multiple keys where key1.upper() == key2.upper() will be lost. mappings = list(args) + [kwargs] for mapping in mappings: if hasattr(mapping, "items"): mapping = iter(mapping.items()) # noqa: PLW2901 for key, value in mapping: self[key] = value def copy(self) -> Self: """Return a shallow copy of the dictionary. Returns: A new instance of the same type with the same contents. """ return type(self)(super().copy()) def __repr__(self) -> str: """Return a string representation of the dictionary. Returns: A string in the form ``CaselessDict({...})``. """ return f"{type(self).__name__}({dict(self)})" def __eq__(self, other: object) -> bool: """Check equality with another dictionary. Two ``CaselessDict`` instances are equal if they contain the same (key, value) pairs after uppercasing keys. Comparison with a regular ``dict`` also works. Parameters: other: The object to compare. Returns: ``True`` if equal, ``NotImplemented`` if ``other`` is not a ``dict``. """ if not isinstance(other, dict): return NotImplemented return self is other or dict(self.items()) == dict(other.items()) def __ne__(self, other: object) -> bool: """Check inequality with another dictionary. Parameters: other: The object to compare. Returns: ``True`` if not equal, else ``False``. """ return not self == other # A list of keys that must appear first in sorted_keys and sorted_items; # must be uppercase. canonical_order = None def sorted_keys(self) -> list[str]: """Sort keys according to the canonical order for this class. Keys listed in :attr:`canonical_order` appear first in that order. Remaining keys appear alphabetically at the end. Returns: A sorted list of keys. """ return canonsort_keys(self.keys(), self.canonical_order) def sorted_items(self) -> list[tuple[Any, Any]]: """Sort items according to the canonical order for this class. Items whose keys are listed in :attr:`canonical_order` appear first in that order. Remaining items appear alphabetically by key. Returns: A sorted list of (key, value) tuples. """ return canonsort_items(self, self.canonical_order) __all__ = ["CaselessDict", "canonsort_items", "canonsort_keys"] collective-icalendar-138c845/src/icalendar/cli.py000077500000000000000000000103421524134033600217020ustar00rootroot00000000000000#!/usr/bin/env python3 """Utility program that allows user to preview calendar's events""" import argparse import sys from datetime import datetime from pathlib import Path from icalendar import __version__, vCalAddress from icalendar.cal.calendar import Calendar from icalendar.cal.event import Event def _format_name(address: str) -> str: """Format a display name and email from an address string. Parameters: address: An address object, such as mailto:name@example.com. Returns: A formatted string, like 'name ', or an empty string if no email is found. """ email = address.rsplit(":", maxsplit=1)[-1] name = email.split("@")[0] if not email: return "" return f"{name} <{email}>" def _format_attendees(attendees: list | str | vCalAddress) -> str: """Format the list of attendees. Parameters: attendees: Either a list, a string, or a vCalAddress object. Returns: A formatted string of attendees, each indented by 5 spaces. """ if isinstance(attendees, str): attendees = [attendees] return "\n".join(s.rjust(len(s) + 5) for s in map(_format_name, attendees)) def view(event: Event) -> str: """Make a human readable summary of an iCalendar file. Parameters: event: An iCalendar event containing fields such as summary, organizer, attendees, location, and timing. Returns: A human readable summary of the event. """ summary = event.get("summary", default="") organizer = _format_name(event.get("organizer", default="")) attendees = _format_attendees(event.get("attendee", default=[])) location = event.get("location", default="") comment = event.get("comment", "") description = event.get("description", "").split("\n") description = "\n".join(s.rjust(len(s) + 5) for s in description) start = event.decoded("dtstart") if "duration" in event: end = event.decoded("dtend", default=start + event.decoded("duration")) else: end = event.decoded("dtend", default=start) duration = event.decoded("duration", default=end - start) if isinstance(start, datetime): start = start.astimezone() start = start.strftime("%c") if isinstance(end, datetime): end = end.astimezone() end = end.strftime("%c") return f""" Organizer: {organizer} Attendees: {attendees} Summary : {summary} Starts : {start} End : {end} Duration : {duration} Location : {location} Comment : {comment} Description: {description}""" def main(): """Parse command-line arguments and print a summary of calendar events. Reads one or more iCalendar files or stdin, and writes a human-readable summary of each event to the specified output file or stdout. """ parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "calendar_files", nargs="+", help="one or more .ics files (use '-' for stdin)" ) parser.add_argument( "--output", "-o", default="-", help="output file path (use '-' for stdout)" ) parser.add_argument( "-v", "--version", action="version", version=f"{parser.prog} version {__version__}", ) argv = parser.parse_args() # Open output file if argv.output == "-": output_file = sys.stdout close_output = False else: output_file = Path(argv.output).open("w", encoding="utf-8") # noqa: SIM115 close_output = True try: # Iterate over input paths for path in argv.calendar_files: if path == "-": f = sys.stdin close_input = False else: f = Path(path).open(encoding="utf-8-sig") # noqa: SIM115 close_input = True try: calendar = Calendar.from_ical(f.read()) output_file.writelines( view(event) + "\n\n" for event in calendar.walk("vevent") ) finally: if close_input: f.close() finally: if close_output: output_file.close() __all__ = ["main", "view"] if __name__ == "__main__": main() collective-icalendar-138c845/src/icalendar/compatibility.py000066400000000000000000000030411524134033600237770ustar00rootroot00000000000000"""This module contains compatibility code for different Python versions. All compatibility checks and imports should go here. This way, we can centralize the handling of different Python versions. Do NOT import this module directly if you use icalendar. Members will be added and removed without deprecation warnings. """ import functools import warnings from typing import TYPE_CHECKING try: from typing import Self except ImportError: try: from typing_extensions import Self except ImportError: Self = "Self" if TYPE_CHECKING: import sys from typing import TypeGuard if sys.version_info >= (3, 13): from typing import TypeIs else: from typing_extensions import TypeIs if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self else: # we cannot use a TypeGuard = "TypeGuard" hack since it's used with a parameter TypeGuard = TypeIs = Self = None def deprecate_for_version_8(func): """Issue a warning for deprecated functions to be removed in version 8.""" public_name = func.__name__.removeprefix("_") @functools.wraps(func) def wrapper(*args, **kwargs): warnings.warn( f"{public_name} is deprecated and will be removed in icalendar 8", DeprecationWarning, stacklevel=2, ) wrapper.__name__ = public_name return func(*args, **kwargs) return wrapper __all__ = [ "Self", "TypeGuard", "TypeIs", "deprecate_for_version_8", ] collective-icalendar-138c845/src/icalendar/config.py000066400000000000000000000017421524134033600224010ustar00rootroot00000000000000"""Runtime configuration for icalendar.""" MAX_ALARM_REPEAT: int = 10_000 """Cap on additional triggers expanded from a ``VALARM`` ``REPEAT`` property. :rfc:`5545#section-3.8.6.2` defines ``REPEAT`` as an ``INTEGER`` data type, which is defined in :rfc:`5545#section-3.3.8` as an integer between -2147483648 and 2147483647. A value for ``REPEAT`` that is greater than approximately ``10_000`` can exhaust memory or CPU. The default value of ``10_000`` is a practical setting, but may be adjusted to specific use cases. Set to ``-1`` to disable the cap, which should be done only for fully trusted input. .. versionadded:: 7.2.1 """ def _clamp_repeat(n: int) -> int: """Return *n* clamped to ``[0, MAX_ALARM_REPEAT]``. Negative values are treated as 0. When :data:`MAX_ALARM_REPEAT` is ``-1`` the value is returned unchanged after clamping negatives to 0. """ if n < 0: return 0 if MAX_ALARM_REPEAT < 0: return n return min(n, MAX_ALARM_REPEAT) collective-icalendar-138c845/src/icalendar/enums.py000066400000000000000000000411411524134033600222600ustar00rootroot00000000000000"""Enumerations for different types in the RFCs.""" from enum import Enum as _Enum class Enum(_Enum): """Enum class that can be pickled.""" def __reduce_ex__(self, _p): """For pickling.""" return self.__class__, (self._name_,) class StrEnum(str, Enum): """Enum for strings.""" def __str__(self) -> str: """Convert to a string. This is needed when we set the value directly in components. """ return self.value class PARTSTAT(StrEnum): """Enum for PARTSTAT from :rfc:`5545`. Values: ``NEEDS_ACTION``, ``ACCEPTED``, ``DECLINED``, ``TENTATIVE``, ``DELEGATED``, ``COMPLETED``, ``IN_PROCESS`` Purpose: To specify the participation status for the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the participation status for the calendar user specified by the property value. The parameter values differ depending on whether they are associated with a group-scheduled "VEVENT", "VTODO", or "VJOURNAL". The values MUST match one of the values allowed for the given calendar component. If not specified on a property that allows this parameter, the default value is NEEDS-ACTION. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the NEEDS-ACTION value. """ NEEDS_ACTION = "NEEDS-ACTION" ACCEPTED = "ACCEPTED" DECLINED = "DECLINED" TENTATIVE = "TENTATIVE" DELEGATED = "DELEGATED" COMPLETED = "COMPLETED" IN_PROCESS = "IN-PROCESS" class STATUS(StrEnum): """Enum for STATUS from :rfc:`5545`. Values for :class:`icalendar.cal.event.Event`: ``CONFIRMED``, ``TENTATIVE``, ``CANCELLED`` Values for :class:`icalendar.cal.todo.Todo`: ``NEEDS_ACTION``, ``COMPLETED``, ``IN_PROCESS``, ``CANCELLED`` Values for :class:`icalendar.cal.journal.Journal`: ``DRAFT``, ``FINAL``, ``CANCELLED`` Description: In a group-scheduled calendar component, the property is used by the "Organizer" to provide a confirmation of the event to the "Attendees". For example in a "VEVENT" calendar component, the "Organizer" can indicate that a meeting is tentative, confirmed, or cancelled. In a "VTODO" calendar component, the "Organizer" can indicate that an action item needs action, is completed, is in process or being worked on, or has been cancelled. In a "VJOURNAL" calendar component, the "Organizer" can indicate that a journal entry is draft, final, or has been cancelled or removed. """ # Event TENTATIVE = "TENTATIVE" CONFIRMED = "CONFIRMED" CANCELLED = "CANCELLED" # VTodo NEEDS_ACTION = "NEEDS-ACTION" COMPLETED = "COMPLETED" IN_PROCESS = "IN-PROCESS" # CANCELLED # Journal DRAFT = "DRAFT" FINAL = "FINAL" # CANCELLED class FBTYPE(StrEnum): """Enum for FBTYPE from :rfc:`5545`. Values: ``FREE``, ``BUSY``, ``BUSY_UNAVAILABLE``, ``BUSY_TENTATIVE`` See also :class:`BUSYTYPE`. Purpose: To specify the free or busy time type. Description: This parameter specifies the free or busy time type. The value FREE indicates that the time interval is free for scheduling. The value BUSY indicates that the time interval is busy because one or more events have been scheduled for that interval. The value BUSY-UNAVAILABLE indicates that the time interval is busy and that the interval can not be scheduled. The value BUSY-TENTATIVE indicates that the time interval is busy because one or more events have been tentatively scheduled for that interval. If not specified on a property that allows this parameter, the default is BUSY. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the BUSY value. """ FREE = "FREE" BUSY = "BUSY" BUSY_UNAVAILABLE = "BUSY-UNAVAILABLE" BUSY_TENTATIVE = "BUSY-TENTATIVE" class CUTYPE(StrEnum): """Enum for CTYPE from :rfc:`5545`. Values: ``INDIVIDUAL``, ``GROUP``, ``RESOURCE``, ``ROOM``, ``UNKNOWN`` Purpose: To identify the type of calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the type of calendar user specified by the property. If not specified on a property that allows this parameter, the default is INDIVIDUAL. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the UNKNOWN value. """ INDIVIDUAL = "INDIVIDUAL" GROUP = "GROUP" RESOURCE = "RESOURCE" ROOM = "ROOM" UNKNOWN = "UNKNOWN" class RELTYPE(StrEnum): """Enum for RELTYPE from :rfc:`5545`. Values: ``PARENT``, ``CHILD``, ``SIBLING``, ``FINISHTOSTART``, ``FINISHTOFINISH``, ``STARTTOFINISH``, ``STARTTOSTART``, ``FIRST``, ``NEXT``, ``DEPENDS-ON``, ``REFID``, ``CONCEPT`` Purpose: To specify the type of hierarchical relationship associated with the calendar component specified by the property. Description: This parameter can be specified on a property that references another related calendar. The parameter specifies the hierarchical relationship type of the calendar component referenced by the property. The parameter value can be PARENT, to indicate that the referenced calendar component is a superior of calendar component; CHILD to indicate that the referenced calendar component is a subordinate of the calendar component; or SIBLING to indicate that the referenced calendar component is a peer of the calendar component. If this parameter is not specified on an allowable property, the default relationship type is PARENT. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the PARENT value. New Temporal RELTYPE Parameter Values (:rfc:`9253`): RELTYPE=FINISHTOSTART Task-B cannot start until Task-A finishes. For example, when painting is complete, carpet laying can begin. RELTYPE=FINISHTOFINISH Task-B can only be completed after Task-A is finished. The related tasks may run in parallel before completion. RELTYPE=STARTTOFINISH The start of Task-A (which occurs after Task-B) controls the finish of Task-B. For example, ticket sales (Task-B) end after the game starts (Task-A). RELTYPE=STARTTOSTART The start of Task-A triggers the start of Task-B, that is, Task-B can start anytime after Task-A starts. Additional New RELTYPE Parameter Values (:rfc:`9253`): RELTYPE=FIRST This indicates that the referenced calendar component is the first in a series the referencing calendar component is part of. RELTYPE=NEXT This indicates that the referenced calendar component is the next in a series the referencing calendar component is part of. RELTYPE=DEPENDS-ON This indicates that the current calendar component depends on the referenced calendar component in some manner. For example, a task may be blocked waiting on the other, referenced, task. RELTYPE=REFID This establishes a reference from the current component to components with a REFID property that matches the value given in the associated RELATED-TO property. RELTYPE=CONCEPT This establishes a reference from the current component to components with a CONCEPT property that matches the value given in the associated RELATED-TO property. Note that the relationship types of PARENT, CHILD, and SIBLING establish a hierarchical relationship. The new types of FIRST and NEXT are an ordering relationship. Examples: The following is an example from :rfc:`5545`: .. code-block:: ics RELATED-TO;RELTYPE=SIBLING:19960401-080045-4000F192713@ example.com Usage inside of RELATED-TO according to :rfc:`9253`: .. code-block:: ics RELATED-TO;VALUE=URI;RELTYPE=STARTTOFINISH: https://example.com/caldav/user/jb/cal/ 19960401-080045-4000F192713.ics """ PARENT = "PARENT" CHILD = "CHILD" SIBLING = "SIBLING" FINISHTOSTART = "FINISHTOSTART" FINISHTOFINISH = "FINISHTOFINISH" STARTTOFINISH = "STARTTOFINISH" STARTTOSTART = "STARTTOSTART" FIRST = "FIRST" NEXT = "NEXT" DEPENDS_ON = "DEPENDS-ON" REFID = "REFID" CONCEPT = "CONCEPT" class RANGE(StrEnum): """Enum for RANGE from :rfc:`5545`. Values: ``THISANDFUTURE``, ``THISANDPRIOR`` (deprecated) Purpose: To specify the effective range of recurrence instances from the instance specified by the recurrence identifier specified by the property. Description: This parameter can be specified on a property that specifies a recurrence identifier. The parameter specifies the effective range of recurrence instances that is specified by the property. The effective range is from the recurrence identifier specified by the property. If this parameter is not specified on an allowed property, then the default range is the single instance specified by the recurrence identifier value of the property. The parameter value can only be "THISANDFUTURE" to indicate a range defined by the recurrence identifier and all subsequent instances. The value "THISANDPRIOR" is deprecated by this revision of iCalendar and MUST NOT be generated by applications. """ THISANDFUTURE = "THISANDFUTURE" THISANDPRIOR = "THISANDPRIOR" # deprecated class RELATED(StrEnum): """Enum for RELATED from :rfc:`5545`. Values: ``START``, ``END`` Purpose: To specify the relationship of the alarm trigger with respect to the start or end of the calendar component. Description: This parameter can be specified on properties that specify an alarm trigger with a "DURATION" value type. The parameter specifies whether the alarm will trigger relative to the start or end of the calendar component. The parameter value START will set the alarm to trigger off the start of the calendar component; the parameter value END will set the alarm to trigger off the end of the calendar component. If the parameter is not specified on an allowable property, then the default is START. """ START = "START" END = "END" class ROLE(StrEnum): """Enum for ROLE from :rfc:`5545`. Values: ``CHAIR``, ``REQ_PARTICIPANT``, ``OPT_PARTICIPANT``, ``NON_PARTICIPANT`` Purpose: To specify the participation role for the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter specifies the participation role for the calendar user specified by the property in the group schedule calendar component. If not specified on a property that allows this parameter, the default value is REQ-PARTICIPANT. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the REQ-PARTICIPANT value. """ CHAIR = "CHAIR" REQ_PARTICIPANT = "REQ-PARTICIPANT" OPT_PARTICIPANT = "OPT-PARTICIPANT" NON_PARTICIPANT = "NON-PARTICIPANT" class VALUE(StrEnum): """VALUE datatypes as defined in :rfc:`5545`. Attributes: ``BINARY``, ``BOOLEAN``, ``CAL_ADDRESS``, ``DATE``, ``DATE_TIME``, ``DURATION``, ``FLOAT``, ``INTEGER``, ``PERIOD``, ``RECUR``, ``TEXT``, ``TIME``, ``URI``, ``UTC_OFFSET`` Description: This parameter specifies the value type and format of the property value. The property values MUST be of a single value type. For example, a "RDATE" property cannot have a combination of DATE-TIME and TIME value types. If the property's value is the default value type, then this parameter need not be specified. However, if the property's default value type is overridden by some other allowable value type, then this parameter MUST be specified. Applications MUST preserve the value data for x-name and iana- token values that they don't recognize without attempting to interpret or parse the value data. """ BINARY = "BINARY" BOOLEAN = "BOOLEAN" CAL_ADDRESS = "CAL-ADDRESS" DATE = "DATE" DATE_TIME = "DATE-TIME" DURATION = "DURATION" FLOAT = "FLOAT" INTEGER = "INTEGER" PERIOD = "PERIOD" RECUR = "RECUR" TEXT = "TEXT" TIME = "TIME" URI = "URI" UTC_OFFSET = "UTC-OFFSET" class BUSYTYPE(StrEnum): """Enum for BUSYTYPE from :rfc:`7953`. Values: ``BUSY``, ``BUSY_UNAVAILABLE``, ``BUSY_TENTATIVE`` Description: This property is used to specify the default busy time type. The values correspond to those used by the :class:`FBTYPE` parameter used on a "FREEBUSY" property, with the exception that the "FREE" value is not used in this property. If not specified on a component that allows this property, the default is "BUSY- UNAVAILABLE". Example: The following is an example of this property: .. code-block:: ics BUSYTYPE:BUSY """ BUSY = "BUSY" BUSY_UNAVAILABLE = "BUSY-UNAVAILABLE" BUSY_TENTATIVE = "BUSY-TENTATIVE" class CLASS(StrEnum): """Enum for CLASS from :rfc:`5545`. Values: ``PUBLIC``, ``PRIVATE``, ``CONFIDENTIAL`` Description: An access classification is only one component of the general security system within a calendar application. It provides a method of capturing the scope of the access the calendar owner intends for information within an individual calendar entry. The access classification of an individual iCalendar component is useful when measured along with the other security components of a calendar system (e.g., calendar user authentication, authorization, access rights, access role, etc.). Hence, the semantics of the individual access classifications cannot be completely defined by this memo alone. Additionally, due to the "blind" nature of most exchange processes using this memo, these access classifications cannot serve as an enforcement statement for a system receiving an iCalendar object. Rather, they provide a method for capturing the intention of the calendar owner for the access to the calendar component. If not specified in a component that allows this property, the default value is PUBLIC. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the PRIVATE value. """ PUBLIC = "PUBLIC" PRIVATE = "PRIVATE" CONFIDENTIAL = "CONFIDENTIAL" class TRANSP(StrEnum): """Enum for TRANSP from :rfc:`5545`. Values: ``OPAQUE``, ``TRANSPARENT`` Description: Time Transparency is the characteristic of an event that determines whether it appears to consume time on a calendar. Events that consume actual time for the individual or resource associated with the calendar SHOULD be recorded as OPAQUE, allowing them to be detected by free/busy time searches. Other events, which do not take up the individual's (or resource's) time SHOULD be recorded as TRANSPARENT, making them invisible to free/ busy time searches. """ OPAQUE = "OPAQUE" TRANSPARENT = "TRANSPARENT" __all__ = [ "BUSYTYPE", "CLASS", "CUTYPE", "FBTYPE", "PARTSTAT", "RANGE", "RELATED", "RELTYPE", "ROLE", "STATUS", "TRANSP", "VALUE", ] collective-icalendar-138c845/src/icalendar/error.py000066400000000000000000000257601524134033600222730ustar00rootroot00000000000000"""Errors thrown by icalendar.""" from __future__ import annotations import contextlib from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Generator class InvalidCalendar(ValueError): """The calendar given is not valid. This calendar does not conform with RFC 5545 or breaks other RFCs. """ class ICalParsingError(InvalidCalendar): """Could not parse an iCalendar.""" def __init__( self, message: str, line: str | None = None, line_number: int | None = None, value: object = None, ) -> None: self.message = message self.line = line self.line_number = line_number self.value = value full_message = message if value is not None: full_message += f": {value!r}" if line_number is not None and line is not None: full_message += f" (line {line_number}: {line!r})" elif line_number is not None: full_message += f" (line {line_number})" elif line is not None: full_message += f" ({line!r})" super().__init__(full_message) class BrokenCalendarProperty(InvalidCalendar): """A property could not be parsed and its value is broken. This error is raised when accessing attributes on a :class:`~icalendar.prop.vBroken` property that would normally be present on the expected type. The original parse error is chained as ``__cause__``. """ class IncompleteComponent(ValueError): """The component is missing attributes. The attributes are not required, otherwise this would be an InvalidCalendar. But in order to perform calculations, this attribute is required. This error is not raised in the UPPERCASE properties like .DTSTART, only in the lowercase computations like .start. """ class IncompleteAlarmInformation(ValueError): """The alarms cannot be calculated yet because information is missing.""" class LocalTimezoneMissing(IncompleteAlarmInformation): """We are missing the local timezone to compute the value. Use Alarms.set_local_timezone(). """ class ComponentEndMissing(IncompleteAlarmInformation): """We are missing the end of a component that the alarm is for. Use Alarms.set_end(). """ class ComponentStartMissing(IncompleteAlarmInformation): """We are missing the start of a component that the alarm is for. Use Alarms.set_start(). """ class FeatureWillBeRemovedInFutureVersion(DeprecationWarning): """This feature will be removed in a future version.""" class GloballyUniqueTZIDGuessed(UserWarning): """A globally unique TZID was resolved by stripping the vendor prefix. Per :rfc:`5545#section-3.2.19`, the trailing component is convention only and not guaranteed to match a known Olson identifier. Suppress this warning if the resolved timezone is correct for your data. """ def _repr_index(index: str | int) -> str: """Create a JSON compatible representation for the index. Parameters: index: It is either a dict key (string) or a list position (integer). Returns: The index as a quoted string if it's a string, else the string representation if it's an integer. """ if isinstance(index, str): return f'"{index}"' return str(index) class JCalParsingError(InvalidCalendar): """Could not parse a part of the JCal.""" _default_value = object() def __init__( self, message: str, parser: str | type = "", path: list[str | int] | None | str | int = None, value: object = _default_value, ) -> None: """Create a new JCalParsingError. Parameters: message: A description of the error that occurred while parsing. parser: The parser class or its name where the error occurred. path: The location in the jCal structure where the error occurred. value: The value which caused the error, if available. """ self.path = self._get_path(path) if not isinstance(parser, str): parser = parser.__name__ self.parser = parser self.message = message self.value = value full_message = message repr_path = "" if self.path: repr_path = "".join([f"[{_repr_index(index)}]" for index in self.path]) full_message = f"{repr_path}: {full_message}" repr_path += " " if parser: full_message = f"{repr_path}in {parser}: {message}" if value is not self._default_value: full_message += f" Got value: {value!r}" super().__init__(full_message) @classmethod @contextlib.contextmanager def reraise_with_path_added( cls, *path_components: int | str, ) -> Generator[None, None, None]: """Automatically re-raise the exception with path components added. Raises: ~error.JCalParsingError: If there was an exception in the context. """ try: yield except JCalParsingError as e: raise cls( path=list(path_components) + e.path, parser=e.parser, message=e.message, value=e.value, ).with_traceback(e.__traceback__) from e @staticmethod def _get_path(path: list[str | int] | None | str | int) -> list[str | int]: """Return the path as a list.""" if path is None: path = [] elif not isinstance(path, list): path = [path] return path @classmethod def validate_property( cls, jcal_property: list[object], parser: str | type, path: list[str | int] | None | str | int = None, ) -> None: """Validate a jCal property. Parameters: jcal_property: A list with at least four items (name, parameters, value type, and value) which is the jCal property to be validated. parser: The parser class or its name where the error occurred. path: The location in the jCal structure where the error occurred. Raises: ~error.JCalParsingError: if the property is not valid. """ path = cls._get_path(path) if not isinstance(jcal_property, list) or len(jcal_property) < 4: raise JCalParsingError( "The property must be a list with at least 4 items.", parser, path, value=jcal_property, ) if not isinstance(jcal_property[0], str): raise JCalParsingError( "The name must be a string.", parser, path + [0], value=jcal_property[0] ) if not isinstance(jcal_property[1], dict): raise JCalParsingError( "The parameters must be a mapping.", parser, path + [1], value=jcal_property[1], ) if not isinstance(jcal_property[2], str): raise JCalParsingError( "The VALUE parameter must be a string.", parser, path + [2], value=jcal_property[2], ) cls.validate_jcal_token(jcal_property[0], "property name", parser, path + [0]) _type_names = { str: "a string", int: "an integer", float: "a float", bool: "a boolean", } @classmethod def validate_value_type( cls, jcal: object, expected_type: type[str | int | float | bool] | tuple[type[str | int | float | bool], ...], parser: str | type = "", path: list[str | int] | None | str | int = None, ) -> None: """Validate the type of a jCal value.""" if not isinstance(jcal, expected_type): type_name = ( cls._type_names[expected_type] if isinstance(expected_type, type) else " or ".join(cls._type_names[t] for t in expected_type) ) raise cls( f"The value must be {type_name}.", parser=parser, value=jcal, path=path, ) @classmethod def validate_list_type( cls, jcal: object, expected_type: type[str | int | float | bool], parser: str | type = "", path: list[str | int] | None | str | int = None, ) -> None: """Validate the type of each item in a jCal list.""" path = cls._get_path(path) if not isinstance(jcal, list): raise cls( "The value must be a list.", parser=parser, value=jcal, path=path, ) for index, item in enumerate(jcal): if not isinstance(item, expected_type): type_name = cls._type_names[expected_type] raise cls( f"Each item in the list must be {type_name}.", parser=parser, value=item, path=path + [index], ) @classmethod def validate_jcal_token( cls, name: str, kind: str, parser: str | type = "", path: list[str | int] | None | str | int = None, ) -> None: r"""Validate a jCal ``name`` as a lowercase iCalendar token. jCal keeps a property name, parameter name, or ``RRULE`` part name verbatim and re-emits it into the content line on serialization, so a ``:``, ``;``, or lone carriage return in the name could inject parameters or a new content line. A valid name matches the iCalendar token pattern ``[\w.-]+`` and, per :rfc:`7265`, must be lowercase. Parameters: name: The jCal name to validate. kind: Names the token in the error message, for example, ``"property name"``. parser: The parser or component to which the name belongs. path: The jCal path to ``name``, used to locate it in the error. Raises: ~error.JCalParsingError: If ``name`` is not a valid lowercase iCalendar token. See also: :meth:`~icalendar.parser.string.validate_token` """ from icalendar.parser.string import validate_token try: validate_token(name) except ValueError: raise cls( rf"The {kind} must be a valid iCalendar token, matching the " rf"regular expression pattern `[\w.-]+`.", parser, path, value=name, ) from None if name != name.lower(): raise cls(f"The {kind} must be lowercase.", parser, path, value=name) __all__ = [ "BrokenCalendarProperty", "ComponentEndMissing", "ComponentStartMissing", "FeatureWillBeRemovedInFutureVersion", "ICalParsingError", "IncompleteAlarmInformation", "IncompleteComponent", "InvalidCalendar", "JCalParsingError", "LocalTimezoneMissing", ] collective-icalendar-138c845/src/icalendar/fuzzing/000077500000000000000000000000001524134033600222525ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/build.sh000077500000000000000000000015771524134033600237220ustar00rootroot00000000000000#!/bin/bash -eu # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ cd "$SRC"/icalendar pip3 install . # Build fuzzers in $OUT for fuzzer in $(find src/icalendar/fuzzing -name '*_fuzzer.py');do compile_python_fuzzer "$fuzzer" done zip -q $OUT/ical_fuzzer_seed_corpus.zip $SRC/corpus/* collective-icalendar-138c845/src/icalendar/fuzzing/corpus/000077500000000000000000000000001524134033600235655ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/corpus/Index_Error.ics000066400000000000000000000000551524134033600265050ustar00rootroot00000000000000!ßbEGÉN: END:‪!ßbEGÉN: END:âŕą�€ŞVTó �’IOEEcollective-icalendar-138c845/src/icalendar/fuzzing/corpus/Type_Error.ics000066400000000000000000000000571524134033600263610ustar00rootroot00000000000000(EBEGIN: U: TJDATßEBEGIN:VEV: RDATE:0¶ ¶ ¶ ENd:collective-icalendar-138c845/src/icalendar/fuzzing/corpus/america_new_york.ics000066400000000000000000000025361524134033600276110ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:custom_America/New_York LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/calendar_with_unicode.ics000066400000000000000000000003061524134033600305760ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//PlönĂ«.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:äöü ÄÖÜ € X-WR-CALDESC:test non ascii: äöü ÄÖÜ € X-WR-RELCALID:12345 END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_escaped_character1.ics000066400000000000000000000000741524134033600322230ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\, 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_escaped_character2.ics000066400000000000000000000000741524134033600322240ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\\ 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_escaped_character3.ics000066400000000000000000000000741524134033600322250ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\; 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_escaped_character4.ics000066400000000000000000000000741524134033600322260ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\: 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_escaped_characters.ics000066400000000000000000000001501524134033600323200ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\: END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_recurrence.ics000066400000000000000000000003101524134033600306500ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:19960401T010000 DTEND:19960401T020000 RRULE:FREQ=DAILY;COUNT=100 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z SUMMARY:A recurring event with exdates END:VEVENT event_with_recurrence_exdates_on_different_lines.ics000066400000000000000000000007101524134033600362260ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/corpusBEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120327T100000 DTEND;TZID=Europe/Vienna:20120327T180000 RRULE:FREQ=WEEKLY;UNTIL=20120703T080000Z;BYDAY=TU EXDATE;TZID=Europe/Vienna:20120529T100000 EXDATE;TZID=Europe/Vienna:20120403T100000 EXDATE;TZID=Europe/Vienna:20120410T100000 EXDATE;TZID=Europe/Vienna:20120501T100000 EXDATE;TZID=Europe/Vienna:20120417T100000 DTSTAMP:20130716T120638Z SUMMARY:A Recurring event with multiple exdates, one per line. END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_rsvp.ics000066400000000000000000000001111524134033600275040ustar00rootroot00000000000000BEGIN:VEVENT ATTENDEE;RSVP=TRUE:mailto:someone@example.com END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_unicode_fields.ics000066400000000000000000000003761524134033600315030ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20101010T100000Z DTEND:20101010T120000Z CREATED:20101010T100000Z UID:123456 SUMMARY:Non-ASCII Test: ÄÖÜ äöü € DESCRIPTION:icalendar should be able to handle non-ascii: €äüöÄÜÖ. LOCATION:TribstrĂĽl END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/event_with_unicode_organizer.ics000066400000000000000000000001261524134033600322260ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN="Джон ДоŃ�":mailto:john.doe@example.org END:VEVENT issue_100_transformed_doctests_into_unittests.ics000066400000000000000000000000651524134033600354060ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/corpusBEGIN:VEVENT SUMMARY;LANGUAGE=ru:te END:VEVENT issue_101_icalendar_chokes_on_umlauts_in_organizer.ics000066400000000000000000000005471524134033600362770ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/corpusBEGIN:VEVENT SUMMARY:wichtiger termin 1 DTSTART:20130416T100000Z DTEND:20130416T110000Z DTSTAMP:20130416T092616Z UID:20130416112341.10064jz0k4j7uem8@acmenet.de CLASS:PUBLIC CREATED:20130416T092341Z LAST-MODIFIED:20130416T092341Z LOCATION:im bĂĽro ORGANIZER;CN="acme, ädmin":mailto:adm-acme@mydomain.de STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_104_mark_events_broken.ics000066400000000000000000000002441524134033600317370ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE X END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_112_missing_tzinfo_on_exdate.ics000066400000000000000000000012361524134033600331520ustar00rootroot00000000000000BEGIN:VEVENT DTSTART;TZID=America/New_York:20130907T120000 DTEND;TZID=America/New_York:20130907T170000 RRULE:FREQ=WEEKLY;BYDAY=FR,SA;UNTIL=20131025T035959Z EXDATE;TZID=America/New_York:20131012T120000 EXDATE;TZID=America/New_York:20131011T120000 DTSTAMP:20131021T025552Z UID:ak30b02u7858q1oo6ji9dm4mgg@google.com CREATED:20130903T181453Z DESCRIPTION:The Fieldhouse and Hard Rock Cafe are working with PhillyRising to provide live entertainment on Friday and Saturday afternoons throughout the Summer. LAST-MODIFIED:20131015T210927Z LOCATION:12th and Market Streets (weather permitting) SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Market East Live! TRANSP:OPAQUE END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_156_RDATE_with_PERIOD.ics000066400000000000000000000002521524134033600310230ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_156_RDATE_with_PERIOD_list.ics000066400000000000000000000003061524134033600320560ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H 30M END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_157_removes_trailing_semicolon.ics000066400000000000000000000001341524134033600335100ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150325T101010 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; END:VEVENT issue_184_broken_representation_of_period.ics000066400000000000000000000002031524134033600344350ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/fuzzing/corpusBEGIN:VEVENT DTSTART:20150219T133000 DTSTAMP:20150219T133000 UID:1234567 RDATE;VALUE=PERIOD:20150219T133000/PT10H END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_464_invalid_rdate.ics000066400000000000000000000003011524134033600306710ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_53_description_parsed_properly.ics000066400000000000000000000013431524134033600336220ustar00rootroot00000000000000BEGIN:VEVENT DTSTAMP:20120605T003759Z DTSTART;TZID=America/New_York:20120712T183000 DTEND;TZID=America/New_York:20120712T213000 STATUS:CONFIRMED SUMMARY:DevOps DC Meetup DESCRIPTION:DevOpsDC\nThursday\, July 12 at 6:30 PM\n\nThis will be a joi nt meetup / hack night with the DC jQuery Users Group. The idea behind the hack night: Small teams consisting of at least 1 member...\n\nDeta ils: http://www.meetup.com/DevOpsDC/events/47635522/ CLASS:PUBLIC CREATED:20120111T120339Z GEO:38.90;-77.01 LOCATION:Fathom Creative\, Inc. (1333 14th Street Northwest\, Washington D.C.\, DC 20005) URL:http://www.meetup.com/DevOpsDC/events/47635522/ LAST-MODIFIED:20120522T174406Z UID:event_qtkfrcyqkbnb@meetup.com END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_64_event_with_ascii_summary.ics000066400000000000000000000000521524134033600331040ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:abcdef END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_64_event_with_non_ascii_summary.ics000066400000000000000000000000521524134033600337560ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:åäö END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_70_rrule_causes_attribute_error.ics000066400000000000000000000004271524134033600337760ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20081114T072804Z UID:D449CA84-00A3-4E55-83E1-34B58268853B DTEND:20070220T180000 RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20070619T225959 TRANSP:OPAQUE SUMMARY:Esb mellon phone conf DTSTART:20070220T170000 DTSTAMP:20070221T095412Z SEQUENCE:0 END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/issue_82_expected_output.ics000066400000000000000000000001331524134033600312240ustar00rootroot00000000000000BEGIN:VEVENT ATTACH;ENCODING=BASE64;FMTTYPE=text/plain;VALUE=BINARY:dGV4dA== END:VEVENT collective-icalendar-138c845/src/icalendar/fuzzing/corpus/pacific_fiji.ics000066400000000000000000000023061524134033600266650ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//tzurl.org//NONSGML Olson 2014g//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/time.ics000066400000000000000000000000731524134033600252230ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOMETIME;VALUE=TIME:172010 END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/timezone_rdate.ics000066400000000000000000000021251524134033600272760ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:posix/Europe/Vaduz BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+002946 TZOFFSETTO:+0100 DTSTART:19011213T211538 RDATE;VALUE=DATE-TIME:19011213T211538 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19410505T010000 RDATE;VALUE=DATE-TIME:19410505T010000 RDATE;VALUE=DATE-TIME:19420504T010000 END:DAYLIGHT BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19810927T030000 RRULE:FREQ=YEARLY;COUNT=15;BYDAY=-1SU;BYMONTH=9 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19411006T020000 RDATE;VALUE=DATE-TIME:19411006T020000 RDATE;VALUE=DATE-TIME:19421005T020000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:123 DTSTART;TZID=posix/Europe/Vaduz:20120213T100000 SUMMARY=testevent END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/timezone_same_start.ics000066400000000000000000000012551524134033600303440ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 METHOD:REQUEST BEGIN:VTIMEZONE TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY;LANGUAGE=en-US:Test 4 DTSTART;TZID="Pacific Standard Time":20170224T120000 DTEND;TZID="Pacific Standard Time":20170224T123000 DTSTAMP:20170224T180431Z UID:040000008200E00074C5B7101A82E0080000000090E19664858ED20100000000000000 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/corpus/timezone_same_start_and_offset.ics000066400000000000000000000007331524134033600325340ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 BEGIN:VTIMEZONE TZID:Tokyo Standard Time BEGIN:STANDARD DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="Tokyo Standard Time":20170224T120000 DTEND;TZID="Tokyo Standard Time":20170224T123000 UID:blafoobar SUMMARY:this is an event END:VEVENT END:VCALENDARD collective-icalendar-138c845/src/icalendar/fuzzing/corpus/timezoned.ics000066400000000000000000000014541524134033600262670ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Plone.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:test create calendar X-WR-CALDESC:icalendar test X-WR-RELCALID:12345 X-WR-TIMEZONE:Europe/Vienna BEGIN:VTIMEZONE TZID:Europe/Vienna X-LIC-LOCATION:Europe/Vienna BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120213T100000 DTEND;TZID=Europe/Vienna:20120217T180000 DTSTAMP:20101010T091010Z CREATED:20101010T091010Z UID:123456 SUMMARY:artsprint 2012 DESCRIPTION:sprinting at the artsprint LOCATION:aka bild, wien END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/fuzzing/ical_fuzzer.py000077500000000000000000000033101524134033600251410ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ import base64 import contextlib import sys import atheris with atheris.instrument_imports(): import icalendar.cal.calendar from icalendar.tests.fuzzed import fuzz_v1_calendar @atheris.instrument_func def TestOneInput(data): fdp = atheris.FuzzedDataProvider(data) multiple = fdp.ConsumeBool() should_walk = fdp.ConsumeBool() calendar_string = fdp.ConsumeString(fdp.remaining_bytes()) print("--- start calendar ---") with contextlib.suppress(UnicodeEncodeError): # print the ICS file for the test case extraction # see https://stackoverflow.com/a/27367173/1320237 print( base64.b64encode(calendar_string.encode("UTF-8", "surrogateescape")).decode( "ASCII" ) ) fuzz_v1_calendar( icalendar.cal.calendar.Calendar.from_ical, calendar_string, multiple, should_walk, ) print("--- end calendar ---") def main(): atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() if __name__ == "__main__": main() collective-icalendar-138c845/src/icalendar/param.py000066400000000000000000000677201524134033600222440ustar00rootroot00000000000000"""Parameter access for icalendar. Related: - :rfc:`5545`, Section 3.2. Property Parameters - :rfc:`7986`, Section 6. Property Parameters - :rfc:`9253` - https://github.com/collective/icalendar/issues/798 """ from __future__ import annotations import functools from typing import TYPE_CHECKING, TypeVar from icalendar import enums if TYPE_CHECKING: from collections.abc import Callable from datetime import timedelta from enum import Enum if TYPE_CHECKING: from icalendar.prop import VPROPERTY def _default_return_none() -> str | None: """Return None by default.""" return None def _default_return_string() -> str: """Return None by default.""" return "" T = TypeVar("T") def string_parameter( name: str, doc: str, default: Callable = _default_return_none, convert: Callable[[str], T] | None = None, convert_to: Callable[[T], str] | None = None, ) -> property: """Create a property for a string parameter with optional conversion. This helper function is used to define properties that read from and write to ``self.params`` while optionally converting values between their stored string representation and a more convenient Python type. Parameters: name: Name of the parameter in the params dictionary. doc: Documentation for the property. default: Function that returns a default value if the parameter is not found. convert: Function that converts the stored string value to the desired type. convert_to: Function to convert a value back to a string for storage. Returns: A property object with a getter, setter, and deleter for the parameter. Example: Define a parameter that is stored as a string but used as an integer: >>> from icalendar.param import string_parameter >>> class Dummy: ... def __init__(self): ... self.params = {} ... >>> Dummy.priority = string_parameter( ... "PRIORITY", ... "Priority of the component", ... default=lambda: 0, ... convert=int, ... convert_to=str, ... ) >>> obj = Dummy() Accessing the property converts the stored string value to the type specified by the ``convert`` parameter, in this case, an ``int``. >>> obj.priority = "5" >>> obj.priority 5 Setting the property stores the new value. >>> obj.priority = 10 >>> obj.priority 10 """ if convert_to is None: convert_to = convert @functools.wraps(default) def fget(self: VPROPERTY) -> str | None: value = self.params.get(name) if value is None: return default() return convert(value) if convert else value def fset(self: VPROPERTY, value: str | None): if value is None: fdel(self) else: self.params[name] = convert_to(value) if convert_to else value def fdel(self: VPROPERTY): self.params.pop(name, None) return property(fget, fset, fdel, doc=doc) ALTREP = string_parameter( "ALTREP", """ALTREP - Specify an alternate text representation for the property value. Description: This parameter specifies a URI that points to an alternate representation for a textual property value. A property specifying this parameter MUST also include a value that reflects the default representation of the text value. The URI parameter value MUST be specified in a quoted-string. .. note:: While there is no restriction imposed on the URI schemes allowed for this parameter, Content Identifier (CID) :rfc:`2392`, HTTP :rfc:`2616`, and HTTPS :rfc:`2818` are the URI schemes most commonly used by current implementations. """, ) CN = string_parameter( "CN", """Specify the common name to be associated with the calendar user specified. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter specifies the common name to be associated with the calendar user specified by the property. The parameter value is text. The parameter value can be used for display text to be associated with the calendar address specified by the property. """, default=_default_return_string, ) def _default_return_individual() -> enums.CUTYPE | str: """Default value.""" return enums.CUTYPE.INDIVIDUAL def _convert_enum(enum: type[Enum]) -> Callable[[str], Enum]: def convert(value: str) -> str: """Convert if possible.""" try: return enum(value.upper()) except ValueError: return value return convert CUTYPE = string_parameter( "CUTYPE", """Identify the type of calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the type of calendar user specified by the property. If not specified on a property that allows this parameter, the default is INDIVIDUAL. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the UNKNOWN value. """, default=_default_return_individual, convert=_convert_enum(enums.CUTYPE), ) def quoted_list_parameter(name: str, doc: str) -> property: """Return a parameter that contains a quoted list.""" def fget(self: VPROPERTY) -> tuple[str]: value = self.params.get(name) if value is None: return () if isinstance(value, str): return tuple(value.split(",")) return value def fset(self: VPROPERTY, value: str | tuple[str]): if value == (): fdel(self) else: self.params[name] = (value,) if isinstance(value, str) else value def fdel(self: VPROPERTY): self.params.pop(name, None) return property(fget, fset, fdel, doc=doc) DELEGATED_FROM = quoted_list_parameter( "DELEGATED-FROM", """Specify the calendar users that have delegated their participation to the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. This parameter specifies those calendar users that have delegated their participation in a group-scheduled event or to-do to the calendar user specified by the property. The individual calendar address parameter values MUST each be specified in a quoted-string. """, # noqa: E501 ) DELEGATED_TO = quoted_list_parameter( "DELEGATED-TO", """Specify the calendar users to whom the calendar user specified by the property has delegated participation. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. This parameter specifies those calendar users whom have been delegated participation in a group-scheduled event or to-do by the calendar user specified by the property. The individual calendar address parameter values MUST each be specified in a quoted-string. """, # noqa: E501 ) DIR = string_parameter( "DIR", """Specify reference to a directory entry associated with the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter specifies a reference to the directory entry associated with the calendar user specified by the property. The parameter value is a URI. The URI parameter value MUST be specified in a quoted-string. .. note:: While there is no restriction imposed on the URI schemes allowed for this parameter, CID :rfc:`2392`, DATA :rfc:`2397`, FILE :rfc:`1738`, FTP :rfc:`1738`, HTTP :rfc:`2616`, HTTPS :rfc:`2818`, LDAP :rfc:`4516`, and MID :rfc:`2392` are the URI schemes most commonly used by current implementations. """, # noqa: E501 ) def _default_return_busy() -> enums.FBTYPE | str: """Default value.""" return enums.FBTYPE.BUSY FBTYPE = string_parameter( "FBTYPE", """Specify the free or busy time type. Description: This parameter specifies the free or busy time type. The value FREE indicates that the time interval is free for scheduling. The value BUSY indicates that the time interval is busy because one or more events have been scheduled for that interval. The value BUSY-UNAVAILABLE indicates that the time interval is busy and that the interval can not be scheduled. The value BUSY-TENTATIVE indicates that the time interval is busy because one or more events have been tentatively scheduled for that interval. If not specified on a property that allows this parameter, the default is BUSY. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the BUSY value. """, default=_default_return_busy, convert=_convert_enum(enums.FBTYPE), ) LANGUAGE = string_parameter( "LANGUAGE", """Specify the language for text values in a property or property parameter. Description: This parameter identifies the language of the text in the property value and of all property parameter values of the property. The value of the "LANGUAGE" property parameter is that defined in :rfc:`5646`. For transport in a MIME entity, the Content-Language header field can be used to set the default language for the entire body part. Otherwise, no default language is assumed. """, ) MEMBER = quoted_list_parameter( "MEMBER", """Specify the group or list membership of the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the groups or list membership for the calendar user specified by the property. The parameter value is either a single calendar address in a quoted-string or a COMMA-separated list of calendar addresses, each in a quoted-string. The individual calendar address parameter values MUST each be specified in a quoted-string. """, # noqa: E501 ) def _default_return_needs_action() -> enums.PARTSTAT | str: """Default value.""" return enums.PARTSTAT.NEEDS_ACTION PARTSTAT = string_parameter( "PARTSTAT", """Specify the participation status for the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the participation status for the calendar user specified by the property value. The parameter values differ depending on whether they are associated with a group-scheduled "VEVENT", "VTODO", or "VJOURNAL". The values MUST match one of the values allowed for the given calendar component. If not specified on a property that allows this parameter, the default value is NEEDS-ACTION. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the NEEDS-ACTION value. """, default=_default_return_needs_action, convert=_convert_enum(enums.PARTSTAT), ) def _default_range_none() -> enums.RANGE | str | None: return None RANGE = string_parameter( "RANGE", """Specify the effective range of recurrence instances from the instance specified by the recurrence identifier specified by the property. Description: This parameter can be specified on a property that specifies a recurrence identifier. The parameter specifies the effective range of recurrence instances that is specified by the property. The effective range is from the recurrence identifier specified by the property. If this parameter is not specified on an allowed property, then the default range is the single instance specified by the recurrence identifier value of the property. The parameter value can only be "THISANDFUTURE" to indicate a range defined by the recurrence identifier and all subsequent instances. The value "THISANDPRIOR" is deprecated by this revision of iCalendar and MUST NOT be generated by applications. """, # noqa: E501 default=_default_range_none, convert=_convert_enum(enums.RANGE), ) def _default_related() -> enums.RELATED | str: return enums.RELATED.START RELATED = string_parameter( "RELATED", """Specify the relationship of the alarm trigger with respect to the start or end of the calendar component. Description: This parameter can be specified on properties that specify an alarm trigger with a "DURATION" value type. The parameter specifies whether the alarm will trigger relative to the start or end of the calendar component. The parameter value START will set the alarm to trigger off the start of the calendar component; the parameter value END will set the alarm to trigger off the end of the calendar component. If the parameter is not specified on an allowable property, then the default is START. """, # noqa: E501 default=_default_related, convert=_convert_enum(enums.RELATED), ) def _default_req_participant() -> enums.ROLE | str: return enums.ROLE.REQ_PARTICIPANT ROLE = string_parameter( "ROLE", """Specify the participation role for the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter specifies the participation role for the calendar user specified by the property in the group schedule calendar component. If not specified on a property that allows this parameter, the default value is REQ-PARTICIPANT. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the REQ-PARTICIPANT value. """, default=_default_req_participant, convert=_convert_enum(enums.ROLE), ) def boolean_parameter(name: str, default: bool, doc: str) -> property: def _default() -> bool: return default return string_parameter( name, doc, default=_default, convert=lambda x: x.upper() == "TRUE", convert_to=lambda x: "TRUE" if x else "FALSE", ) RSVP = boolean_parameter( "RSVP", False, """Specify whether there is an expectation of a favor of anreply from the calendar user specified by the property value. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter identifies the expectation of a reply from the calendar user specified by the property value. This parameter is used by the "Organizer" to request a participation status reply from an "Attendee" of a group-scheduled event or to-do. If not specified on a property that allows this parameter, the default value is ``False``. """, # noqa: E501 ) SENT_BY = string_parameter( "SENT-BY", """Specify the calendar user that is acting on behalf of the calendar user specified by the property. Description: This parameter can be specified on properties with a CAL-ADDRESS value type. The parameter specifies the calendar user that is acting on behalf of the calendar user specified by the property. The parameter value MUST be a mailto URI as defined in :rfc:`2368`. The individual calendar address parameter values MUST each be specified in a quoted-string. """, # noqa: E501 ) TZID = string_parameter( "TZID", """Specify the identifier for the time zone definition for a time component in the property value. Description: This parameter MUST be specified on the "DTSTART", "DTEND", "DUE", "EXDATE", and "RDATE" properties when either a DATE-TIME or TIME value type is specified and when the value is neither a UTC or a "floating" time. Refer to the DATE-TIME or TIME value type definition for a description of UTC and "floating time" formats. This property parameter specifies a text value that uniquely identifies the "VTIMEZONE" calendar component to be used when evaluating the time portion of the property. The value of the "TZID" property parameter will be equal to the value of the "TZID" property for the matching time zone definition. An individual "VTIMEZONE" calendar component MUST be specified for each unique "TZID" parameter value specified in the iCalendar object. The parameter MUST be specified on properties with a DATE-TIME value if the DATE-TIME is not either a UTC or a "floating" time. Failure to include and follow VTIMEZONE definitions in iCalendar objects may lead to inconsistent understanding of the local time at any given location. The presence of the SOLIDUS character as a prefix, indicates that this "TZID" represents a unique ID in a globally defined time zone registry (when such registry is defined). .. note:: This document does not define a naming convention for time zone identifiers. Implementers may want to use the naming conventions defined in existing time zone specifications such as the public-domain TZ database (TZDB). The specification of globally unique time zone identifiers is not addressed by this document and is left for future study. """, # noqa: E501 ) def _default_return_parent() -> enums.RELTYPE: return enums.RELTYPE.PARENT RELTYPE = string_parameter( "RELTYPE", """Specify the type of hierarchical relationship associated with a component. Conformance: :rfc:`5545` introduces the RELTYPE property parameter. :rfc:`9253` adds new values. Description: This parameter can be specified on a property that references another related calendar. The parameter specifies the hierarchical relationship type of the calendar component referenced by the property. The parameter value can be PARENT, to indicate that the referenced calendar component is a superior of calendar component; CHILD to indicate that the referenced calendar component is a subordinate of the calendar component; or SIBLING to indicate that the referenced calendar component is a peer of the calendar component. If this parameter is not specified on an allowable property, the default relationship type is PARENT. Applications MUST treat x-name and iana-token values they don't recognize the same way as they would the PARENT value. """, default=_default_return_parent, convert=_convert_enum(enums.RELTYPE), ) def _get_value(self: VPROPERTY) -> str: """The VALUE parameter or the default. Purpose: VALUE explicitly specify the value type format for a property value. Description: This parameter specifies the value type and format of the property value. The property values MUST be of a single value type. For example, a "RDATE" property cannot have a combination of DATE-TIME and TIME value types. If the property's value is the default value type, then this parameter need not be specified. However, if the property's default value type is overridden by some other allowable value type, then this parameter MUST be specified. Applications MUST preserve the value data for ``x-name`` and ``iana-token`` values that they don't recognize without attempting to interpret or parse the value data. Returns: The VALUE parameter or the default. Examples: The VALUE defaults to the name of the property. Note that it is case-insensitive but always uppercase. .. code-block:: pycon >>> from icalendar import vBoolean >>> b = vBoolean(True) >>> b.VALUE 'BOOLEAN' Setting the VALUE parameter of a typed property usually does not make sense. For convenience, using this property, the value will be converted to an uppercase string. If you have some custom property, you might use it like this: .. code-block:: pycon >>> from icalendar import vUnknown, Event >>> v = vUnknown("Some property text.") >>> v.VALUE = "x-type" # lower case >>> v.VALUE 'X-TYPE' >>> event = Event() >>> event.add("x-prop", v) >>> print(event.to_ical()) BEGIN:VEVENT X-PROP;VALUE=X-TYPE:Some property text. END:VEVENT """ value = self.params.value if value is None: _get_default_value = getattr(self, "_get_value", None) if _get_default_value is not None: value = _get_default_value() return self.default_value if value is None else value def _set_value(self: VPROPERTY, value: str | None): """Set the VALUE parameter.""" self.params.value = value def _del_value(self: VPROPERTY): """Delete the VALUE parameter.""" del self.params.value VALUE = property(_get_value, _set_value, _del_value) LABEL = string_parameter( "LABEL", """LABEL provides a human-readable label. Conformance: This property parameter is specified in :rfc:`7986`, iCalendar Property Extensions. :rfc:`9253` makes use of this for the LINK property. This parameter maps to the "title" attribute defined in Section 3.4.1 of :rfc:`8288`. LABEL is used to label the destination of a link such that it can be used as a human-readable identifier (e.g., a menu entry) in the language indicated by the LANGUAGE (if present). The LABEL MUST NOT appear more than once in a given link; occurrences after the first MUST be ignored by parsers. Description: This property parameter MAY be specified on the "CONFERENCE" property. It is anticipated that other extensions to iCalendar will reuse this property parameter on new properties that they define. As a result, clients MUST expect to find this property parameter present on many different properties. It provides a human-readable label that can be presented to calendar users to allow them to discriminate between properties that might be similar or provide additional information for properties that are not self-describing. The "LANGUAGE" property parameter can be used to specify the language of the text in the parameter value (as per Section 3.2.10 of :rfc:`5545`). Examples: This is a label of a chat. .. code-block:: ics CONFERENCE;VALUE=URI;FEATURE=VIDEO; LABEL="Web video chat, access code=76543"; :https://video-chat.example.com/;group-id=1234 """, ) FMTTYPE = string_parameter( "FMTTYPE", """FMTTYPE specfies the content type of a referenced object. Conformance: :rfc:`5545` specifies the FMTTYPE. :rfc:`9253` adds FMTTYPE to LINK properties. In a LINK, FMTTYPE maps to the "type" attribute defined in Section 3.4.1 of :rfc:`8288`. See :rfc:`6838`. Description: This parameter can be specified on properties that are used to reference an object. The parameter specifies the media type :rfc:`4288` of the referenced object. For example, on the "ATTACH" property, an FTP type URI value does not, by itself, necessarily convey the type of content associated with the resource. The parameter value MUST be the text for either an IANA-registered media type or a non-standard media type. Example: A Microsoft Word document: .. code-block:: ics ATTACH;FMTTYPE=application/msword:ftp://example.com/pub/docs/ agenda.doc A website: .. code-block:: ics LINK;FMTTYPE=text/html;LINKREL=SOURCE;LABEL=Venue;VALUE=URI: https://example.com/venue """, ) LINKREL = string_parameter( "LINKREL", """LINKREL Purpose: LINKREL specifies the relationship of data referenced by a LINK property. Conformance: LINKREL is specified in :rfc:`9253`. This parameter maps to the link relation type defined in Section 2.1 of :rfc:`8288`. It is always quoted. Description: This parameter MUST be specified on all LINK properties and define the type of reference. This allows programs consuming this data to automatically scan for references they support. There is no default relation type. Any link relation in the link registry established by :rfc:`8288`, or new link relations, may be used. It is expected that link relation types seeing significant usage in calendaring will have the calendaring usage described in an RFC. In the simplest case, a link relation type identifies the semantics of a link. For example, a link with the relation type "copyright" indicates that the current link context has a copyright resource at the link target. Link relation types can also be used to indicate that the target resource has particular attributes, or exhibits particular behaviours; for example, a "service" link implies that the link target can be used as part of a defined protocol (in this case, a service description). Registration: There are two kinds of relation types: registered and extension. These relation types are registered in :rfc:`8288`. .. seealso:: `Registered Link Relation Types `_. Examples: This identifies the latest version of the event information. .. code-block:: ics LINKREL=latest-version """, ) def _get_GAP(prop) -> timedelta | None: # noqa: N802 """GAP Purpose: GAP specifies the length of the gap, positive or negative, between two components with a temporal relationship. Format Definition: Same as the DURATION value type defined in :rfc:`5545`, Section 3.3.6. Description: This parameter MAY be specified on the RELATED-TO property and defines the duration of time between the predecessor and successor in an interval. When positive, it defines the lag time between a task and its logical successor. When negative, it defines the lead time. Examples: An example of lag time might be if Task-A is "paint the room" and Task-B is "lay the carpets". Then, Task-A may be related to Task-B with RELTYPE=FINISHTOSTART with a gap of 1 day -- long enough for the paint to dry. .. code-block:: text ==================== | paint the room |--+ ==================== | |(lag of one day) | | =================== +->| lay the carpet | =================== For an example of lead time, in constructing a two-story building, the electrical work must be done before painting. However, the painter can move in to the first floor as the electricians move upstairs. .. code-block:: text ===================== | electrical work |--+ ===================== | +-------------+ |(lead of estimated time) | ================== +->| painting | ================== """ value = prop.params.get("GAP") if value is None: return None from icalendar.prop import vDuration if isinstance(value, str): return vDuration.from_ical(value) if not isinstance(value, vDuration): raise TypeError("Value MUST be a vDuration instance") return value.td def _set_GAP(prop, value: timedelta | str | None): # noqa: N802 """Set the GAP parameter as a timedelta.""" if value is None: prop.params.pop("GAP", None) return from icalendar.prop import vDuration prop.params["GAP"] = vDuration(value) def _del_GAP(prop): # noqa: N802 """Delete the GAP parameter.""" prop.params.pop("GAP", None) GAP = property(_get_GAP, _set_GAP, _del_GAP) __all__ = [ "ALTREP", "CN", "CUTYPE", "DELEGATED_FROM", "DELEGATED_TO", "DIR", "FBTYPE", "FMTTYPE", "GAP", "LABEL", "LANGUAGE", "LINKREL", "MEMBER", "PARTSTAT", "RANGE", "RELATED", "ROLE", "RSVP", "SENT_BY", "TZID", "VALUE", "quoted_list_parameter", "string_parameter", ] collective-icalendar-138c845/src/icalendar/parser/000077500000000000000000000000001524134033600220525ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/parser/__init__.py000066400000000000000000000026331524134033600241670ustar00rootroot00000000000000"""This module parses and generates contentlines as defined in RFC 5545 (iCalendar), but will probably work for other MIME types with similar syntax. Eg. RFC 2426 (vCard) It is stupid in the sense that it treats the content purely as strings. No type conversion is attempted. """ from .content_line import Contentline, Contentlines from .parameter import ( Parameters, dquote, param_value, q_join, q_split, rfc_6868_escape, rfc_6868_unescape, validate_param_value, ) from .property import ( split_on_unescaped_comma, split_on_unescaped_semicolon, unescape_backslash, unescape_list_or_string, ) from .string import ( _escape_char, _escape_string, _foldline, _unescape_char, _unescape_string, escape_char, escape_string, foldline, unescape_char, unescape_string, validate_token, ) __all__ = [ "Contentline", "Contentlines", "Parameters", "_escape_char", "_escape_string", "_foldline", "_unescape_char", "_unescape_string", "dquote", "escape_char", "escape_string", "foldline", "param_value", "q_join", "q_split", "rfc_6868_escape", "rfc_6868_unescape", "split_on_unescaped_comma", "split_on_unescaped_semicolon", "unescape_backslash", "unescape_char", "unescape_list_or_string", "unescape_string", "validate_param_value", "validate_token", ] collective-icalendar-138c845/src/icalendar/parser/content_line.py000066400000000000000000000263431524134033600251150ustar00rootroot00000000000000"""parsing and generation of content lines""" import re from icalendar.parser.parameter import Parameters from icalendar.parser.property import unescape_backslash, unescape_list_or_string from icalendar.parser.string import ( _escape_string, _foldline, _unescape_string, validate_token, ) from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode # Equivalent to the natural ``(\r?\n)+[ \t]`` but without its quadratic cost: # the greedy run is re-tried at every line break of a long unfoldable block, so # a megabyte of bare newlines takes seconds. The leading lookbehinds pin a match # to the first line break of a run, making a failed attempt O(1) instead of a # full rescan, while matching exactly the same strings. UFOLD = re.compile(r"(?:(? str: """Strip optional whitespace around delimiters outside of quoted sections, respecting backslash escapes so that escaped delimiters are not treated as separators. This is a lenient parsing helper (used when strict=False) to support iCalendar content lines that contain extra whitespace around tokens. """ if not st: return st # Fast path for the common case in non-strict mode: # no whitespace in the parameter section means there is nothing to normalize. if " " not in st and "\t" not in st: return st # Fast regex-based path for simple parameter sections without quoting/escaping. if delimiters == ";=" and '"' not in st and "\\" not in st: st = OWS_BEFORE_DELIMITER_RE.sub(r"\1", st) return OWS_AFTER_DELIMITER_RE.sub(r"\1", st).strip() out: list[str] = [] pending_ws: list[str] = [] in_quotes = False escaped = False # True only if the last appended char was a raw delimiter. last_was_delimiter = False def flush_pending() -> None: nonlocal pending_ws if not pending_ws: return if not last_was_delimiter: out.extend(pending_ws) pending_ws.clear() for ch in st: # Handle escaped character (the backslash set escaped in previous iteration) if escaped: flush_pending() out.append(ch) escaped = False last_was_delimiter = False continue # Handle backslash to escape next character if ch == "\\" and not in_quotes: flush_pending() out.append(ch) escaped = True last_was_delimiter = False continue # Handle quote toggling if ch == '"' and not escaped: in_quotes = not in_quotes flush_pending() out.append(ch) last_was_delimiter = False continue # Whitespace outside quotes is buffered if not in_quotes and not escaped and ch in OWS: pending_ws.append(ch) continue # Raw delimiter (unescaped and outside quotes) if not in_quotes and not escaped and ch in delimiters: pending_ws.clear() while out and out[-1] in OWS: out.pop() out.append(ch) last_was_delimiter = True continue # Regular character flush_pending() out.append(ch) last_was_delimiter = False if pending_ws and not last_was_delimiter: out.extend(pending_ws) return "".join(out).strip() class Contentline(str): """A content line is basically a string that can be folded and parsed into parts. """ __slots__ = ("strict",) def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): value = to_unicode(value, encoding=encoding) assert "\n" not in value, ( "Content line can not contain unescaped new line characters." ) self = super().__new__(cls, value) self.strict = strict return self @classmethod def from_parts( cls, name: ICAL_TYPE, params: Parameters, values, sorted: bool = True, # noqa: A002 ): """Turn a parts into a content line.""" assert isinstance(params, Parameters) if hasattr(values, "to_ical"): values = values.to_ical() else: from icalendar.prop import vText values = vText(values).to_ical() # elif isinstance(values, basestring): # values = escape_char(values) # TODO: after unicode only, remove this # Convert back to unicode, after to_ical encoded it. name = to_unicode(name) values = to_unicode(values) if params: params = to_unicode(params.to_ical(sorted=sorted)) if params: # some parameter values can be skipped during serialization return cls(f"{name};{params}:{values}") return cls(f"{name}:{values}") def raw_parts(self) -> tuple[str, Parameters, str]: """Split the line into ``name``, ``parameters``, and raw ``values`` parts. This is :meth:`parts` without the unescaping: the values are returned verbatim, preserving both backslash sequences and URL encoding. It is used for :rfc:`7265` ``UNKNOWN`` values, whose real value type—and therefore whose escaping rules—are not known. See :meth:`parts` for the parts themselves and for examples. """ try: name_split: int | None = None value_split: int | None = None in_quotes: bool = False escaped: bool = False for i, ch in enumerate(self): if ch == '"' and not escaped: in_quotes = not in_quotes elif ch == "\\" and not in_quotes: escaped = True continue elif not in_quotes and not escaped: # Find first delimiter for name if ch in ":;" and name_split is None: name_split = i # Find value delimiter (first colon) if ch == ":" and value_split is None: value_split = i escaped = False # Validate parsing results if not value_split: # No colon found - value is empty, use end of string value_split = len(self) # Extract name - if no delimiter, # take whole string for validate_token to reject name = self[:name_split] if name_split else self if not self.strict: name = re.sub(r"[ \t]+", "", name.strip()) validate_token(name) if not name_split or name_split + 1 == value_split: # No delimiter or empty parameter section raise ValueError("Invalid content line") # noqa: TRY301 # Parse parameters - they still need to be escaped/unescaped # for proper handling of commas, semicolons, etc. in parameter values raw_param_str = self[name_split + 1 : value_split] if not self.strict: raw_param_str = _strip_ows_around_delimiters(raw_param_str) param_str = _escape_string(raw_param_str) params = Parameters.from_ical(param_str, strict=self.strict) params = Parameters( (_unescape_string(key), unescape_list_or_string(value)) for key, value in iter(params.items()) ) values = self[value_split + 1 :] except ValueError as exc: raise ValueError( f"Content line could not be parsed into parts: '{self}': {exc}" ) from exc return (name, params, values) def parts(self) -> tuple[str, Parameters, str]: """Split the line into ``name``, ``parameters``, and unescaped ``values`` parts. Properly handles escaping with backslashes and double-quote sections to avoid corrupting URL-encoded characters in values. The backslash sequences in the values are unescaped, as for the values of ``TEXT`` properties, while URL encoding is preserved. Use :meth:`raw_parts` to get the values verbatim instead. Examples: With parameter: .. code-block:: ics DESCRIPTION;ALTREP="cid:part1.0001@example.org":The Fall'98 Wild Without parameters: .. code-block:: ics DESCRIPTION:The Fall'98 Wild """ name, params, values = self.raw_parts() return (name, params, unescape_backslash(values)) def value_separator_index(self) -> int: r"""Return the index of the colon that separates the value. This is the first colon that is not inside a quoted parameter section. A colon inside a quoted parameter value (for example ``ALTREP="http://x"``) is skipped, and a colon that belongs to the value (``TEXT`` does not escape ``:``) is not mistaken for the separator. Backslash has no special meaning in the parameter grammar (:rfc:`5545#section-3.1`), so it is treated as an ordinary character. Returns: An integer representing the index position of the separator, or ``-1`` if there is none. """ in_quotes = False for i, ch in enumerate(self): if ch == '"': in_quotes = not in_quotes elif ch == ":" and not in_quotes: return i return -1 @classmethod def from_ical(cls, ical, strict=False): """Unfold the content lines in an iCalendar into long content lines.""" ical = to_unicode(ical) # a fold is carriage return followed by either a space or a tab return cls(UFOLD.sub("", ical), strict=strict) def to_ical(self): """Long content lines are folded so they are less than 75 characters wide. """ return _foldline(self).encode(DEFAULT_ENCODING) class Contentlines(list[Contentline]): """I assume that iCalendar files generally are a few kilobytes in size. Then this should be efficient. for Huge files, an iterator should probably be used instead. """ def to_ical(self): """Simply join self.""" return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n" @classmethod def from_ical(cls, st): """Parses a string into content lines.""" st = to_unicode(st) try: # a fold is carriage return followed by either a space or a tab unfolded = UFOLD.sub("", st) lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line) lines.append("") # '\r\n' at the end of every content line except Exception as e: raise ValueError("Expected StringType with content lines") from e return lines __all__ = ["Contentline", "Contentlines"] collective-icalendar-138c845/src/icalendar/parser/ical/000077500000000000000000000000001524134033600227625ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/parser/ical/__init__.py000066400000000000000000000002151524134033600250710ustar00rootroot00000000000000from .calendar import CalendarIcalParser from .component import ComponentIcalParser __all__ = ["CalendarIcalParser", "ComponentIcalParser"] collective-icalendar-138c845/src/icalendar/parser/ical/calendar.py000066400000000000000000000027421524134033600251120ustar00rootroot00000000000000"""Special parsing for calendar components.""" from .component import ComponentIcalParser class CalendarIcalParser(ComponentIcalParser): """A parser for calendar components.""" def prepare_components(self) -> None: """Prepare the parsed components. This handles timezone forward references. """ all_timezones_so_far = True for comp in self._components: for component in comp.subcomponents: if component.name == "VTIMEZONE": if not all_timezones_so_far: # If a preceding component refers to a VTIMEZONE # defined later in the source string (forward # references are allowed by RFC 5545), then the # earlier component may have the wrong timezone # attached. # However, during computation of comps, all VTIMEZONEs # observed do end up in the timezone cache. So simply # re-running from_ical will rely on the cache for those # forward references to produce the correct result. # See test_create_america_new_york_forward_reference. self.initialize_parsing() self.parse_content_lines() return else: all_timezones_so_far = False __all__ = ["CalendarIcalParser"] collective-icalendar-138c845/src/icalendar/parser/ical/component.py000066400000000000000000000256551524134033600253530ustar00rootroot00000000000000"""Parsing a component's iCalendar data.""" from __future__ import annotations from typing import TYPE_CHECKING, ClassVar from icalendar.parser.content_line import Contentline, Contentlines from icalendar.parser.property import split_on_unescaped_comma from icalendar.prop import vBroken from icalendar.timezone import tzp if TYPE_CHECKING: from icalendar.cal import Component, ComponentFactory from icalendar.parser.parameter import Parameters from icalendar.prop import VPROPERTY, TypesFactory class ComponentIcalParser: """A parser for a component's iCalendar data. This uses the template method pattern, where the main parsing logic can be refined in subclasses. """ datetime_names: ClassVar[tuple[str, ...]] = ( "DTSTART", "DTEND", "RECURRENCE-ID", "DUE", "RDATE", "EXDATE", ) """Names to check for TZID parameter when parsing datetimes. Their ``from_ical`` methods take an optional ``tzid`` argument, which is used if the property has a TZID parameter. """ def __init__( self, data: bytes | str | list[Contentline], component_factory: ComponentFactory, types_factory: TypesFactory, ) -> None: """Initialize the parser with the raw data. Parameters: data: The raw iCalendar data to parse, either as bytes or a list of content lines. component_factory: The factory to use for creating components. types_factory: The factory to use for creating property values. """ self._data = data self._component_factory = component_factory self._types_factory = types_factory self._tzp = tzp _content_lines: list[Contentline] def contains_component(self, name: str) -> bool: """Check if the parser contains a component.""" self.initialize_parsing() begin_line = "BEGIN:" + name.upper() return any( len(content_line) == len(begin_line) and content_line.upper() == begin_line for content_line in self._content_lines ) def contains_uid(self, uid: str) -> bool: """Determines whether the component contains a ``uid``. Returns: ``True`` if the component contains a ``uid``, else ``False``. """ self.initialize_parsing() return any(uid in line for line in self._content_lines) def initialize_parsing(self): self._stack: list[Component] = [] self._components: list[Component] = [] self._data = self._content_lines = ( self._data if isinstance(self._data, list) else Contentlines.from_ical(self._data) ) self._content_lines_iterator = iter(self._content_lines) def handle_line_parse_error(self, exception: Exception): """Handle a line parsing error.""" # if unable to parse a line within a component # that ignores exceptions, mark the component # as broken and skip the line. otherwise raise. component = self.component if not component or not component.ignore_exceptions: raise exception component.errors.append((None, str(exception))) def handle_begin_component(self, vals: str) -> None: """Handle the beginning of a component.""" # try and create one of the components defined in the spec, # otherwise get a general Components for robustness. c_name = vals.upper() c_class = self._component_factory.get_component_class(c_name) # If component factory cannot resolve ``c_name``, the generic # ``Component`` class is used which does not have the name set. # That's opposed to the usage of ``cls``, which represents a # more concrete subclass with a name set (e.g. VCALENDAR). component = c_class() if not getattr(component, "name", ""): # undefined components component.name = c_name self._stack.append(component) def handle_end_component(self, vals: str) -> None: """Handle the end of a component.""" # we are done adding properties to this component # so pop it from the stack and add it to the new top. if not self._stack: # The stack is currently empty, the input must be invalid raise ValueError("END encountered without an accompanying BEGIN!") component = self._stack.pop() if not self._stack: # we are at the end self._components.append(component) else: self._stack[-1].add_component(component) if vals.upper() == "VTIMEZONE" and "TZID" in component: tzp.cache_timezone_component(component) def prepare_components(self) -> None: """Prepare the parsed components. This is called when all components are parsed. """ def parse(self) -> list[Component]: """Parse the raw data.""" self.initialize_parsing() self.parse_content_lines() self.prepare_components() return self._components def parse_content_lines(self) -> None: """Parse the content lines.""" for line in self._content_lines_iterator: if not line: continue try: name, params, vals = line.parts() except ValueError as e: self.handle_line_parse_error(e) continue uname = name.upper() if uname == "BEGIN": self.handle_begin_component(vals) elif uname == "END": self.handle_end_component(vals) else: self.handle_property(uname, params, vals, line) @property def component(self) -> Component | None: return self._stack[-1] if self._stack else None def get_factory_for_property(self, name: str, params: Parameters) -> VPROPERTY: """Get the factory for a property.""" return self._types_factory.for_property(name, params.value) def handle_property( self, name: str, params: Parameters, vals: str, line: Contentline ) -> None: """Handle a property line. Add properties to the top of the current stack. Parameters: name: The name of the property, uppercased. params: The parameters of the property. vals: The value of the property. line: The original content line. """ # Extract VALUE parameter if present if not self.component: # only accept X-COMMENT at the end of the .ics file # ignore these components in parsing if name == "X-COMMENT": return raise ValueError(f'Property "{name}" does not have a parent component.') # Determine TZID for datetime properties tzid = params.get("TZID") if params and name in self.datetime_names else None # Handle special cases for value list preparation if name == "CATEGORIES": if self.handle_categories(params, vals, line): return # Fallback to normal processing if we can't find colon vals_list = [vals] elif name == "FREEBUSY": # Handle FREEBUSY comma-separated values vals_list = vals.split(",") # Workaround broken ICS files with empty RDATE # (not EXDATE - let it parse and fail) elif name == "RDATE" and vals == "": vals_list = [] else: factory = self.get_factory_for_property(name, params) # Value types that must not be unescaped provide their own value. get_value_from_content_line = getattr( factory, "get_value_from_content_line", None ) if get_value_from_content_line is not None: vals = get_value_from_content_line(line) vals_list = [vals] # Parse all properties eagerly for val in vals_list: self.parse_and_add_property(name, params, val, tzid, line) def parse_and_add_property( self, name: str, params: Parameters, val: str, tzid: str | None, line: Contentline, ): """Parse a property value and add it to the current component.""" factory = self.get_factory_for_property(name, params) try: if tzid: parsed_val = factory.from_ical(val, tzid) else: parsed_val = factory.from_ical(val) except (ValueError, TypeError) as e: self.handle_property_parse_error(e, name, params, val, line) else: vals_inst = factory(parsed_val) vals_inst.params = params self.component.add(name, vals_inst, encode=False) def handle_property_parse_error( self, exception: Exception, name: str, params: Parameters, val: str, line: Contentline, ): """Handle the parse error for a property.""" if not self.component.ignore_exceptions and not name[:2].upper() == "X-": raise exception # Error-tolerant mode: create vBroken factory = self.get_factory_for_property(name, params) expected_type = getattr(factory, "__name__", "unknown") broken_prop = vBroken.from_parse_error( raw_value=val, params=params, property_name=name, expected_type=expected_type, error=exception, ) self.component.errors.append((name, str(exception))) self.component.add(name, broken_prop, encode=0) def handle_categories( self, params: Parameters, vals: str, line: Contentline ) -> bool: """Handle the special case of CATEGORIES property. Returns: ``True`` if handled, else ``False``. """ # Special handling for CATEGORIES - need raw value # before unescaping to properly split on unescaped commas line_str = str(line) # The value separator is the first colon outside a quoted parameter # section. ``rfind`` here would pick a colon that belongs to the value # (``TEXT`` does not escape ``:``) and truncate the categories. colon_idx = line.value_separator_index() if colon_idx > 0: raw_value = line_str[colon_idx + 1 :] # Parse categories immediately (not lazily) for both # strict and tolerant components. # CATEGORIES needs special comma handling try: category_list = split_on_unescaped_comma(raw_value) factory = self.get_factory_for_property("CATEGORIES", params) vals_inst = factory(category_list) vals_inst.params = params self.component.add("CATEGORIES", vals_inst, encode=0) except ValueError as e: self.handle_property_parse_error( e, "CATEGORIES", params, raw_value, line ) return True return False collective-icalendar-138c845/src/icalendar/parser/ical/lazy.py000066400000000000000000000124641524134033600243220ustar00rootroot00000000000000"""Special parsing for calendar components.""" from __future__ import annotations from typing import TYPE_CHECKING, ClassVar from icalendar.parser.content_line import Contentline from .component import ComponentIcalParser if TYPE_CHECKING: from icalendar.cal.component import Component class LazyCalendarIcalParser(ComponentIcalParser): """A parser for calendar components. Instead of parsing the components, LazyComponents are created. Parsing can happen on demand. A calendar may grow over time, with its subcomponents greatly increasing in number, and requiring more memory and time to fully parse. This optimization lazily parses calendar files without consuming more memory than necessary, reducing the initial time it takes to access meta data. """ parse_instantly: ClassVar[tuple[str, ...]] = ("VCALENDAR",) """Parse these components immediately, instead of lazily. All other components are parsed lazily. """ def handle_begin_component(self, vals): """Begin a new component. This may be the first component. """ c_name = vals.upper() if ( c_name in self.parse_instantly or not self.component or not self.component.is_lazy() ): # these components are parsed immediately super().handle_begin_component(vals) else: self.handle_lazy_begin_component(c_name) def handle_lazy_begin_component(self, component_name: str) -> None: """Begin a new component, but do not parse it yet. Parameters: component_name: The upper case name of the component, for example, ``"VEVENT"``. """ content_lines = [Contentline(f"BEGIN:{component_name}")] for line in self._content_lines_iterator: content_lines.append(line) if ( line[:4].upper() == "END:" and line[4:].strip().upper() == component_name ): break if self.component is None: raise ValueError( f"BEGIN:{component_name} encountered outside of a parent component." ) self.component.add_component( LazySubcomponent( component_name, self.get_subcomponent_parser(content_lines), ) ) def get_subcomponent_parser( self, content_lines: list[Contentline] ) -> ComponentIcalParser: """Get the parser for a subcomponent. Parameters: content_lines: The content lines of the subcomponent. """ return ComponentIcalParser( content_lines, self._component_factory, self._types_factory ) def prepare_components(self): """Prepare the lazily parsed components.""" class LazySubcomponent: """A subcomponent that is evaluated lazily. This class holds the raw data of the subcomponent ready for parsing. """ def __init__(self, name: str, parser: ComponentIcalParser) -> None: """Initialize the lazy subcomponent with the raw data.""" self._name = name self._parser = parser self._component: Component | None = None @property def name(self) -> str: """The name of the subcomponent. The name is uppercased, per :rfc:`5545#section-2.1`. """ return self._name def is_parsed(self) -> bool: """Return whether the subcomponent is already parsed.""" return self._component is not None def parse(self) -> Component: """Parse the raw data and return the component.""" if self._component is None: components = self._parser.parse() if len(components) != 1: raise ValueError( f"Expected exactly one component in the subcomponent, " f"but got {len(components)}." ) self._component = components[0] self._parser = None # free memory return self._component def is_lazy(self) -> bool: """Return whether the subcomponents were accessed and parsed lazily. Call :meth:`parse` to get the fully parsed component. """ return True def __repr__(self) -> str: return f"LazySubcomponent(name={self._name}, parsed={self.is_parsed()})" def walk(self, name: str) -> list[Component]: """Walk through this component. This only parses the component if necessary. Parameters: name: The name to match for all components in the calendar, then walk through and parse the resulting matches. """ if not isinstance(name, str): raise TypeError("name must be a string.") if name == self.name or ( self._parser and self._parser.contains_component(name) ): return self.parse().walk(name) return [] def with_uid(self, uid: str) -> list[Component]: """Return the components containing the given ``uid``. This only parses the component if necessary. Parameters: uid: The UID of the components. """ if self._parser and not self._parser.contains_uid(uid): return [] return self.parse().with_uid(uid) __all__ = ["LazyCalendarIcalParser", "LazySubcomponent"] collective-icalendar-138c845/src/icalendar/parser/parameter.py000066400000000000000000000440421524134033600244100ustar00rootroot00000000000000"""Functions for parsing parameters.""" from __future__ import annotations import functools import os import re from datetime import datetime, time from typing import TYPE_CHECKING, Any, Protocol from icalendar.caselessdict import CaselessDict from icalendar.error import JCalParsingError from icalendar.parser.string import validate_token from icalendar.parser_tools import ( DEFAULT_ENCODING, SEQUENCE_TYPES, ) from icalendar.timezone.tzid import tzid_from_dt if TYPE_CHECKING: from collections.abc import Callable, Sequence from icalendar.enums import VALUE from icalendar.prop import VPROPERTY class HasToIcal(Protocol): """Protocol for objects with a to_ical method.""" def to_ical(self) -> bytes: """Convert to iCalendar format.""" ... def param_value( value: Sequence[str] | str | HasToIcal, always_quote: bool = False ) -> str: """Convert a parameter value to its iCalendar representation. Applies :rfc:`6868` escaping and optionally quotes the value according to :rfc:`5545` parameter value formatting rules. Parameters: value: The parameter value to convert. Can be a sequence, string, or object with a ``to_ical()`` method. always_quote: If ``True``, always enclose the value in double quotes. Defaults to ``False`` (only quote when necessary). Returns: The formatted parameter value, escaped and quoted as needed. """ if isinstance(value, SEQUENCE_TYPES): return q_join(map(rfc_6868_escape, value), always_quote=always_quote) if isinstance(value, str): return dquote(rfc_6868_escape(value), always_quote=always_quote) return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING))) # Could be improved UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]') QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]') def validate_param_value(value: str, quoted: bool = True) -> None: """Validate a parameter value for unsafe characters. Checks parameter values for characters that are not allowed according to :rfc:`5545`. Uses different validation rules for quoted and unquoted values. Parameters: value: The parameter value to validate. quoted: If ``True``, validate as a quoted value (allows more characters). If ``False``, validate as an unquoted value (stricter). Defaults to ``True``. Raises: ValueError: If the value contains unsafe characters for its quote state. """ validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR if validator.findall(value): raise ValueError(value) # chars presence of which in parameter value will be cause the value # to be enclosed in double-quotes QUOTABLE = re.compile("[,;:’]") # noqa: RUF001 def dquote(val: str, always_quote: bool = False) -> str: """Enclose parameter values in double quotes when needed. Parameter values containing special characters ``,``, ``;``, ``:`` or ``'`` must be enclosed in double quotes according to :rfc:`5545`. Double-quote characters in the value are replaced with single quotes since they're forbidden in parameter values. Parameters: val: The parameter value to quote. always_quote: If ``True``, always enclose in quotes regardless of content. Defaults to ``False`` (only quote when necessary). Returns: The value, enclosed in double quotes if needed or requested. """ # a double-quote character is forbidden to appear in a parameter value # so replace it with a single-quote character val = val.replace('"', "'") if QUOTABLE.search(val) or always_quote: return f'"{val}"' return val # parsing helper def q_split(st: str, sep: str = ",", maxsplit: int = -1) -> list[str]: """Split a string on a separator, respecting double quotes. Splits the string on the separator character, but ignores separators that appear inside double-quoted sections. This is needed for parsing parameter values that may contain quoted strings. Parameters: st: The string to split. sep: The separator character. Defaults to ``,``. maxsplit: Maximum number of splits to perform. If ``-1`` (default), then perform all possible splits. Returns: The split string parts. Examples: .. code-block:: pycon >>> from icalendar.parser import q_split >>> q_split('a,b,c') ['a', 'b', 'c'] >>> q_split('a,"b,c",d') ['a', '"b,c"', 'd'] >>> q_split('a;b;c', sep=';') ['a', 'b', 'c'] """ if maxsplit == 0: return [st] result = [] cursor = 0 length = len(st) inquote = 0 splits = 0 for i, ch in enumerate(st): if ch == '"': inquote = not inquote if not inquote and ch == sep: result.append(st[cursor:i]) cursor = i + 1 splits += 1 if i + 1 == length or splits == maxsplit: result.append(st[cursor:]) break return result def q_join(lst: Sequence[str], sep: str = ",", always_quote: bool = False) -> str: """Join a list with a separator, quoting items as needed. Joins list items with the separator, applying :func:`dquote` to each item to add double quotes when they contain special characters. Parameters: lst: The list of items to join. sep: The separator to use. Defaults to ``,``. always_quote: If ``True``, always quote all items. Defaults to ``False`` (only quote when necessary). Returns: The joined string with items quoted as needed. Examples: .. code-block:: pycon >>> from icalendar.parser import q_join >>> q_join(['a', 'b', 'c']) 'a,b,c' >>> q_join(['plain', 'has,comma']) 'plain,"has,comma"' """ return sep.join(dquote(itm, always_quote=always_quote) for itm in lst) def single_string_parameter(func: Callable | None = None, upper=False): """Create a parameter getter/setter for a single string parameter. Parameters: upper: Convert the value to uppercase func: The function to decorate. Returns: The property for the parameter or a decorator for the parameter if func is ``None``. """ def decorator(func): name = func.__name__ @functools.wraps(func) def fget(self: Parameters): """Get the value.""" value = self.get(name) if value is not None and upper: value = value.upper() return value def fset(self: Parameters, value: str | None): """Set the value""" if value is None: fdel(self) else: if upper: value = value.upper() self[name] = value def fdel(self: Parameters): """Delete the value.""" self.pop(name, None) return property(fget, fset, fdel, doc=func.__doc__) if func is None: return decorator return decorator(func) class Parameters(CaselessDict): """Parser and generator of Property parameter strings. It knows nothing of datatypes. Its main concern is textual structure. Examples: Modify parameters: .. code-block:: pycon >>> from icalendar import Parameters >>> params = Parameters() >>> params['VALUE'] = 'TEXT' >>> params.value 'TEXT' >>> params Parameters({'VALUE': 'TEXT'}) Create new parameters: .. code-block:: pycon >>> params = Parameters(value="BINARY") >>> params.value 'BINARY' Set a default: .. code-block:: pycon >>> params = Parameters(value="BINARY", default_value="TEXT") >>> params Parameters({'VALUE': 'BINARY'}) """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Create new parameters.""" if args and args[0] is None: # allow passing None args = args[1:] defaults = { key[8:]: kwargs.pop(key) for key in list(kwargs.keys()) if key.lower().startswith("default_") } super().__init__(*args, **kwargs) for key, value in defaults.items(): self.setdefault(key, value) # The following paremeters must always be enclosed in double quotes always_quoted = ( "ALTREP", "DELEGATED-FROM", "DELEGATED-TO", "DIR", "MEMBER", "SENT-BY", # Part of X-APPLE-STRUCTURED-LOCATION "X-ADDRESS", "X-TITLE", # RFC 9253 "LINKREL", ) # this is quoted should one of the values be present quote_also = { # This is escaped in the RFC "CN": " '", } def params(self): """In RFC 5545 keys are called parameters, so this is to be consitent with the naming conventions. """ return self.keys() def to_ical(self, sorted: bool = True): # noqa: A002 """Returns an :rfc:`5545` representation of the parameters. Parameters: sorted (bool): Sort the parameters before encoding. exclude_utc (bool): Exclude TZID if it is set to ``"UTC"`` """ result = [] items = list(self.items()) if sorted: items.sort() for key, value in items: if key == "TZID" and value == "UTC": # The "TZID" property parameter MUST NOT be applied to DATE-TIME # properties whose time values are specified in UTC. continue upper_key = key.upper() check_quoteable_characters = self.quote_also.get(key.upper()) always_quote = upper_key in self.always_quoted or ( check_quoteable_characters and any(c in value for c in check_quoteable_characters) ) quoted_value = param_value(value, always_quote=always_quote) if isinstance(quoted_value, str): quoted_value = quoted_value.encode(DEFAULT_ENCODING) # CaselessDict keys are always unicode result.append(upper_key.encode(DEFAULT_ENCODING) + b"=" + quoted_value) return b";".join(result) @classmethod def from_ical(cls, st, strict=False): """Parses the parameter format from ical text format.""" # parse into strings result = cls() for param in q_split(st, ";"): try: key, val = q_split(param, "=", maxsplit=1) validate_token(key) # Property parameter values that are not in quoted # strings are case insensitive. vals = [] for v in q_split(val, ","): if v.startswith('"') and v.endswith('"'): v2 = v.strip('"') validate_param_value(v2, quoted=True) vals.append(rfc_6868_unescape(v2)) else: validate_param_value(v, quoted=False) if strict: vals.append(rfc_6868_unescape(v.upper())) else: vals.append(rfc_6868_unescape(v)) if not vals: result[key] = val elif len(vals) == 1: result[key] = vals[0] else: result[key] = vals except ValueError as exc: # noqa: PERF203 raise ValueError( f"{param!r} is not a valid parameter string: {exc}" ) from exc return result @single_string_parameter(upper=True) def value(self) -> VALUE | str | None: """The VALUE parameter from :rfc:`5545`. Description: This parameter specifies the value type and format of the property value. The property values MUST be of a single value type. For example, a "RDATE" property cannot have a combination of DATE-TIME and TIME value types. If the property's value is the default value type, then this parameter need not be specified. However, if the property's default value type is overridden by some other allowable value type, then this parameter MUST be specified. Applications MUST preserve the value data for x-name and iana- token values that they don't recognize without attempting to interpret or parse the value data. For convenience, using this property, the value will be converted to an uppercase string. .. code-block:: pycon >>> from icalendar import Parameters >>> params = Parameters() >>> params.value = "unknown" >>> params Parameters({'VALUE': 'UNKNOWN'}) """ def _parameter_value_to_jcal( self, value: str | float | list | VPROPERTY ) -> str | int | float | list[str] | list[int] | list[float]: """Convert a parameter value to jCal format. Parameters: value: The parameter value Returns: The jCal representation of the parameter value """ if isinstance(value, list): return [self._parameter_value_to_jcal(v) for v in value] if hasattr(value, "to_jcal"): # proprty values respond to this jcal = value.to_jcal() # we only need the value part if len(jcal) == 4: return jcal[3] return jcal[3:] for t in (int, float, str): if isinstance(value, t): return t(value) raise TypeError( "Unsupported parameter value type for jCal conversion: " f"{type(value)} {value!r}" ) def to_jcal(self, exclude_utc=False) -> dict[str, str]: """Return the jCal representation of the parameters. Parameters: exclude_utc (bool): Exclude the TZID parameter if it is UTC """ jcal = { k.lower(): self._parameter_value_to_jcal(v) for k, v in self.items() if k.lower() != "value" } if exclude_utc and jcal.get("tzid") == "UTC": del jcal["tzid"] return jcal @single_string_parameter def tzid(self) -> str | None: """The TZID parameter from :rfc:`5545`.""" def is_utc(self) -> bool: """Whether the TZID parameter is UTC.""" return self.tzid == "UTC" def update_tzid_from(self, dt: datetime | time | Any) -> None: """Update the TZID parameter from a datetime object. This sets the TZID parameter or deletes it according to the datetime. :rfc:`5545#section-3.2.19` prohibits TZID on UTC datetimes, which use the ``Z`` suffix instead. """ if isinstance(dt, (datetime, time)): tzid = tzid_from_dt(dt) if tzid != "UTC": # UTC uses Z suffix and does not appear as TZID parameter self.tzid = tzid @classmethod def from_jcal(cls, jcal: dict[str : str | list[str]]): """Parse jCal parameters.""" if not isinstance(jcal, dict): raise JCalParsingError("The parameters must be a mapping.", cls) for name, value in jcal.items(): if not isinstance(name, str): raise JCalParsingError( "All parameter names must be strings.", cls, value=name ) JCalParsingError.validate_jcal_token(name, "parameter name", cls) if not ( ( isinstance(value, list) and all(isinstance(v, (str, int, float)) for v in value) and value ) or isinstance(value, (str, int, float)) ): raise JCalParsingError( "Parameter values must be a string, integer or " "float or a list of those.", cls, name, value=value, ) return cls(jcal) @classmethod def from_jcal_property(cls, jcal_property: list): """Create the parameters for a jCal property. Parameters: jcal_property (list): The jCal property [name, params, value, ...] default_value (str, optional): The default value of the property. If this is given, the default value will not be set. """ if not isinstance(jcal_property, list) or len(jcal_property) < 4: raise JCalParsingError( "The property must be a list with at least 4 items.", cls ) jcal_params = jcal_property[1] with JCalParsingError.reraise_with_path_added(1): self = cls.from_jcal(jcal_params) if self.is_utc(): del self.tzid # we do not want this parameter return self RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'") def rfc_6868_unescape(param_value: str) -> str: """Take care of :rfc:`6868` unescaping. - ^^ -> ^ - ^n -> system specific newline - ^' -> " - ^ with others stay intact """ replacements = { "^^": "^", "^n": os.linesep, "^'": '"', } return RFC_6868_UNESCAPE_REGEX.sub( lambda m: replacements.get(m.group(0), m.group(0)), param_value ) RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"') def rfc_6868_escape(param_value: str) -> str: """Take care of :rfc:`6868` escaping. - ^ -> ^^ - " -> ^' - newline -> ^n """ replacements = { "^": "^^", "\n": "^n", "\r": "^n", "\r\n": "^n", '"': "^'", } return RFC_6868_ESCAPE_REGEX.sub( lambda m: replacements.get(m.group(0), m.group(0)), param_value ) __all__ = [ "Parameters", "dquote", "param_value", "q_join", "q_split", "rfc_6868_escape", "rfc_6868_unescape", "validate_param_value", ] collective-icalendar-138c845/src/icalendar/parser/property.py000066400000000000000000000105671524134033600243210ustar00rootroot00000000000000"""Tools for parsing properties.""" import re from icalendar.parser.string import _unescape_string def unescape_list_or_string(val: str | list[str]) -> str | list[str]: """Unescape a value that may be a string or list of strings. Applies :func:`_unescape_string` to the value. If the value is a list, unescapes each element. Parameters: val: A string or list of strings to unescape. Returns: The unescaped values. """ if isinstance(val, list): return [_unescape_string(s) for s in val] return _unescape_string(val) _unescape_backslash_regex = re.compile(r"\\([\\,;:nN])") def unescape_backslash(val: str): r"""Unescape backslash sequences in iCalendar text. Unlike :func:`~icalendar.parser.string.unescape_string`, this only handles actual backslash escapes per :rfc:`5545`, not URL encoding. This preserves URL-encoded values like ``%3A`` in URLs. Processes backslash escape sequences in a single pass using regex matching. """ return _unescape_backslash_regex.sub( lambda m: "\n" if m.group(1) in "nN" else m.group(1), val ) def split_on_unescaped_comma(text: str) -> list[str]: r"""Split text on unescaped commas and unescape each part. Splits only on commas not preceded by backslash. After splitting, unescapes backslash sequences in each part. Parameters: text: Text with potential escaped commas (e.g., "foo\\, bar,baz") Returns: List of unescaped category strings Examples: .. code-block:: pycon >>> from icalendar.parser import split_on_unescaped_comma >>> split_on_unescaped_comma(r"foo\, bar,baz") ['foo, bar', 'baz'] >>> split_on_unescaped_comma("a,b,c") ['a', 'b', 'c'] >>> split_on_unescaped_comma(r"a\,b\,c") ['a,b,c'] >>> split_on_unescaped_comma(r"Work,Personal\,Urgent") ['Work', 'Personal,Urgent'] """ if not text: return [""] result = [] current = [] i = 0 while i < len(text): if text[i] == "\\" and i + 1 < len(text): # Escaped character - keep both backslash and next char current.append(text[i]) current.append(text[i + 1]) i += 2 elif text[i] == ",": # Unescaped comma - split point result.append(unescape_backslash("".join(current))) current = [] i += 1 else: current.append(text[i]) i += 1 # Add final part result.append(unescape_backslash("".join(current))) return result def split_on_unescaped_semicolon(text: str) -> list[str]: r"""Split text on unescaped semicolons and unescape each part. Splits only on semicolons not preceded by a backslash. After splitting, unescapes backslash sequences in each part. Used by vCard structured properties (ADR, N, ORG) per :rfc:`6350`. Parameters: text: Text with potential escaped semicolons (e.g., "field1\\;with;field2") Returns: List of unescaped field strings Examples: .. code-block:: pycon >>> from icalendar.parser import split_on_unescaped_semicolon >>> split_on_unescaped_semicolon(r"field1\;with;field2") ['field1;with', 'field2'] >>> split_on_unescaped_semicolon("a;b;c") ['a', 'b', 'c'] >>> split_on_unescaped_semicolon(r"a\;b\;c") ['a;b;c'] >>> split_on_unescaped_semicolon(r"PO Box 123\;Suite 200;City") ['PO Box 123;Suite 200', 'City'] """ if not text: return [""] result = [] current = [] i = 0 while i < len(text): if text[i] == "\\" and i + 1 < len(text): # Escaped character - keep both backslash and next char current.append(text[i]) current.append(text[i + 1]) i += 2 elif text[i] == ";": # Unescaped semicolon - split point result.append(unescape_backslash("".join(current))) current = [] i += 1 else: current.append(text[i]) i += 1 # Add final part result.append(unescape_backslash("".join(current))) return result __all__ = [ "split_on_unescaped_comma", "split_on_unescaped_semicolon", "unescape_backslash", "unescape_list_or_string", ] collective-icalendar-138c845/src/icalendar/parser/string.py000066400000000000000000000212341524134033600237340ustar00rootroot00000000000000"""Functions for manipulating strings and bytes.""" import re from icalendar.compatibility import deprecate_for_version_8 from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode def _escape_char(text: str | bytes) -> str: r"""Format value according to iCalendar TEXT escaping rules. Escapes special characters in text values according to :rfc:`5545#section-3.3.11` rules. The order of replacements matters to avoid double-escaping. Parameters: text: The text to escape. Returns: The escaped text with special characters escaped. Note: The replacement order is critical: 1. ``\N`` -> ``\n`` (normalize newlines to lowercase) 2. ``\`` -> ``\\`` (escape backslashes) 3. ``;`` -> ``\;`` (escape semicolons) 4. ``,`` -> ``\,`` (escape commas) 5. ``\r\n`` -> ``\n`` (normalize line endings) 6. ``"\n"`` -> ``r"\n"`` (transform a newline character to a literal, or raw, newline character) 7. ``"\r"`` -> ``r"\n"`` (transform a lone carriage return to a literal newline character) Steps 5 to 7 normalize ``\r\n``, ``\n``, or a lone ``\r`` to ``\n``. The line-ending normalization is an implementation convenience, not part of :rfc:`5545`, which only defines ``\n`` or ``\N`` for an intentional line break, and doesn't give an escape form for a lone ``\r``. """ assert isinstance(text, (str, bytes)) text = to_unicode(text) # NOTE: ORDER MATTERS! return ( text.replace(r"\N", "\n") .replace("\\", "\\\\") .replace(";", r"\;") .replace(",", r"\,") .replace("\r\n", r"\n") .replace("\n", r"\n") .replace("\r", r"\n") ) escape_char = deprecate_for_version_8(_escape_char) """Format value according to iCalendar TEXT escaping rules. .. deprecated:: 7.0.0 Use the private :func:`_escape_char` internally. For external use, this function is deprecated. Please use alternative escaping methods or contact the maintainers. """ def _unescape_char(text: str | bytes) -> str | bytes | None: r"""Unescape iCalendar TEXT values. Reverses the escaping applied by :func:`_escape_char` according to :rfc:`5545#section-3.3.11` TEXT escaping rules. Parameters: text: The escaped text. Returns: The unescaped text, or ``None`` if ``text`` is neither ``str`` nor ``bytes``. Note: The replacement order is critical to avoid double-unescaping: 1. ``\N`` -> ``\n`` (intermediate step) 2. ``\r\n`` -> ``\n`` (normalize line endings) 3. ``\n`` -> newline (unescape newlines) 4. ``\,`` -> ``,`` (unescape commas) 5. ``\;`` -> ``;`` (unescape semicolons) 6. ``\\`` -> ``\`` (unescape backslashes last) """ assert isinstance(text, (str, bytes)) # NOTE: ORDER MATTERS! if isinstance(text, str): return ( text.replace("\\N", "\\n") .replace("\r\n", "\n") .replace("\\n", "\n") .replace("\\,", ",") .replace("\\;", ";") .replace("\\\\", "\\") ) if isinstance(text, bytes): return ( text.replace(b"\\N", b"\\n") .replace(b"\r\n", b"\n") .replace(b"\\n", b"\n") .replace(b"\\,", b",") .replace(b"\\;", b";") .replace(b"\\\\", b"\\") ) return None unescape_char = deprecate_for_version_8(_unescape_char) """Unescape iCalendar TEXT values. .. deprecated:: 7.0.0 Use the private :func:`_unescape_char` internally. For external use, this function is deprecated. Please use alternative unescaping methods or contact the maintainers. """ def _foldline(line: str, limit: int = 75, fold_sep: str = "\r\n ") -> str: """Make a string folded as defined in RFC5545. Lines of text SHOULD NOT be longer than 75 octets, excluding the line break. Long content lines SHOULD be split into a multiple line representations using a line "folding" technique. That is, a long line can be split between any two characters by inserting a CRLF immediately followed by a single linear white-space character (i.e., SPACE or HTAB). """ assert isinstance(line, str) assert "\n" not in line folded_lines: list[str] = [] current_chars: list[str] = [] byte_count = 0 for char in line: char_byte_len = len(char.encode(DEFAULT_ENCODING)) if current_chars and byte_count + char_byte_len >= limit: # For compatibility with existing clients, avoid splitting escaped # values such as TEXT backslash escapes or RFC 6868 parameter # escapes across a folded line boundary. See issue #1501. if len(current_chars) > 1 and current_chars[-1] in r"\^": escaped_prefix = current_chars.pop() folded_lines.append("".join(current_chars)) current_chars = [escaped_prefix] byte_count = len(escaped_prefix.encode(DEFAULT_ENCODING)) else: folded_lines.append("".join(current_chars)) current_chars = [] byte_count = 0 current_chars.append(char) byte_count += char_byte_len if current_chars: folded_lines.append("".join(current_chars)) return fold_sep.join(folded_lines) foldline = deprecate_for_version_8(_foldline) """Make a string folded as defined in RFC5545. .. deprecated:: 7.0.0 Use the private :func:`_foldline` internally. """ def _escape_string(val: str) -> str: r"""Escape backslash sequences to URL-encoded hex values. Converts backslash-escaped characters to their percent-encoded hex equivalents. This is used for parameter parsing to preserve escaped characters during processing. Parameters: val: The string with backslash escapes. Returns: The string with backslash escapes converted to percent encoding. Note: Conversions: - ``%`` -> ``%25`` - ``\,`` -> ``%2C`` - ``\:`` -> ``%3A`` - ``\;`` -> ``%3B`` - ``\\`` -> ``%5C`` A literal ``%`` is escaped first so that percent sequences already in the value (e.g. ``%2C`` in a URI) are not confused with the markers introduced here. :func:`_unescape_string` reverses it. """ # f'{i:02X}' return ( val.replace("%", "%25") .replace(r"\,", "%2C") .replace(r"\:", "%3A") .replace(r"\;", "%3B") .replace(r"\\", "%5C") ) escape_string = deprecate_for_version_8(_escape_string) """Escape backslash sequences to URL-encoded hex values. .. deprecated:: 7.0.0 Use the private :func:`_escape_string` internally. For external use, this function is deprecated. """ def _unescape_string(val: str) -> str: r"""Unescape URL-encoded hex values to their original characters. Reverses :func:`_escape_string` by converting percent-encoded hex values back to their original characters. This is used for parameter parsing. Parameters: val: The string with percent-encoded values. Returns: The string with percent encoding converted to characters. Note: Conversions: - ``%2C`` -> ``,`` - ``%3A`` -> ``:`` - ``%3B`` -> ``;`` - ``%5C`` -> ``\`` - ``%25`` -> ``%`` ``%25`` is restored last so a literal ``%`` that :func:`_escape_string` protected does not re-trigger the marker replacements above. """ return ( val.replace("%2C", ",") .replace("%3A", ":") .replace("%3B", ";") .replace("%5C", "\\") .replace("%25", "%") ) unescape_string = deprecate_for_version_8(_unescape_string) """Unescape URL-encoded hex values to their original characters. .. deprecated:: 7.0.0 Use the private :func:`_unescape_string` internally. For external use, this function is deprecated. """ # [\w-] because of the iCalendar RFC # . because of the vCard RFC NAME = re.compile(r"[\w.-]+") def validate_token(name: str) -> None: r"""Validate that a name is a valid iCalendar token. Checks if the name matches the :rfc:`5545` token syntax using the NAME regex pattern (``[\w.-]+``). Parameters: name: The token name to validate. Raises: ValueError: If the name is not a valid token. """ match = NAME.findall(name) if len(match) == 1 and name == match[0]: return raise ValueError(name) __all__ = [ "_escape_char", "_escape_string", "_foldline", "_unescape_char", "_unescape_string", "escape_char", "escape_string", "foldline", "unescape_char", "unescape_string", "validate_token", ] collective-icalendar-138c845/src/icalendar/parser_tools.py000066400000000000000000000037151524134033600236520ustar00rootroot00000000000000SEQUENCE_TYPES = (list, tuple) DEFAULT_ENCODING = "utf-8" ICAL_TYPE = str | bytes def from_unicode(value: ICAL_TYPE, encoding: str = "utf-8") -> bytes: """Converts a value to bytes, even if it is already bytes. Parameters: value: The value to convert. encoding: The encoding to use in the conversion. Returns: The bytes representation of the value. """ if isinstance(value, bytes): return value if isinstance(value, str): try: return value.encode(encoding) except UnicodeEncodeError: return value.encode("utf-8", "replace") else: return value def to_unicode(value: ICAL_TYPE, encoding: str = "utf-8-sig") -> str: """Converts a value to Unicode, even if it is already a Unicode string. Parameters: value: The value to convert. encoding: The encoding to use in the conversion. """ if isinstance(value, str): return value if isinstance(value, bytes): try: return value.decode(encoding) except UnicodeDecodeError: return value.decode("utf-8-sig", "replace") else: return value def data_encode( data: ICAL_TYPE | dict | list, encoding: str = DEFAULT_ENCODING ) -> bytes | list[bytes] | dict: """Encode all datastructures to the given encoding. Currently Unicode strings, dicts, and lists are supported. Parameters: data: The datastructure to encode. """ # https://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str if isinstance(data, str): return data.encode(encoding) if isinstance(data, dict): return dict(map(data_encode, iter(data.items()))) if isinstance(data, (list, tuple)): return list(map(data_encode, data)) return data __all__ = [ "DEFAULT_ENCODING", "ICAL_TYPE", "SEQUENCE_TYPES", "data_encode", "from_unicode", "to_unicode", ] collective-icalendar-138c845/src/icalendar/prop/000077500000000000000000000000001524134033600215365ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/prop/__init__.py000066400000000000000000000063661524134033600236620ustar00rootroot00000000000000"""This module contains the parser/generators (or coders/encoders if you prefer) for the classes/datatypes that are used in iCalendar: ########################################################################### # This module defines these property value data types and property parameters 4.2 Defined property parameters are: .. code-block:: ics ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE, FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP, SENT-BY, TZID, VALUE 4.3 Defined value data types are: .. code-block:: ics BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER, PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET ########################################################################### iCalendar properties have values. The values are strongly typed. This module defines these types, calling val.to_ical() on them will render them as defined in rfc5545. If you pass any of these classes a Python primitive, you will have an object that can render itself as iCalendar formatted date. Property Value Data Types start with a 'v'. they all have an to_ical() and from_ical() method. The to_ical() method generates a text string in the iCalendar format. The from_ical() method can parse this format and return a primitive Python datatype. So it should always be true that: .. code-block:: python x == vDataType.from_ical(VDataType(x).to_ical()) These types are mainly used for parsing and file generation. But you can set them directly. """ from typing import TypeAlias from .adr import AdrFields, vAdr from .binary import vBinary from .boolean import vBoolean from .broken import vBroken from .cal_address import vCalAddress from .categories import vCategory from .dt import ( DT_TYPE, TimeBase, vDate, vDatetime, vDDDLists, vDDDTypes, vDuration, vPeriod, vTime, vUTCOffset, ) from .factory import TypesFactory from .float import vFloat from .geo import vGeo from .inline import vInline from .integer import vInt from .n import NFields, vN from .org import vOrg from .recur import vFrequency, vMonth, vRecur, vSkip, vWeekday from .text import vText from .uid import vUid from .unknown import vUnknown from .uri import vUri from .xml_reference import vXmlReference VPROPERTY: TypeAlias = ( vAdr | vBoolean | vBroken | vCalAddress | vCategory | vDDDLists | vDDDTypes | vDate | vDatetime | vDuration | vFloat | vFrequency | vInt | vMonth | vN | vOrg | vPeriod | vRecur | vSkip | vText | vTime | vUTCOffset | vUri | vWeekday | vInline | vBinary | vGeo | vUnknown | vXmlReference | vUid ) __all__ = [ "DT_TYPE", "VPROPERTY", "AdrFields", "NFields", "TimeBase", "TypesFactory", "vAdr", "vBinary", "vBoolean", "vBroken", "vCalAddress", "vCategory", "vDDDLists", "vDDDTypes", "vDate", "vDatetime", "vDuration", "vFloat", "vFrequency", "vGeo", "vInline", "vInt", "vMonth", "vN", "vOrg", "vPeriod", "vRecur", "vSkip", "vText", "vTime", "vUTCOffset", "vUid", "vUnknown", "vUri", "vWeekday", "vXmlReference", ] collective-icalendar-138c845/src/icalendar/prop/adr.py000066400000000000000000000134611524134033600226630ustar00rootroot00000000000000"""ADR property of :rfc:`6350`.""" from typing import Any, ClassVar, NamedTuple from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class AdrFields(NamedTuple): """Named fields for vCard ADR (Address) property per :rfc:`6350#section-6.3.1`. Provides named access to the seven address components. """ po_box: str """Post office box.""" extended: str """Extended address (e.g., apartment or suite number).""" street: str """Street address.""" locality: str """Locality (e.g., city).""" region: str """Region (e.g., state or province).""" postal_code: str """Postal code.""" country: str """Country name (full name).""" class vAdr: """vCard ADR (Address) structured property per :rfc:`6350#section-6.3.1`. The ADR property represents a delivery address as a single text value. The structured type value consists of a sequence of seven address components. The component values must be specified in their corresponding position. - post office box - extended address (e.g., apartment or suite number) - street address - locality (e.g., city) - region (e.g., state or province) - postal code - country name (full name) When a component value is missing, the associated component separator MUST still be specified. Semicolons are field separators and are NOT escaped. Commas and backslashes within field values ARE escaped per :rfc:`6350`. Examples: .. code-block:: pycon >>> from icalendar.prop import vAdr >>> adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA")) >>> adr.to_ical() b';;123 Main St;Springfield;IL;62701;USA' >>> vAdr.from_ical(";;123 Main St;Springfield;IL;62701;USA") AdrFields(po_box='', extended='', street='123 Main St', locality='Springfield', region='IL', postal_code='62701', country='USA') """ default_value: ClassVar[str] = "TEXT" params: Parameters fields: AdrFields def __init__( self, fields: tuple[str, ...] | list[str] | str | AdrFields, /, params: dict[str, Any] | None = None, ) -> None: """Initialize ADR with seven fields or parse from vCard format string. Parameters: fields: Either an AdrFields, tuple, or list of seven strings, one per field, or a vCard format string with semicolon-separated fields params: Optional property parameters """ if isinstance(fields, str): fields = self.from_ical(fields) if isinstance(fields, AdrFields): self.fields = fields else: if len(fields) != 7: raise ValueError(f"ADR must have exactly 7 fields, got {len(fields)}") self.fields = AdrFields(*(str(f) for f in fields)) self.params = Parameters(params) def to_ical(self) -> bytes: """Generate vCard format with semicolon-separated fields.""" # Each field is vText (handles comma/backslash escaping) # but we join with unescaped semicolons (field separators) from icalendar.prop.text import vText parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] return ";".join(parts).encode(DEFAULT_ENCODING) @staticmethod def from_ical(ical: str | bytes) -> AdrFields: """Parse vCard ADR format into an AdrFields named tuple. Parameters: ical: vCard format string with semicolon-separated fields Returns: AdrFields named tuple with seven field values. """ from icalendar.parser import split_on_unescaped_semicolon ical = to_unicode(ical) fields = split_on_unescaped_semicolon(ical) if len(fields) != 7: raise ValueError( f"ADR must have exactly 7 fields, got {len(fields)}: {ical}" ) return AdrFields(*fields) def __eq__(self, other: object) -> bool: """self == other""" return isinstance(other, vAdr) and self.fields == other.fields def __repr__(self) -> str: """String representation.""" return f"{self.__class__.__name__}({self.fields}, params={self.params})" @property def ical_value(self) -> AdrFields: """The address fields as a named tuple.""" return self.fields from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" result = [name, self.params.to_jcal(), self.VALUE.lower()] result.extend(self.fields) return result @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) if len(jcal_property) != 10: # name, params, value_type, 7 fields raise JCalParsingError( f"ADR must have 10 elements (name, params, value_type, 7 fields), " f"got {len(jcal_property)}" ) for i, field in enumerate(jcal_property[3:], start=3): JCalParsingError.validate_value_type(field, str, cls, i) return cls( tuple(jcal_property[3:]), Parameters.from_jcal_property(jcal_property), ) @classmethod def examples(cls) -> list[Self]: """Examples of vAdr.""" return [cls(("", "", "123 Main St", "Springfield", "IL", "62701", "USA"))] __all__ = ["AdrFields", "vAdr"] collective-icalendar-138c845/src/icalendar/prop/binary.py000066400000000000000000000111171524134033600233750ustar00rootroot00000000000000"""BINARY values from :rfc:`5545`.""" import base64 import binascii from typing import ClassVar from icalendar.compatibility import Self, deprecate_for_version_8 from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import to_unicode class vBinary: """Binary property values are Base64 encoded.""" default_value: ClassVar[str] = "BINARY" params: Parameters bytes: bytes """The raw binary value of the BINARY property. This is the authoritative storage and round-trips losslessly, including for non-UTF-8 data. Use this instead of the deprecated :attr:`obj`. """ def __init__(self, obj: str | bytes, params: dict[str, str] | None = None) -> None: if isinstance(obj, str): self.bytes = obj.encode("utf-8") else: self.bytes = obj self.params = Parameters(encoding="BASE64", value="BINARY") if params: self.params.update(params) def __repr__(self) -> str: return f"vBinary({self.to_ical()})" def to_ical(self) -> bytes: return base64.b64encode(self.bytes) @staticmethod def from_ical(ical: str | bytes) -> bytes: try: return base64.b64decode(ical, validate=True) except (binascii.Error, ValueError) as e: raise ValueError("Not valid base 64 encoding.") from e @property def base64data(self) -> str: """The Base64-encoded string view of this value. This is the same string that :meth:`to_ical` produces, exposed as a plain :class:`str` instead of :class:`bytes` so you don't need to call :func:`base64.b64encode` yourself. See :issue:`1550`. Returns: The Base64-encoded representation of the stored value. """ return self.to_ical().decode("ascii") @base64data.setter def base64data(self, value: str) -> None: """Set this value from a Base64-encoded string. The decoded raw bytes are stored in :attr:`bytes`, so non-UTF-8 payloads round-trip losslessly. Parameters: value: A Base64-encoded string. Raises: ValueError: If ``value`` isn't valid Base64. """ self.bytes = self.from_ical(value) @property @deprecate_for_version_8 def obj(self) -> str: """Deprecated string view of the value. .. deprecated:: 7.1.3 Use :attr:`bytes` for the raw binary value. ``obj`` decodes :attr:`bytes` as text and is lossy for non-UTF-8 data. It will be removed in icalendar 8. """ return to_unicode(self.bytes) @obj.setter @deprecate_for_version_8 def obj(self, value: str | bytes) -> None: self.bytes = value.encode("utf-8") if isinstance(value, str) else value def __eq__(self, other: object) -> bool: """self == other""" return isinstance(other, vBinary) and self.bytes == other.bytes def __hash__(self) -> int: """Hash of the vBinary object.""" return hash(self.bytes) @classmethod def examples(cls) -> list[Self]: """Examples of vBinary.""" return [cls("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4")] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" params = self.params.to_jcal() if params.get("encoding") == "BASE64": # BASE64 is the only allowed encoding del params["encoding"] return [name, params, self.VALUE.lower(), self.base64data] @property def ical_value(self) -> bytes: """The raw ``bytes`` value of the BINARY property. .. versionadded:: 7.1.0 .. versionchanged:: 7.1.3 Returns the raw stored bytes. Previously the stored value was Base64-decoded, which raised :class:`ValueError` for non-Base64 input. See :pr:`1356`. """ return self.bytes @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265` to a vBinary. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3) return cls( cls.from_ical(jcal_property[3]), params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vBinary"] collective-icalendar-138c845/src/icalendar/prop/boolean.py000066400000000000000000000065031524134033600235330ustar00rootroot00000000000000"""BOOLEAN values from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.caselessdict import CaselessDict from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters class vBoolean(int): """An iCalendar boolean value. Converts between iCalendar ``BOOLEAN`` value types and Python boolean values. Conforming with :rfc:`5545#section-3.3.2`, iCalendar boolean values are represented as ``TRUE`` or ``FALSE``. Values parsed from iCalendar text are case insensitive. Parameters: params: iCalendar property parameters associated with the value. Returns: A new :class:`vBoolean` instance with the supplied property parameters. Examples: Parse, create, and use iCalendar boolean values. .. code-block:: pycon >>> from icalendar import vBoolean >>> vBoolean.from_ical("TRUE") True >>> vBoolean.from_ical("fAlse") False >>> boolean = vBoolean(True) >>> if boolean: ... print("TRUE") TRUE >>> boolean.to_ical() b'TRUE' """ default_value: ClassVar[str] = "BOOLEAN" params: Parameters BOOL_MAP = CaselessDict({"true": True, "false": False}) def __new__( cls, *args: Any, params: dict[str, Any] | None = None, **kwargs: Any ) -> Self: self = super().__new__(cls, *args, **kwargs) self.params = Parameters(params) return self def to_ical(self) -> bytes: """Converts a :class:`~icalendar.prop.boolean.vBoolean` to a BOOLEAN property type. This class method takes a ``vBoolean``—a Python boolean value—and converts it to an iCalendar BOOLEAN property type, in compliance with :rfc:`5545#section-3.3.2`. Returns: Either "TRUE" or "FALSE" as bytes, depending on the value of the ``vBoolean``. """ return b"TRUE" if self else b"FALSE" @property def ical_value(self) -> bool: """BOOLEAN property type according to :rfc:`5545#section-3.3.2`""" return bool(self) @classmethod def from_ical(cls, ical: str) -> bool: try: return cls.BOOL_MAP[ical] except Exception as e: raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") from e @classmethod def examples(cls) -> list[Self]: """Examples of vBoolean.""" return [ cls(True), cls(False), ] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [name, self.params.to_jcal(), self.VALUE.lower(), bool(self)] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265` to a vBoolean. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) JCalParsingError.validate_value_type(jcal_property[3], bool, cls, 3) return cls( jcal_property[3], params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vBoolean"] collective-icalendar-138c845/src/icalendar/prop/broken.py000066400000000000000000000057041524134033600233760ustar00rootroot00000000000000"""Parsing error value preservation.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from icalendar.error import BrokenCalendarProperty from icalendar.parser_tools import DEFAULT_ENCODING from icalendar.prop.text import vText if TYPE_CHECKING: from icalendar.compatibility import Self from icalendar.parser import Parameters class vBroken(vText): """Property that failed to parse, preserving raw value as text. Represents property values that failed to parse with their expected type. The raw iCalendar string is preserved for round-trip serialization. """ default_value: ClassVar[str] = "TEXT" __slots__ = ("expected_type", "parse_error", "property_name") def __new__( cls, value: str | bytes, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, expected_type: str | None = None, property_name: str | None = None, parse_error: BaseException | None = None, ) -> Self: self = super().__new__(cls, value, encoding, params=params) self.expected_type = expected_type self.property_name = property_name self.parse_error = parse_error return self def __getattr__(self, name: str): """Raise BrokenCalendarProperty for attribute access. Attributes like ``.dt`` that the expected type would have raise :class:`~icalendar.error.BrokenCalendarProperty` with the original parse error chained. Private attributes (starting with ``_``) raise normal ``AttributeError`` to preserve ``getattr(..., default)`` patterns. """ if name.startswith("_"): raise AttributeError(name) raise BrokenCalendarProperty( f"Cannot access {name!r} on broken property " f"{self.property_name!r} (expected {self.expected_type!r}): " f"{self.parse_error}" ) from self.parse_error def __repr__(self) -> str: return ( f"{self.__class__.__name__}({str(self)!r}, " f"expected_type={self.expected_type!r}, " f"property_name={self.property_name!r})" ) @classmethod def from_parse_error( cls, raw_value: str, params: Parameters, property_name: str, expected_type: str, error: Exception, ) -> Self: """Create vBroken from parse failure.""" return cls( raw_value, params=params, expected_type=expected_type, property_name=property_name, parse_error=error, ) @classmethod def examples(cls) -> list[Self]: """Examples of vBroken.""" return [ cls( "INVALID-DATE", expected_type="date-time", property_name="DTSTART", parse_error=ValueError("Invalid date format"), ) ] __all__ = ["vBroken"] collective-icalendar-138c845/src/icalendar/prop/cal_address.py000066400000000000000000000167521524134033600243670ustar00rootroot00000000000000"""CAL-ADDRESS values from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class vCalAddress(str): r"""Calendar User Address Value Name: CAL-ADDRESS Purpose: This value type is used to identify properties that contain a calendar user address. Description: The value is a URI as defined by [RFC3986] or any other IANA-registered form for a URI. When used to address an Internet email transport address for a calendar user, the value MUST be a mailto URI, as defined by [RFC2368]. Example: ``mailto:`` is in front of the address. .. code-block:: ics mailto:jane_doe@example.com Parsing: .. code-block:: pycon >>> from icalendar import vCalAddress >>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com') >>> cal_address vCalAddress('mailto:jane_doe@example.com') Encoding: .. code-block:: pycon >>> from icalendar import vCalAddress, Event >>> event = Event() >>> jane = vCalAddress("mailto:jane_doe@example.com") >>> jane.name = "Jane" >>> event["organizer"] = jane >>> print(event.to_ical().decode().replace('\\r\\n', '\\n').strip()) BEGIN:VEVENT ORGANIZER;CN=Jane:mailto:jane_doe@example.com END:VEVENT """ default_value: ClassVar[str] = "CAL-ADDRESS" params: Parameters __slots__ = ("params",) def __new__( cls, value: str | bytes, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ) -> Self: value = to_unicode(value, encoding=encoding) if "\r" in value or "\n" in value: raise ValueError( f"A CAL-ADDRESS value may not contain CR or LF characters: {value!r}" ) self = super().__new__(cls, value) self.params = Parameters(params) return self def __repr__(self) -> str: return f"vCalAddress('{self}')" def to_ical(self) -> bytes: return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical: str | bytes) -> Self: return cls(ical) @property def ical_value(self) -> str: """The ``mailto:`` part of the address.""" return str(self) @property def email(self) -> str: """The email address without ``mailto:`` at the start.""" if self.lower().startswith("mailto:"): return self[7:] return str(self) from icalendar.param import ( CN, CUTYPE, DELEGATED_FROM, DELEGATED_TO, DIR, LANGUAGE, PARTSTAT, ROLE, RSVP, SENT_BY, VALUE, ) name = CN @staticmethod def _get_email(email: str) -> str: """Extract email and add mailto: prefix if needed. Handles case-insensitive mailto: prefix checking. Parameters: email: Email string that may or may not have mailto: prefix Returns: Email string with mailto: prefix """ if not email.lower().startswith("mailto:"): return f"mailto:{email}" return email @classmethod def new( cls, email: str, /, cn: str | None = None, cutype: str | None = None, delegated_from: str | None = None, delegated_to: str | None = None, directory: str | None = None, language: str | None = None, partstat: str | None = None, role: str | None = None, rsvp: bool | None = None, # noqa: FBT001, RUF100 sent_by: str | None = None, ) -> Self: """Create a new vCalAddress with RFC 5545 parameters. Creates a vCalAddress instance with automatic mailto: prefix handling and support for all standard RFC 5545 parameters. Parameters: email: The email address (mailto: prefix added automatically if missing) cn: Common Name parameter cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM) delegated_from: Email of the calendar user that delegated delegated_to: Email of the calendar user that was delegated to directory: Reference to directory information language: Language for text values partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.) role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR) rsvp: Whether RSVP is requested sent_by: Email of the calendar user acting on behalf of this user Returns: vCalAddress: A new calendar address with specified parameters Raises: TypeError: If email is not a string Examples: Basic usage: >>> from icalendar.prop import vCalAddress >>> addr = vCalAddress.new("test@test.com") >>> str(addr) 'mailto:test@test.com' With parameters: >>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR") >>> addr.params["CN"] 'Test User' >>> addr.params["ROLE"] 'CHAIR' """ if not isinstance(email, str): raise TypeError(f"Email must be a string, not {type(email).__name__}") # Handle mailto: prefix (case-insensitive) email_with_prefix = cls._get_email(email) # Create the address addr = cls(email_with_prefix) # Set parameters if provided if cn is not None: addr.params["CN"] = cn if cutype is not None: addr.params["CUTYPE"] = cutype if delegated_from is not None: addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from) if delegated_to is not None: addr.params["DELEGATED-TO"] = cls._get_email(delegated_to) if directory is not None: addr.params["DIR"] = directory if language is not None: addr.params["LANGUAGE"] = language if partstat is not None: addr.params["PARTSTAT"] = partstat if role is not None: addr.params["ROLE"] = role if rsvp is not None: addr.params["RSVP"] = "TRUE" if rsvp else "FALSE" if sent_by is not None: addr.params["SENT-BY"] = cls._get_email(sent_by) return addr def to_jcal(self, name: str) -> list: """Return this property in jCal format.""" return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value] @classmethod def examples(cls) -> list[Self]: """Examples of vCalAddress.""" return [cls.new("you@example.org", cn="You There")] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3) return cls( jcal_property[3], params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vCalAddress"] collective-icalendar-138c845/src/icalendar/prop/categories.py000066400000000000000000000072061524134033600242420ustar00rootroot00000000000000"""CATEGORIES property values from :rfc:`5545`.""" from collections.abc import Iterator from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import to_unicode from icalendar.prop.text import vText class vCategory: default_value: ClassVar[str] = "TEXT" params: Parameters def __init__( self, c_list: list[str] | str, /, params: dict[str, Any] | None = None ) -> None: if not hasattr(c_list, "__iter__") or isinstance(c_list, str): c_list = [c_list] self.cats: list[vText | str] = [vText(c) for c in c_list] self.params = Parameters(params) def __iter__(self) -> Iterator[vText | str]: return iter(self.cats) def to_ical(self) -> bytes: return b",".join( [ c.to_ical() if hasattr(c, "to_ical") else vText(c).to_ical() for c in self.cats ] ) @staticmethod def from_ical(ical: list[str] | str) -> list[str]: """Parse a CATEGORIES value from iCalendar format. This helper is normally called by :meth:`Component.from_ical`, which already splits the CATEGORIES property into a list of unescaped category strings. New code should therefore pass a list of strings. Passing a single comma-separated string is supported only for backwards compatibility with older parsing code and is considered legacy behavior. Parameters: ical: A list of category strings (preferred, as provided by :meth:`Component.from_ical`), or a single comma-separated string from a legacy caller. Returns: A list of category strings. """ if isinstance(ical, list): # Already split by Component.from_ical() return ical # Legacy: simple comma split (no escaping handled) ical = to_unicode(ical) return ical.split(",") def __eq__(self, other: object) -> bool: """self == other""" return isinstance(other, vCategory) and self.cats == other.cats def __hash__(self) -> int: """Hash of the vCategory object.""" return hash(tuple(self.cats)) def __repr__(self) -> str: """String representation.""" return f"{self.__class__.__name__}({self.cats}, params={self.params})" def to_jcal(self, name: str) -> list: """The jCal representation for categories.""" result = [name, self.params.to_jcal(), self.VALUE.lower()] result.extend(map(str, self.cats)) if not self.cats: result.append("") return result @classmethod def examples(cls) -> list[Self]: """Examples of vCategory.""" return [cls(["HOME", "COSY"])] from icalendar.param import VALUE @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) for i, category in enumerate(jcal_property[3:], start=3): JCalParsingError.validate_value_type(category, str, cls, i) return cls( jcal_property[3:], Parameters.from_jcal_property(jcal_property), ) @property def ical_value(self) -> list[str]: """The list of categories as strings.""" return [str(cat) for cat in self.cats] __all__ = ["vCategory"] collective-icalendar-138c845/src/icalendar/prop/conference.py000066400000000000000000000077021524134033600242250ustar00rootroot00000000000000"""Conferences according to Section 5.11 of :rfc:`7986`.""" from __future__ import annotations from dataclasses import dataclass from icalendar.prop.uri import vUri @dataclass class Conference: """Conferences according to Section 5.11 of :rfc:`7986`. Purpose: Information for accessing a conferencing system. Conformance: This property can be specified multiple times in a "VEVENT" or "VTODO" calendar component. Description: This property specifies information for accessing a conferencing system for attendees of a meeting or task. This might be for a telephone-based conference number dial-in with access codes included (such as a tel: URI :rfc:`3966` or a sip: or sips: URI :rfc:`3261`), for a web-based video chat (such as an http: or https: URI :rfc:`7230`), or for an instant messaging group chat room (such as an xmpp: URI :rfc:`5122`). If a specific URI for a conferencing system is not available, a data: URI :rfc:`2397` containing a text description can be used. A conference system can be a bidirectional communication channel or a uni-directional "broadcast feed". The "FEATURE" property parameter is used to describe the key capabilities of the conference system to allow a client to choose the ones that give the required level of interaction from a set of multiple properties. The "LABEL" property parameter is used to convey additional details on the use of the URI. For example, the URIs or access codes for the moderator and attendee of a teleconference system could be different, and the "LABEL" property parameter could be used to "tag" each "CONFERENCE" property to indicate which is which. The "LANGUAGE" property parameter can be used to specify the language used for text values used with this property (as per Section 3.2.10 of :rfc:`5545`). Example: The following are examples of this property: .. code-block:: ics CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR; LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123 CONFERENCE;VALUE=URI;FEATURE=CHAT; LABEL=Chat room:xmpp:chat-123@conference.example.com CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO; LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456 """ # see https://stackoverflow.com/a/18348004/1320237 uri: str feature: list[str] | str | None = None label: str | None = None language: str | None = None @classmethod def from_uri(cls, uri: vUri | str) -> Conference: """Create a Conference from a URI.""" params = uri.params if isinstance(uri, vUri) else {} return cls( uri, feature=params.get("feature"), label=params.get("label"), language=params.get("language"), ) def to_uri(self) -> vUri: """Convert the Conference to a vUri.""" def normalize(value: str | list[str] | None) -> str | None: if value is None or (isinstance(value, list) and len(value) == 0): return None if isinstance(value, str): return value return ",".join(value) params: dict[str, str | None] = { "FEATURE": normalize(self.feature), "LABEL": normalize(self.label), "LANGUAGE": normalize(self.language), "VALUE": "URI", } filtered_params: dict[str, str] = { key: value for key, value in params.items() if value is not None } return vUri(self.uri, params=filtered_params) __all__ = ["Conference"] collective-icalendar-138c845/src/icalendar/prop/dt/000077500000000000000000000000001524134033600221455ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/prop/dt/__init__.py000066400000000000000000000007531524134033600242630ustar00rootroot00000000000000"""Property types related to dates and times.""" from .base import TimeBase from .date import vDate from .datetime import vDatetime from .duration import vDuration from .list import vDDDLists from .period import vPeriod from .time import vTime from .types import DT_TYPE, vDDDTypes from .utc_offset import vUTCOffset __all__ = [ "DT_TYPE", "TimeBase", "vDDDLists", "vDDDTypes", "vDate", "vDatetime", "vDuration", "vPeriod", "vTime", "vUTCOffset", ] collective-icalendar-138c845/src/icalendar/prop/dt/base.py000066400000000000000000000022721524134033600234340ustar00rootroot00000000000000from datetime import date from typing import ClassVar from icalendar.parser import Parameters class TimeBase: """Make classes with a datetime/date comparable.""" default_value: ClassVar[str] params: Parameters ignore_for_equality = {"TZID", "VALUE"} def __eq__(self, other): """self == other""" if isinstance(other, date): return self.dt == other if isinstance(other, TimeBase): default = object() for key in ( set(self.params) | set(other.params) ) - self.ignore_for_equality: if key[:2].lower() != "x-" and self.params.get( key, default ) != other.params.get(key, default): return False return self.dt == other.dt from .list import vDDDLists if isinstance(other, vDDDLists): return other == self return False def __hash__(self): return hash(self.dt) from icalendar.param import RANGE, RELATED, TZID def __repr__(self): """String representation.""" return f"{self.__class__.__name__}({self.dt}, {self.params})" __all__ = ["TimeBase"] collective-icalendar-138c845/src/icalendar/prop/dt/date.py000066400000000000000000000103441524134033600234360ustar00rootroot00000000000000"""DATE property type from :rfc:`5545`.""" from datetime import date, datetime from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from .base import TimeBase class vDate(TimeBase): """Date Value Name: DATE Purpose: This value type is used to identify values that contain a calendar date. Format Definition: This value type is defined by the following notation: .. code-block:: text date = date-value date-value = date-fullyear date-month date-mday date-fullyear = 4DIGIT date-month = 2DIGIT ;01-12 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31 ;based on month/year Description: If the property permits, multiple "date" values are specified as a COMMA-separated list of values. The format for the value type is based on the [ISO.8601.2004] complete representation, basic format for a calendar date. The textual format specifies a four-digit year, two-digit month, and two-digit day of the month. There are no separator characters between the year, month, and day component text. Example: The following represents July 14, 1997: .. code-block:: ics 19970714 .. code-block:: pycon >>> from icalendar.prop import vDate >>> date = vDate.from_ical('19970714') >>> date.year 1997 >>> date.month 7 >>> date.day 14 """ default_value: ClassVar[str] = "DATE" params: Parameters def __init__(self, dt: date, params: dict[str, Any] | None = None) -> None: if not isinstance(dt, date): raise TypeError("Value MUST be a date instance") self.dt = dt self.params = Parameters(params or {}) def to_ical(self): s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}" return s.encode("utf-8") @staticmethod def from_ical(ical): # date-value = 4DIGIT 2DIGIT 2DIGIT, no separators, # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.4 if len(ical) != 8 or not ical.isascii() or not ical.isdigit(): raise ValueError(f"Wrong date format {ical}") try: timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month int(ical[6:8]), # day ) return date(*timetuple) except Exception as e: raise ValueError(f"Wrong date format {ical}") from e @classmethod def examples(cls) -> list[Self]: """Examples of vDate.""" return [cls(date(2025, 11, 10))] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [ name, self.params.to_jcal(), self.VALUE.lower(), self.dt.strftime("%Y-%m-%d"), ] @classmethod def parse_jcal_value(cls, jcal: str) -> date: """Parse a jCal string to a :class:`datetime.date`. Raises: ~error.JCalParsingError: If it can't parse a date. """ JCalParsingError.validate_value_type(jcal, str, cls) try: return datetime.strptime(jcal, "%Y-%m-%d").date() except ValueError as e: raise JCalParsingError("Cannot parse date.", cls, value=jcal) from e @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) with JCalParsingError.reraise_with_path_added(3): value = cls.parse_jcal_value(jcal_property[3]) return cls( value, params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vDate"] collective-icalendar-138c845/src/icalendar/prop/dt/datetime.py000066400000000000000000000155101524134033600243150ustar00rootroot00000000000000"""DATE-TIME property type from :rfc:`5545`.""" from datetime import datetime from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import to_unicode from icalendar.timezone import tzp from icalendar.timezone.tzid import is_utc from .base import TimeBase class vDatetime(TimeBase): """Date-Time Value Name: DATE-TIME Purpose: This value type is used to identify values that specify a precise calendar date and time of day. The format is based on the ISO.8601.2004 complete representation. Format Definition: This value type is defined by the following notation: .. code-block:: text date-time = date "T" time date = date-value date-value = date-fullyear date-month date-mday date-fullyear = 4DIGIT date-month = 2DIGIT ;01-12 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31 ;based on month/year time = time-hour time-minute time-second [time-utc] time-hour = 2DIGIT ;00-23 time-minute = 2DIGIT ;00-59 time-second = 2DIGIT ;00-60 time-utc = "Z" The following is the representation of the date-time format. .. code-block:: text YYYYMMDDTHHMMSS Description: vDatetime is timezone aware and uses a timezone library. When a vDatetime object is created from an ical string, you can pass a valid timezone identifier. When a vDatetime object is created from a Python :mod:`datetime` object, it uses the tzinfo component, if present. Otherwise a timezone-naive object is created. Be aware that there are certain limitations with timezone naive DATE-TIME components in the icalendar standard. Example: The following represents March 2, 2021 at 10:15 AM with local time: .. code-block:: pycon >>> from icalendar import vDatetime >>> datetime = vDatetime.from_ical("20210302T101500") >>> datetime.tzname() >>> datetime.year 2021 >>> datetime.minute 15 The following represents March 2, 2021 at 10:15 AM in New York: .. code-block:: pycon >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York') >>> datetime.tzname() 'EST' The following represents March 2, 2021 at 10:15 AM in Berlin: .. code-block:: pycon >>> from zoneinfo import ZoneInfo >>> timezone = ZoneInfo("Europe/Berlin") >>> vDatetime.from_ical("20210302T101500", timezone) datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin')) """ default_value: ClassVar[str] = "DATE-TIME" params: Parameters def __init__(self, dt: datetime, /, params: dict[str, Any] | None = None) -> None: self.dt = dt self.params = Parameters(params) self.params.update_tzid_from(dt) def to_ical(self): dt = self.dt s = ( f"{dt.year:04}{dt.month:02}{dt.day:02}" f"T{dt.hour:02}{dt.minute:02}{dt.second:02}" ) if self.is_utc(): s += "Z" return s.encode("utf-8") @staticmethod def from_ical(ical, timezone=None): """Create a datetime from the RFC string.""" ical = to_unicode(ical) tzinfo = None if isinstance(timezone, str): tzinfo = tzp.timezone(timezone) elif timezone is not None: tzinfo = timezone # Extract the value part if parameters are present per # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5 # Form #3: TZID=America/New_York:19980119T020000 ical_value = ical.rpartition(":")[2] if len(ical_value) < 15 or ical_value[8] != "T": raise ValueError(f"Wrong datetime format: {ical}") try: timetuple = ( int(ical_value[:4]), # year int(ical_value[4:6]), # month int(ical_value[6:8]), # day int(ical_value[9:11]), # hour int(ical_value[11:13]), # minute int(ical_value[13:15]), # second ) if tzinfo: return tzp.localize(datetime(*timetuple), tzinfo) if not ical_value[15:]: return datetime(*timetuple) if ical_value[15:] == "Z": return tzp.localize_utc(datetime(*timetuple)) except Exception as e: raise ValueError(f"Wrong datetime format: {ical}") from e raise ValueError(f"Wrong datetime format: {ical}") @classmethod def examples(cls) -> list[Self]: """Examples of vDatetime.""" return [cls(datetime(2025, 11, 10, 16, 52))] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" value = self.dt.strftime("%Y-%m-%dT%H:%M:%S") if self.is_utc(): value += "Z" return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] def is_utc(self) -> bool: """Whether this datetime is UTC.""" return self.params.is_utc() or is_utc(self.dt) @classmethod def parse_jcal_value(cls, jcal: str) -> datetime: """Parse a jCal string to a :class:`datetime.datetime`. Raises: ~error.JCalParsingError: If it can't parse a date-time value. """ JCalParsingError.validate_value_type(jcal, str, cls) utc = jcal.endswith("Z") if utc: jcal = jcal[:-1] try: dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S") except ValueError as e: raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e if utc: return tzp.localize_utc(dt) return dt @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) params = Parameters.from_jcal_property(jcal_property) with JCalParsingError.reraise_with_path_added(3): dt = cls.parse_jcal_value(jcal_property[3]) if params.tzid: dt = tzp.localize(dt, params.tzid) return cls( dt, params=params, ) __all__ = ["vDatetime"] collective-icalendar-138c845/src/icalendar/prop/dt/duration.py000066400000000000000000000151611524134033600243500ustar00rootroot00000000000000"""DURATION property type from :rfc:`5545`.""" import re from datetime import timedelta from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import InvalidCalendar, JCalParsingError from icalendar.parser import Parameters from .base import TimeBase DURATION_REGEX = re.compile( r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?\Z" ) class vDuration(TimeBase): """Duration Value Name: DURATION Purpose: This value type is used to identify properties that contain a duration of time. Format Definition: This value type is defined by the following notation: .. code-block:: text dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) dur-date = dur-day [dur-time] dur-time = "T" (dur-hour / dur-minute / dur-second) dur-week = 1*DIGIT "W" dur-hour = 1*DIGIT "H" [dur-minute] dur-minute = 1*DIGIT "M" [dur-second] dur-second = 1*DIGIT "S" dur-day = 1*DIGIT "D" Description: If the property permits, multiple "duration" values are specified by a COMMA-separated list of values. The format is based on the [ISO.8601.2004] complete representation basic format with designators for the duration of time. The format can represent nominal durations (weeks and days) and accurate durations (hours, minutes, and seconds). Note that unlike [ISO.8601.2004], this value type doesn't support the "Y" and "M" designators to specify durations in terms of years and months. The duration of a week or a day depends on its position in the calendar. In the case of discontinuities in the time scale, such as the change from standard time to daylight time and back, the computation of the exact duration requires the subtraction or addition of the change of duration of the discontinuity. Leap seconds MUST NOT be considered when computing an exact duration. When computing an exact duration, the greatest order time components MUST be added first, that is, the number of days MUST be added first, followed by the number of hours, number of minutes, and number of seconds. Example: A duration of 15 days, 5 hours, and 20 seconds would be: .. code-block:: ics P15DT5H0M20S A duration of 7 weeks would be: .. code-block:: ics P7W .. code-block:: pycon >>> from icalendar.prop import vDuration >>> duration = vDuration.from_ical('P15DT5H0M20S') >>> duration datetime.timedelta(days=15, seconds=18020) >>> duration = vDuration.from_ical('P7W') >>> duration datetime.timedelta(days=49) """ default_value: ClassVar[str] = "DURATION" params: Parameters def __init__( self, td: timedelta | str, /, params: dict[str, Any] | None = None ) -> None: if isinstance(td, str): td = vDuration.from_ical(td) if not isinstance(td, timedelta): raise TypeError("Value MUST be a timedelta instance") self.td = td self.params = Parameters(params) def to_ical(self): sign = "" td = self.td if td.days < 0: sign = "-" td = -td timepart = "" if td.seconds: timepart = "T" hours = td.seconds // 3600 minutes = td.seconds % 3600 // 60 seconds = td.seconds % 60 if hours: timepart += f"{hours}H" if minutes or (hours and seconds): timepart += f"{minutes}M" if seconds: timepart += f"{seconds}S" if td.days == 0 and timepart: return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8") return ( str(sign).encode("utf-8") + b"P" + str(abs(td.days)).encode("utf-8") + b"D" + str(timepart).encode("utf-8") ) @staticmethod def from_ical(ical): match = DURATION_REGEX.match(ical) if not match: raise InvalidCalendar(f"Invalid iCalendar duration: {ical}") sign, weeks, days, hours, minutes, seconds = match.groups() try: value = timedelta( weeks=int(weeks or 0), days=int(days or 0), hours=int(hours or 0), minutes=int(minutes or 0), seconds=int(seconds or 0), ) except OverflowError as e: # ``timedelta`` rejects values that are too large for its C # implementation. Raise the same error as other invalid durations # instead of leaking ``OverflowError`` to callers. raise InvalidCalendar(f"Impractical iCalendar duration: {ical}") from e if sign == "-": value = -value return value @property def dt(self) -> timedelta: """The time delta for compatibility.""" return self.td @classmethod def examples(cls) -> list[Self]: """Examples of vDuration.""" return [cls(timedelta(1, 99))] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [ name, self.params.to_jcal(), self.VALUE.lower(), self.to_ical().decode(), ] @classmethod def parse_jcal_value(cls, jcal: str) -> timedelta: """Parse a jCal string to a :class:`datetime.timedelta`. Raises: ~error.JCalParsingError: If it can't parse a duration.""" JCalParsingError.validate_value_type(jcal, str, cls) try: return cls.from_ical(jcal) except (ValueError, InvalidCalendar) as e: raise JCalParsingError("Cannot parse duration.", cls, value=jcal) from e @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) with JCalParsingError.reraise_with_path_added(3): duration = cls.parse_jcal_value(jcal_property[3]) return cls( duration, Parameters.from_jcal_property(jcal_property), ) __all__ = ["vDuration"] collective-icalendar-138c845/src/icalendar/prop/dt/list.py000066400000000000000000000064471524134033600235050ustar00rootroot00000000000000from collections.abc import Sequence from datetime import date, datetime from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import from_unicode from .base import TimeBase from .types import vDDDTypes class vDDDLists: """A list of vDDDTypes values.""" default_value: ClassVar[str] = "DATE-TIME" params: Parameters dts: list[vDDDTypes] def __init__(self, dt_list, params: dict[str, Any] | None = None) -> None: if params is None: params = {} if not hasattr(dt_list, "__iter__") or ( isinstance(dt_list, Sequence) and len(dt_list) == 2 and dt_list[1] is None ): # A ``(dt, None)`` pair (tuple or list) is the rdates form of a single # date, not a list of two values, so don't iterate it element-wise (#1439). dt_list = [dt_list] vddd = [] tzid = None for dt_l in dt_list: dt = vDDDTypes(dt_l) if not isinstance(dt_l, vDDDTypes) else dt_l vddd.append(dt) if "TZID" in dt.params: tzid = dt.params["TZID"] if tzid: # NOTE: no support for multiple timezones here! params["TZID"] = tzid self.params = Parameters(params) self.dts = vddd def to_ical(self): dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts) return b",".join(dts_ical) @staticmethod def from_ical(ical, timezone=None): out = [] ical_dates = ical.split(",") for ical_dt in ical_dates: out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone)) return out def __eq__(self, other): if isinstance(other, vDDDLists): return self.dts == other.dts if isinstance(other, (TimeBase, date)): return self.dts == [other] return False def __repr__(self): """String representation.""" return f"{self.__class__.__name__}({self.dts})" @classmethod def examples(cls) -> list[Self]: """Examples of vDDDLists.""" return [vDDDLists([datetime(2025, 11, 10, 16, 50)])] def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [ name, self.params.to_jcal(), self.VALUE.lower(), *[dt.to_jcal(name)[3] for dt in self.dts], ] def _get_value(self) -> str | None: return None if not self.dts else self.dts[0].VALUE from icalendar.param import VALUE @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the jCal provided is invalid. """ JCalParsingError.validate_property(jcal_property, cls) values = jcal_property[3:] prop = jcal_property[:3] dts = [] for value in values: dts.append(vDDDTypes.from_jcal(prop + [value])) return cls( dts, params=Parameters.from_jcal_property(jcal_property), ) __hash__ = None __all__ = ["vDDDLists"] collective-icalendar-138c845/src/icalendar/prop/dt/period.py000066400000000000000000000250671524134033600240130ustar00rootroot00000000000000"""PERIOD property type from :rfc:`5545`.""" from datetime import date, datetime, timedelta, tzinfo from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.timezone import tzp from icalendar.tools import is_date, is_datetime, is_pytz, normalize_pytz, to_datetime from .base import TimeBase from .datetime import vDatetime from .duration import vDuration def _to_midnight(dt: date, tz: tzinfo | None) -> datetime: """Convert a date to the datetime at midnight. Parameters: dt: The date to convert. tz: The timezone of the result, or ``None`` for a naive result. Returns: The datetime at midnight. """ midnight = to_datetime(dt) if tz is None: return midnight if is_pytz(tz): return tz.localize(midnight) # type: ignore[attr-defined] return midnight.replace(tzinfo=tz) def _to_period_datetimes( start: date | datetime, end_or_duration: date | datetime | timedelta ) -> tuple[datetime, datetime | timedelta]: """Convert the dates of a period to datetimes. :rfc:`5545#section-3.3.9` builds a period from datetimes only, but calendars in the wild use dates. A date becomes midnight so that the period can be used and written back out. A converted half takes the timezone of the other half. Without this, an aware and a naive datetime could not be subtracted to compute the duration. Parameters: start: The start of the period. end_or_duration: The end of the period or its duration. Returns: The start and the end or duration, with any date converted. """ if is_date(start): start = _to_midnight(start, getattr(end_or_duration, "tzinfo", None)) if is_date(end_or_duration): end_or_duration = _to_midnight(end_or_duration, getattr(start, "tzinfo", None)) return start, end_or_duration class vPeriod(TimeBase): """A span of time, written either as a start and an end or as a start and a duration. The value is a tuple of two :class:`datetime.datetime` objects, or of a datetime and a :class:`datetime.timedelta`. Whichever way it was written, :attr:`start`, :attr:`end`, and :attr:`duration` are all available, and :attr:`by_duration` says which of the two forms is written back out. Conforming with :rfc:`5545#section-3.3.9`, a period is built from datetimes only, the start must come before the end, and a duration must be positive. A half written as a date does not conform. Such a half is read as midnight, in the timezone of the other half where it has one, so that a calendar that gets this wrong can still be read and written. ``FREEBUSY`` holds periods directly. ``RDATE`` holds them through :class:`~icalendar.prop.dt.list.vDDDLists`, which splits a comma separated list and hands each value to :class:`~icalendar.prop.dt.types.vDDDTypes`. The halves themselves are parsed by :class:`~icalendar.prop.dt.datetime.vDatetime` and :class:`~icalendar.prop.dt.duration.vDuration`. Parameters: per: The start of the period, and either its end or its duration. params: The parameters of the property. Raises: TypeError: If the start is not a date or a datetime, or if the end is not a date, a datetime, or a duration. ValueError: If the start is after the end. Examples: A period from 18:00:00 UTC on January 1, 1997 to 07:00:00 UTC on January 2, 1997, and one that starts at 18:00:00 UTC and lasts 5 hours and 30 minutes: .. code-block:: ics 19970101T180000Z/19970102T070000Z 19970101T180000Z/PT5H30M .. code-block:: pycon >>> from icalendar.prop import vPeriod >>> period = vPeriod.from_ical("19970101T180000Z/19970102T070000Z") >>> vPeriod(period).to_ical() b'19970101T180000Z/19970102T070000Z' >>> period = vPeriod.from_ical("19970101T180000Z/PT5H30M") >>> vPeriod(period).duration datetime.timedelta(seconds=19800) A half written as a date is read as midnight: .. code-block:: pycon >>> vPeriod(vPeriod.from_ical("19970101/19970102")).to_ical() b'19970101T000000/19970102T000000' .. versionchanged:: 7.2.3 A period written with dates is read as midnight. """ default_value: ClassVar[str] = "PERIOD" params: Parameters #: Whether the value is written as a duration rather than as an end. by_duration: bool #: The start of the period. start: datetime #: The end of the period, computed from the duration where there is one. end: datetime #: The time between the start and the end. duration: timedelta def __init__( self, per: tuple[date | datetime, date | datetime | timedelta], params: dict[str, Any] | None = None, ) -> None: start, end_or_duration = per if not (isinstance(start, (datetime, date))): raise TypeError("Start value MUST be a datetime or date instance") if not (isinstance(end_or_duration, (datetime, date, timedelta))): raise TypeError( "end_or_duration MUST be a datetime, date or timedelta instance" ) start, end_or_duration = _to_period_datetimes(start, end_or_duration) by_duration = isinstance(end_or_duration, timedelta) if by_duration: duration = end_or_duration end = normalize_pytz(start + duration) else: end = end_or_duration duration = normalize_pytz(end - start) if start > end: raise ValueError("Start time is greater than end time") self.params = Parameters(params or {"value": "PERIOD"}) # set the timezone identifier # does not support different timezones for start and end self.params.update_tzid_from(start) self.start = start self.end = end self.by_duration = by_duration self.duration = duration def overlaps(self, other): if self.start > other.start: return other.overlaps(self) return self.start <= other.start < self.end def to_ical(self): if self.by_duration: return ( vDatetime(self.start).to_ical() + b"/" + vDuration(self.duration).to_ical() ) return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical() @staticmethod def from_ical(ical, timezone=None): from icalendar.prop.dt.types import vDDDTypes try: start, end_or_duration = ical.split("/") start = vDDDTypes.from_ical(start, timezone=timezone) end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone) except Exception as e: raise ValueError(f"Expected period format, got: {ical}") from e return _to_period_datetimes(start, end_or_duration) def __repr__(self): p = (self.start, self.duration) if self.by_duration else (self.start, self.end) return f"vPeriod({p!r})" @property def dt(self): """Make this cooperate with the other vDDDTypes.""" return (self.start, (self.duration if self.by_duration else self.end)) @property def ical_value(self) -> tuple[datetime, timedelta | datetime]: """ Returns the period as a tuple of its start datetime and either its end datetime or duration. """ return self.dt from icalendar.param import FBTYPE @classmethod def examples(cls) -> list[Self]: """Examples of vPeriod.""" return [ vPeriod((datetime(2025, 11, 10, 16, 35), timedelta(hours=1, minutes=30))), vPeriod((datetime(2025, 11, 10, 16, 35), datetime(2025, 11, 10, 18, 5))), ] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" value = [vDatetime(self.start).to_jcal(name)[-1]] if self.by_duration: value.append(vDuration(self.duration).to_jcal(name)[-1]) else: value.append(vDatetime(self.end).to_jcal(name)[-1]) return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] @classmethod def parse_jcal_value( cls, jcal: str | list ) -> tuple[datetime, datetime] | tuple[datetime, timedelta]: """Parse a jCal value. Raises: ~error.JCalParsingError: If the period is not a list with exactly two items, or it can't parse a date-time or duration. """ if isinstance(jcal, str) and "/" in jcal: # only occurs in the example of RFC7265, Section B.2.2. jcal = jcal.split("/") if not isinstance(jcal, list) or len(jcal) != 2: raise JCalParsingError( "A period must be a list with exactly 2 items.", cls, value=jcal ) with JCalParsingError.reraise_with_path_added(0): start = vDatetime.parse_jcal_value(jcal[0]) with JCalParsingError.reraise_with_path_added(1): JCalParsingError.validate_value_type(jcal[1], str, cls) if jcal[1].startswith(("P", "-P", "+P")): end_or_duration = vDuration.parse_jcal_value(jcal[1]) else: try: end_or_duration = vDatetime.parse_jcal_value(jcal[1]) except JCalParsingError as e: raise JCalParsingError( "Cannot parse date-time or duration.", cls, value=jcal[1], ) from e return start, end_or_duration @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) with JCalParsingError.reraise_with_path_added(3): start, end_or_duration = cls.parse_jcal_value(jcal_property[3]) params = Parameters.from_jcal_property(jcal_property) tzid = params.tzid if tzid: start = tzp.localize(start, tzid) if is_datetime(end_or_duration): end_or_duration = tzp.localize(end_or_duration, tzid) return cls((start, end_or_duration), params=params) __all__ = ["vPeriod"] collective-icalendar-138c845/src/icalendar/prop/dt/time.py000066400000000000000000000216431524134033600234630ustar00rootroot00000000000000"""TIME property type from :rfc:`5545`.""" import re from datetime import datetime, time, timezone, tzinfo from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.timezone import tzp from icalendar.timezone.tzid import is_utc from .base import TimeBase TIME_JCAL_REGEX = re.compile( r"^(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9]{2})(?PZ)?\Z" ) class vTime(TimeBase): """Time Value Name: TIME Purpose: This value type is used to identify values that contain a time of day. Format Definition: This value type is defined by the following notation: .. code-block:: text time = time-hour time-minute time-second [time-utc] time-hour = 2DIGIT ;00-23 time-minute = 2DIGIT ;00-59 time-second = 2DIGIT ;00-60 ;The "60" value is used to account for positive "leap" seconds. time-utc = "Z" Description: If the property permits, multiple "time" values are specified by a COMMA-separated list of values. No additional content value encoding (i.e., BACKSLASH character encoding, see vText) is defined for this value type. The "TIME" value type is used to identify values that contain a time of day. The format is based on the [ISO.8601.2004] complete representation, basic format for a time of day. The text format consists of a two-digit, 24-hour of the day (i.e., values 00-23), two-digit minute in the hour (i.e., values 00-59), and two-digit seconds in the minute (i.e., values 00-60). The seconds value of 60 MUST only be used to account for positive "leap" seconds. Fractions of a second are not supported by this format. In parallel to the "DATE-TIME" definition above, the "TIME" value type expresses time values in three forms: The form of time with UTC offset MUST NOT be used. For example, the following is not valid for a time value: .. code-block:: ics 230000-0800 ;Invalid time format **FORM #1 LOCAL TIME** The local time form is simply a time value that does not contain the UTC designator nor does it reference a time zone. For example, 11:00 PM: .. code-block:: ics 230000 Time values of this type are said to be "floating" and are not bound to any time zone in particular. They are used to represent the same hour, minute, and second value regardless of which time zone is currently being observed. For example, an event can be defined that indicates that an individual will be busy from 11:00 AM to 1:00 PM every day, no matter which time zone the person is in. In these cases, a local time can be specified. The recipient of an iCalendar object with a property value consisting of a local time, without any relative time zone information, SHOULD interpret the value as being fixed to whatever time zone the "ATTENDEE" is in at any given moment. This means that two "Attendees", may participate in the same event at different UTC times; floating time SHOULD only be used where that is reasonable behavior. In most cases, a fixed time is desired. To properly communicate a fixed time in a property value, either UTC time or local time with time zone reference MUST be specified. The use of local time in a TIME value without the "TZID" property parameter is to be interpreted as floating time, regardless of the existence of "VTIMEZONE" calendar components in the iCalendar object. **FORM #2: UTC TIME** UTC time, or absolute time, is identified by a LATIN CAPITAL LETTER Z suffix character, the UTC designator, appended to the time value. For example, the following represents 07:00 AM UTC: .. code-block:: ics 070000Z The "TZID" property parameter MUST NOT be applied to TIME properties whose time values are specified in UTC. **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE** The local time with reference to time zone information form is identified by the use the "TZID" property parameter to reference the appropriate time zone definition. Example: The following represents 8:30 AM in New York in winter, five hours behind UTC, in each of the three formats: .. code-block:: ics 083000 133000Z TZID=America/New_York:083000 """ default_value: ClassVar[str] = "TIME" params: Parameters def __init__(self, *args: Any, params: dict[str, Any] | None = None) -> None: if len(args) == 1: if not isinstance(args[0], (time, datetime)): raise ValueError(f"Expected a datetime.time, got: {args[0]}") self.dt = args[0] else: self.dt = time(*args) self.params = Parameters(params or {}) self.params.update_tzid_from(self.dt) def to_ical(self): value = self.dt.strftime("%H%M%S") if self.is_utc(): value += "Z" return value def is_utc(self) -> bool: """Whether this time is UTC.""" return self.params.is_utc() or is_utc(self.dt) @staticmethod def from_ical(ical: str, timezone: str | None | tzinfo = None) -> time: """Convert an ical string into a time. This method supports parsing the three forms of time values defined in :rfc:`5545#section-3.3.12`: - Local time (floating) - UTC time - Local time with time zone reference Returns: A :class:`datetime.time` object representing the parsed time, with timezone information if applicable. Raises: ValueError: if the provided string cannot be parsed as a time. """ tzinfo = None if isinstance(timezone, str): tzinfo = tzp.timezone(timezone) elif timezone is not None: tzinfo = timezone if isinstance(ical, bytes): ical = ical.decode() # Extract the value part if parameters are present per # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5 # Form #3: TZID=America/New_York:083000 ical = ical.rpartition(":")[2] if utc := ical.endswith("Z"): ical = ical[:-1] # time = time-hour time-minute time-second [time-utc], six digits, # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.12 if len(ical) != 6 or not ical.isascii() or not ical.isdigit(): raise ValueError(f"Expected time, got: {ical}") try: timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) if tzinfo: return tzp.localize(time(*timetuple), tzinfo) if utc: return tzp.localize_utc(time(*timetuple)) return time(*timetuple) except Exception as e: raise ValueError(f"Expected time, got: {ical}") from e @classmethod def examples(cls) -> list[Self]: """Examples of vTime.""" return [cls(time(12, 30))] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" value = self.dt.strftime("%H:%M:%S") if self.is_utc(): value += "Z" return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] @classmethod def parse_jcal_value(cls, jcal: str) -> time: """Parse a jCal string to a :class:`datetime.time`. Raises: ~error.JCalParsingError: If it can't parse a time. """ JCalParsingError.validate_value_type(jcal, str, cls) match = TIME_JCAL_REGEX.match(jcal) if match is None: raise JCalParsingError("Cannot parse time.", cls, value=jcal) hour = int(match.group("hour")) minute = int(match.group("minute")) second = int(match.group("second")) utc = bool(match.group("utc")) return time(hour, minute, second, tzinfo=timezone.utc if utc else None) @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) with JCalParsingError.reraise_with_path_added(3): value = cls.parse_jcal_value(jcal_property[3]) return cls( value, params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vTime"] collective-icalendar-138c845/src/icalendar/prop/dt/types.py000066400000000000000000000150711524134033600236670ustar00rootroot00000000000000"""Combined property type for the date and time properties from :rfc:`5545`.""" from __future__ import annotations from collections.abc import Sequence from datetime import date, datetime, time, timedelta from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.timezone import tzp from icalendar.tools import is_date, is_datetime, to_datetime from .base import TimeBase from .date import vDate from .datetime import vDatetime from .duration import vDuration from .period import vPeriod from .time import vTime if TYPE_CHECKING: from icalendar.compatibility import Self DT_TYPE: TypeAlias = ( datetime | date | timedelta | time | tuple[datetime, datetime] | tuple[datetime, timedelta] ) class vDDDTypes(TimeBase): """A combined Datetime, Date or Duration parser/generator. Their format cannot be confused, and often values can be of either types. So this is practical. """ default_value: ClassVar[str] = "DATE-TIME" params: Parameters dt: DT_TYPE def __init__(self, dt: DT_TYPE, params: dict[str, Any] | None = None) -> None: if params is None: params = {} if isinstance(dt, Sequence) and len(dt) == 2 and dt[1] is None: # ``(dt, None)`` (tuple or list) is the rdates form of a single date; # the ``[1] is None`` guard skips strings and real two-value pairs (#1439). dt = dt[0] if not isinstance(dt, (datetime, date, timedelta, time, tuple)): raise TypeError( "You must use datetime, date, timedelta, time or tuple (for periods)" ) self.dt = dt if is_date(dt): params.update({"value": "DATE"}) elif isinstance(dt, time): params.update({"value": "TIME"}) elif isinstance(dt, tuple): params.update({"value": "PERIOD"}) self.params = Parameters(params) self.params.update_tzid_from(dt) def to_property_type(self) -> vDatetime | vDate | vDuration | vTime | vPeriod: """Convert to a property type. Raises: ValueError: If the type is unknown. """ dt = self.dt if isinstance(dt, datetime): result = vDatetime(dt) elif isinstance(dt, date): result = vDate(dt) elif isinstance(dt, timedelta): result = vDuration(dt) elif isinstance(dt, time): result = vTime(dt) elif isinstance(dt, tuple) and len(dt) == 2: result = vPeriod(dt) else: raise ValueError(f"Unknown date type: {type(dt)}") result.params = self.params return result def to_ical(self) -> bytes: """Return the ical representation.""" return self.to_property_type().to_ical() @classmethod def from_ical(cls, ical, timezone=None): if isinstance(ical, cls): return ical.dt u = ical.upper() if u.startswith(("P", "-P", "+P")): return vDuration.from_ical(ical) if "/" in u: return vPeriod.from_ical(ical, timezone=timezone) if len(ical) in (15, 16): return vDatetime.from_ical(ical, timezone=timezone) if len(ical) == 8: if timezone: tzinfo = tzp.timezone(timezone) if tzinfo is not None: # ``replace`` picks the first offset a pytz timezone knows, # which is the local mean time one, so localize instead. return tzp.localize(to_datetime(vDate.from_ical(ical)), tzinfo) return vDate.from_ical(ical) if len(ical) in (6, 7): return vTime.from_ical(ical, timezone=timezone) raise ValueError(f"Expected datetime, date, or time. Got: '{ical}'") @property def td(self) -> timedelta: """Compatibility property returning ``self.dt``. This class is used to replace different time components. Some of them contain a datetime or date (``.dt``). Some of them contain a timedelta (``.td``). This property allows interoperability. """ return self.dt @property def dts(self) -> list: """Compatibility method to return a list of datetimes.""" return [self] @classmethod def examples(cls) -> list[vDDDTypes]: """Examples of vDDDTypes.""" return [cls(date(2025, 11, 10)), cls(timedelta(days=1, hours=5))] def _get_value(self) -> str | None: """Determine the VALUE parameter.""" return self.to_property_type().VALUE from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return self.to_property_type().to_jcal(name) @classmethod def parse_jcal_value(cls, jcal: str | list) -> timedelta: """Parse a jCal value. Raises: ~error.JCalParsingError: If the value can't be parsed as either a date, time, date-time, duration, or period. """ if isinstance(jcal, list): return vPeriod.parse_jcal_value(jcal) JCalParsingError.validate_value_type(jcal, str, cls) if "/" in jcal: return vPeriod.parse_jcal_value(jcal) for jcal_type in (vDatetime, vDate, vTime, vDuration): try: return jcal_type.parse_jcal_value(jcal) except JCalParsingError: pass raise JCalParsingError( "Cannot parse date, time, date-time, duration, or period.", cls, value=jcal ) @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) with JCalParsingError.reraise_with_path_added(3): dt = cls.parse_jcal_value(jcal_property[3]) params = Parameters.from_jcal_property(jcal_property) if params.tzid: if isinstance(dt, tuple): # period start = tzp.localize(dt[0], params.tzid) end = tzp.localize(dt[1], params.tzid) if is_datetime(dt[1]) else dt[1] dt = (start, end) else: dt = tzp.localize(dt, params.tzid) return cls( dt, params=params, ) __all__ = ["DT_TYPE", "vDDDTypes"] collective-icalendar-138c845/src/icalendar/prop/dt/utc_offset.py000066400000000000000000000132421524134033600246620ustar00rootroot00000000000000"""UTC-Offset property type from :rfc:`5545`.""" import re from datetime import timedelta from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters UTC_OFFSET_JCAL_REGEX = re.compile( r"^(?P[+-])?(?P\d\d):(?P\d\d)(?::(?P\d\d))?\Z" ) class vUTCOffset: """UTC Offset Value Name: UTC-OFFSET Purpose: This value type is used to identify properties that contain an offset from UTC to local time. Format Definition: This value type is defined by the following notation: .. code-block:: text utc-offset = time-numzone time-numzone = ("+" / "-") time-hour time-minute [time-second] Description: The PLUS SIGN character MUST be specified for positive UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST be specified for negative UTC offsets (i.e., behind of UTC). The value of "-0000" and "-000000" are not allowed. The time-second, if present, MUST NOT be 60; if absent, it defaults to zero. Example: The following UTC offsets are given for standard time for New York (five hours behind UTC) and Geneva (one hour ahead of UTC): .. code-block:: ics -0500 +0100 .. code-block:: pycon >>> from icalendar.prop import vUTCOffset >>> utc_offset = vUTCOffset.from_ical('-0500') >>> utc_offset datetime.timedelta(days=-1, seconds=68400) >>> utc_offset = vUTCOffset.from_ical('+0100') >>> utc_offset datetime.timedelta(seconds=3600) """ default_value: ClassVar[str] = "UTC-OFFSET" params: Parameters ignore_exceptions = False # if True, and we cannot parse this # component, we will silently ignore # it, rather than let the exception # propagate upwards def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None) -> None: if not isinstance(td, timedelta): raise TypeError("Offset value MUST be a timedelta instance") self.td = td self.params = Parameters(params) def to_ical(self) -> str: """Return the ical representation.""" return self.format("") def format(self, divider: str = "") -> str: """Represent the value with a possible divider. .. code-block:: pycon >>> from icalendar import vUTCOffset >>> from datetime import timedelta >>> utc_offset = vUTCOffset(timedelta(hours=-5)) >>> utc_offset.format() '-0500' >>> utc_offset.format(divider=':') '-05:00' """ if self.td < timedelta(0): sign = "-%s" td = timedelta(0) - self.td # get timedelta relative to 0 else: # Google Calendar rejects '0000' but accepts '+0000' sign = "+%s" td = self.td days, seconds = td.days, td.seconds hours = abs(days * 24 + seconds // 3600) minutes = abs((seconds % 3600) // 60) seconds = abs(seconds % 60) if seconds: duration = f"{hours:02}{divider}{minutes:02}{divider}{seconds:02}" else: duration = f"{hours:02}{divider}{minutes:02}" return sign % duration @classmethod def from_ical(cls, ical): if isinstance(ical, cls): return ical.td try: sign, hours, minutes, seconds = ( ical[0:1], int(ical[1:3]), int(ical[3:5]), int(ical[5:7] or 0), ) offset = timedelta(hours=hours, minutes=minutes, seconds=seconds) except Exception as e: raise ValueError(f"Expected UTC offset, got: {ical}") from e if not cls.ignore_exceptions and offset >= timedelta(hours=24): raise ValueError(f"Offset must be less than 24 hours, was {ical}") if sign == "-": return -offset return offset def __eq__(self, other): if not isinstance(other, vUTCOffset): return False return self.td == other.td def __hash__(self): return hash(self.td) def __repr__(self): return f"vUTCOffset({self.td!r})" @classmethod def examples(cls) -> list[Self]: """Examples of vUTCOffset.""" return [ cls(timedelta(hours=3)), cls(timedelta(0)), ] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3]) if match is None: raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.") negative = match.group("sign") == "-" hours = int(match.group("hours")) minutes = int(match.group("minutes")) seconds = int(match.group("seconds") or 0) t = timedelta(hours=hours, minutes=minutes, seconds=seconds) if negative: t = -t return cls(t, Parameters.from_jcal_property(jcal_property)) __all__ = ["vUTCOffset"] collective-icalendar-138c845/src/icalendar/prop/factory.py000066400000000000000000000241521524134033600235630ustar00rootroot00000000000000"""Factory class for all the property types.""" from __future__ import annotations from typing import Any, ClassVar from icalendar.caselessdict import CaselessDict from icalendar.prop.adr import vAdr from icalendar.prop.binary import vBinary from icalendar.prop.boolean import vBoolean from icalendar.prop.cal_address import vCalAddress from icalendar.prop.categories import vCategory from icalendar.prop.dt import ( vDate, vDatetime, vDDDLists, vDDDTypes, vDuration, vPeriod, vTime, vUTCOffset, ) from icalendar.prop.float import vFloat from icalendar.prop.geo import vGeo from icalendar.prop.inline import vInline from icalendar.prop.n import vN from icalendar.prop.org import vOrg from icalendar.prop.recur import vFrequency, vRecur, vWeekday from icalendar.prop.text import vText from icalendar.prop.uid import vUid from icalendar.prop.unknown import vUnknown from icalendar.prop.uri import vUri from icalendar.prop.xml_reference import vXmlReference from .integer import vInt class TypesFactory(CaselessDict): """Factory for all value types defined in :rfc:`5545` and subsequent. The value and parameter names don't overlap. So one factory is enough for both kinds. """ _instance: ClassVar[TypesFactory | None] = None def instance() -> TypesFactory: """Return a singleton instance of this class.""" if TypesFactory._instance is None: TypesFactory._instance = TypesFactory() return TypesFactory._instance def __init__(self, *args: Any, **kwargs: Any) -> None: """Set keys to upper for initial dict""" super().__init__(*args, **kwargs) self.all_types = ( vBinary, vBoolean, vCalAddress, vDDDLists, vDDDTypes, vDate, vDatetime, vDuration, vFloat, vFrequency, vGeo, vInline, vInt, vPeriod, vRecur, vText, vTime, vUTCOffset, vUri, vWeekday, vCategory, vAdr, vN, vOrg, vUid, vXmlReference, vUnknown, ) self["binary"] = vBinary self["boolean"] = vBoolean self["cal-address"] = vCalAddress self["date"] = vDDDTypes self["date-time"] = vDDDTypes self["duration"] = vDDDTypes self["float"] = vFloat self["integer"] = vInt self["period"] = vPeriod self["recur"] = vRecur self["text"] = vText self["time"] = vTime self["uri"] = vUri self["utc-offset"] = vUTCOffset self["geo"] = vGeo self["inline"] = vInline self["date-time-list"] = vDDDLists self["categories"] = vCategory self["adr"] = vAdr # RFC 6350 vCard self["n"] = vN # RFC 6350 vCard self["org"] = vOrg # RFC 6350 vCard self["unknown"] = vUnknown # RFC 7265 self["uid"] = vUid # RFC 9253 self["xml-reference"] = vXmlReference # RFC 9253 ################################################# # Property types # These are the default types types_map = CaselessDict( { #################################### # Property value types # Calendar Properties "calscale": "text", "method": "text", "prodid": "text", "version": "text", # Descriptive Component Properties "attach": "uri", "categories": "categories", "class": "text", # vCard Properties (RFC 6350) "adr": "adr", "n": "n", "org": "org", "comment": "text", "description": "text", "geo": "geo", "location": "text", "percent-complete": "integer", "priority": "integer", "resources": "text", "status": "text", "summary": "text", # RFC 9253 # link should be uri, xml-reference or uid # uri is likely most helpful if people forget to set VALUE "link": "uri", "concept": "uri", "refid": "text", # Date and Time Component Properties "completed": "date-time", "dtend": "date-time", "due": "date-time", "dtstart": "date-time", "duration": "duration", "freebusy": "period", "transp": "text", "refresh-interval": "duration", # RFC 7986 # Time Zone Component Properties "tzid": "text", "tzname": "text", "tzoffsetfrom": "utc-offset", "tzoffsetto": "utc-offset", "tzurl": "uri", # Relationship Component Properties "attendee": "cal-address", "contact": "text", "organizer": "cal-address", "recurrence-id": "date-time", "related-to": "text", "url": "uri", "conference": "uri", # RFC 7986 "image": "unknown", # RFC 7986 has no default value type "source": "uri", "uid": "text", # Recurrence Component Properties "exdate": "date-time-list", "exrule": "recur", "rdate": "date-time-list", "rrule": "recur", # Alarm Component Properties "action": "text", "repeat": "integer", "trigger": "duration", "acknowledged": "date-time", "proximity": "text", # RFC 9074 # Change Management Component Properties "created": "date-time", "dtstamp": "date-time", "last-modified": "date-time", "sequence": "integer", # Miscellaneous Component Properties "request-status": "text", #################################### # parameter types (luckily there is no name overlap) "altrep": "uri", "cn": "text", "cutype": "text", "delegated-from": "cal-address", "delegated-to": "cal-address", "dir": "uri", "encoding": "text", "fmttype": "text", "fbtype": "text", "language": "text", "member": "cal-address", "partstat": "text", "range": "text", "related": "text", "reltype": "text", "role": "text", "rsvp": "boolean", "sent-by": "cal-address", "value": "text", # rfc 9253 parameters "label": "text", "linkrel": "text", "gap": "duration", } ) def for_property(self, name, value_param: str | None = None) -> type: """Returns the type class for a property or parameter. Parameters: name: Property or parameter name value_param: Optional ``VALUE`` parameter, for example, "DATE", "DATE-TIME", or other string. Returns: The appropriate value type class. """ # Special case: RDATE and EXDATE always use vDDDLists to support list values # regardless of the VALUE parameter if name.upper() in ("RDATE", "EXDATE"): return self["date-time-list"] # Only use VALUE parameter for known properties that support multiple value # types (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME) # For unknown/custom properties, always use the default type from types_map if value_param and name in self.types_map and value_param in self: return self[value_param] if value_param and (value_param in self) and value_param != "IMAGE": return self[value_param] return self[self.types_map.get(name, "unknown")] #: jCal value types defined by :rfc:`7265` that are used verbatim as the #: type field. Internal type keys outside of this set (for example #: ``"categories"`` or ``"date-time-list"``) are mapped to their concrete #: jCal type via the value class. _jcal_value_types = frozenset( { "binary", "boolean", "cal-address", "date", "date-time", "duration", "float", "integer", "period", "recur", "text", "time", "uri", "utc-offset", "unknown", } ) def default_value_type(self, name: str) -> str: """Return the default jCal value type for a property. The result is a lowercase :rfc:`7265` value type, for example ``"date-time"`` for ``DTSTART`` or ``"duration"`` for ``TRIGGER``. This is the value type a property uses when no explicit ``VALUE`` parameter is given, so it tells :func:`Component.from_jcal` whether a ``VALUE`` parameter has to be restored from the jCal type field. Parameters: name: Property name. Returns: The default jCal value type as a lowercase string. """ internal = self.types_map.get(name.lower(), "unknown") if internal in self._jcal_value_types: return internal # Internal keys such as ``categories`` or ``date-time-list`` are not # jCal value types themselves; ask the value class for the type it # actually serialises to. try: return self[internal].examples()[0].VALUE.lower() except (KeyError, IndexError, AttributeError): return "unknown" def to_ical(self, name, value): """Encodes a named value from a primitive python type to an icalendar encoded string. """ type_class = self.for_property(name) return type_class(value).to_ical() def from_ical(self, name, value): """Decodes a named property or parameter value from an icalendar encoded string to a primitive python type. """ type_class = self.for_property(name) return type_class.from_ical(value) __all__ = ["TypesFactory"] collective-icalendar-138c845/src/icalendar/prop/float.py000066400000000000000000000062631524134033600232240ustar00rootroot00000000000000"""FLOAT values from :rfc:`5545`.""" import math from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters class vFloat(float): """Float Value Name: FLOAT Purpose: This value type is used to identify properties that contain a real-number value. Format Definition: This value type is defined by the following notation: .. code-block:: text float = (["+"] / "-") 1*DIGIT ["." 1*DIGIT] Description: If the property permits, multiple "float" values are specified by a COMMA-separated list of values. Example: .. code-block:: text 1000000.0000001 1.333 -3.14 .. code-block:: pycon >>> from icalendar.prop import vFloat >>> float = vFloat.from_ical('1000000.0000001') >>> float 1000000.0000001 >>> float = vFloat.from_ical('1.333') >>> float 1.333 >>> float = vFloat.from_ical('+1.333') >>> float 1.333 >>> float = vFloat.from_ical('-3.14') >>> float -3.14 """ default_value: ClassVar[str] = "FLOAT" params: Parameters def __new__( cls, *args: Any, params: dict[str, Any] | None = None, **kwargs: Any ) -> Self: self = super().__new__(cls, *args, **kwargs) self.params = Parameters(params) return self def to_ical(self) -> bytes: return str(self).encode("utf-8") @classmethod def from_ical(cls, ical: str | float) -> Self: try: self = cls(ical) except Exception as e: raise ValueError(f"Expected float value, got: {ical}") from e if not math.isfinite(self): raise ValueError(f"Expected finite float value, got: {ical}") return self @classmethod def examples(cls) -> list[Self]: """Examples of vFloat.""" return [cls(3.1415)] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [name, self.params.to_jcal(), self.VALUE.lower(), float(self)] @property def ical_value(self) -> float: """Converts the FLOAT property type according to :rfc:`5545#section-3.3.7` to a Python float.""" return float(self) @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the jCal provided is invalid. """ JCalParsingError.validate_property(jcal_property, cls) if jcal_property[0].upper() == "GEO": from icalendar.prop import vGeo return vGeo.from_jcal(jcal_property) JCalParsingError.validate_value_type(jcal_property[3], float, cls, 3) return cls( jcal_property[3], params=Parameters.from_jcal_property(jcal_property), ) __all__ = ["vFloat"] collective-icalendar-138c845/src/icalendar/prop/geo.py000066400000000000000000000114211524134033600226610ustar00rootroot00000000000000"""GEO property values from :rfc:`5545`.""" import math from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters class vGeo: """Geographic Position Property Name: GEO Purpose: This property specifies information related to the global position for the activity specified by a calendar component. Value Type: FLOAT. The value MUST be two SEMICOLON-separated FLOAT values. Property Parameters: IANA and non-standard property parameters can be specified on this property. Conformance: This property can be specified in "VEVENT" or "VTODO" calendar components. Description: This property value specifies latitude and longitude, in that order (i.e., "LAT LON" ordering). The longitude represents the location east or west of the prime meridian as a positive or negative real number, respectively. The longitude and latitude values MAY be specified up to six decimal places, which will allow for accuracy to within one meter of geographical position. Receiving applications MUST accept values of this precision and MAY truncate values of greater precision. Example: .. code-block:: ics GEO:37.386013;-122.082932 Parse vGeo: .. code-block:: pycon >>> from icalendar.prop import vGeo >>> geo = vGeo.from_ical('37.386013;-122.082932') >>> geo (37.386013, -122.082932) Add a geo location to an event: .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> latitude = 37.386013 >>> longitude = -122.082932 >>> event.add('GEO', (latitude, longitude)) >>> event['GEO'] vGeo((37.386013, -122.082932)) """ default_value: ClassVar[str] = "FLOAT" params: Parameters def __init__( self, geo: tuple[float | str | int, float | str | int], /, params: dict[str, Any] | None = None, ) -> None: """Create a new vGeo from a tuple of (latitude, longitude). Raises: ValueError: if geo is not a tuple of (latitude, longitude) """ try: latitude, longitude = (geo[0], geo[1]) latitude = float(latitude) longitude = float(longitude) except Exception as e: raise ValueError( "Input must be (float, float) for latitude and longitude" ) from e self.latitude = latitude self.longitude = longitude self.params = Parameters(params) def to_ical(self) -> str: return f"{self.latitude};{self.longitude}" @property def ical_value(self) -> tuple[float, float]: """Geographic position as a tuple of (latitude, longitude) according to :rfc:`5545#section-3.8.1.6`.""" return (self.latitude, self.longitude) @staticmethod def from_ical(ical: str) -> tuple[float, float]: try: latitude, longitude = ical.split(";") latitude, longitude = float(latitude), float(longitude) except Exception as e: raise ValueError(f"Expected 'float;float' , got: {ical}") from e if not (math.isfinite(latitude) and math.isfinite(longitude)): raise ValueError(f"Expected finite 'float;float', got: {ical}") return (latitude, longitude) def __eq__(self, other: object) -> bool: return isinstance(other, vGeo) and self.to_ical() == other.to_ical() def __hash__(self) -> int: """Hash of the vGeo object.""" return hash((self.latitude, self.longitude)) def __repr__(self) -> str: """repr(self)""" return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))" def to_jcal(self, name: str) -> list: """Convert to jCal object.""" return [ name, self.params.to_jcal(), self.VALUE.lower(), [self.latitude, self.longitude], ] @classmethod def examples(cls) -> list[Self]: """Examples of vGeo.""" return [cls((37.386013, -122.082932))] from icalendar.param import VALUE @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) return cls( jcal_property[3], Parameters.from_jcal_property(jcal_property), ) __all__ = ["vGeo"] collective-icalendar-138c845/src/icalendar/prop/image.py000066400000000000000000000072271524134033600232020ustar00rootroot00000000000000"""This contains the IMAGE property from :rfc:`7986`.""" from __future__ import annotations from typing import TYPE_CHECKING from icalendar.prop.binary import vBinary from icalendar.prop.uri import vUri if TYPE_CHECKING: from icalendar.prop.text import vText class Image: """An image as URI or BINARY according to :rfc:`7986`. Value Type: URI or BINARY -- no default. The value MUST be data with a media type of "image" or refer to such data. Description: This property specifies an image for an iCalendar object or a calendar component via a URI or directly with inline data that can be used by calendar user agents when presenting the calendar data to a user. Multiple properties MAY be used to specify alternative sets of images with, for example, varying media subtypes, resolutions, or sizes. When multiple properties are present, calendar user agents SHOULD display only one of them, picking one that provides the most appropriate image quality, or display none. The "DISPLAY" parameter is used to indicate the intended display mode for the image. The "ALTREP" parameter, defined in :rfc:`5545`, can be used to provide a "clickable" image where the URI in the parameter value can be "launched" by a click on the image in the calendar user agent. Parameters: uri: The URI of the image. b64data: The data of the image, base64 encoded. fmttype: The format type, e.g. ``"image/png"``. altrep: Link target of the image. display: The display mode, e.g. ``"BADGE"``. """ @classmethod def from_property_value(cls, value: vUri | vBinary | vText) -> Image: """Create an Image from a property value.""" params: dict[str, str] = {} if not hasattr(value, "params"): raise TypeError("Value must be URI or BINARY.") try: value_type = value.params.get("VALUE", "").upper() except (AttributeError, TypeError) as err: raise TypeError("Value must have a valid params attribute.") from err if value_type == "URI" or isinstance(value, vUri): params["uri"] = str(value) elif isinstance(value, vBinary): params["b64data"] = value.base64data elif value_type == "BINARY": params["b64data"] = str(value) else: raise TypeError( f"The VALUE parameter must be URI or BINARY, not {value_type!r}." ) params.update( { "fmttype": value.params.get("FMTTYPE"), "altrep": value.params.get("ALTREP"), "display": value.params.get("DISPLAY"), } ) return cls(**params) def __init__( self, uri: str | None = None, b64data: str | None = None, fmttype: str | None = None, altrep: str | None = None, display: str | None = None, ) -> None: """Create a new image according to :rfc:`7986`.""" if uri is not None and b64data is not None: raise ValueError("Image cannot have both URI and binary data (RFC 7986)") if uri is None and b64data is None: raise ValueError("Image must have either URI or binary data") self.uri = uri self.b64data = b64data self.fmttype = fmttype self.altrep = altrep self.display = display @property def data(self) -> bytes | None: """Return the binary data, if available.""" if self.b64data is None: return None return vBinary.from_ical(self.b64data) __all__ = ["Image"] collective-icalendar-138c845/src/icalendar/prop/inline.py000066400000000000000000000022031524134033600233630ustar00rootroot00000000000000from typing import Any from icalendar.compatibility import Self from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode class vInline(str): """This is an especially dumb class that just holds raw unparsed text and has parameters. Conversion of inline values are handled by the Component class, so no further processing is needed. """ params: Parameters __slots__ = ("params",) def __new__( cls, value: ICAL_TYPE, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ) -> Self: value = to_unicode(value, encoding=encoding) if "\r" in value or "\n" in value: raise ValueError( f"An inline value may not contain CR or LF characters: {value!r}" ) self = super().__new__(cls, value) self.params = Parameters(params) return self def to_ical(self) -> bytes: return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical: ICAL_TYPE) -> Self: return cls(ical) __all__ = ["vInline"] collective-icalendar-138c845/src/icalendar/prop/integer.py000066400000000000000000000110741524134033600235500ustar00rootroot00000000000000"""INT values from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import ICAL_TYPE class vInt(int): """Integer Value Name: INTEGER Purpose: This value type is used to identify properties that contain a signed integer value. Format Definition: This value type is defined by the following notation: .. code-block:: text integer = (["+"] / "-") 1*DIGIT Description: If the property permits, multiple "integer" values are specified by a COMMA-separated list of values. The valid range for "integer" is -2147483648 to 2147483647. If the sign is not specified, then the value is assumed to be positive. The ``__new__`` method creates a vInt instance: Parameters: value: Integer value to encode. Must be within :attr:`min` to :attr:`max`. params: Optional parameter dictionary for the property. Returns: vInt instance Raises: ValueError: If the value is outside the RFC 5545 signed 32-bit integer range as defined in :attr:`min` and :attr:`max`. Examples: .. code-block:: text 1234567890 -1234567890 +1234567890 432109876 .. code-block:: pycon >>> from icalendar.prop import vInt >>> integer = vInt.from_ical('1234567890') >>> integer 1234567890 >>> integer = vInt.from_ical('-1234567890') >>> integer -1234567890 >>> integer = vInt.from_ical('+1234567890') >>> integer 1234567890 >>> integer = vInt.from_ical('432109876') >>> integer 432109876 Create a PRIORITY property (1 = highest priority): .. code-block:: pycon >>> priority = vInt(1) >>> priority 1 >>> priority.to_ical() b'1' Create SEQUENCE property (for versioning): .. code-block:: pycon >>> sequence = vInt(3) >>> sequence.to_ical() b'3' """ default_value: ClassVar[str] = "INTEGER" params: Parameters min: ClassVar[int] = -2_147_483_648 """min: The minimum valid value per :rfc:`5545#section-3.3.8` (``-2147483648``).""" max: ClassVar[int] = 2_147_483_647 """max: The maximum valid value per :rfc:`5545#section-3.3.8` (``2147483647``).""" def __new__(cls, *args, params: dict[str, Any] | None = None, **kwargs): self = super().__new__(cls, *args, **kwargs) self.params = Parameters(params) if not (cls.min <= self <= cls.max): raise ValueError( f"Integer {self} is outside the RFC 5545 range [{cls.min}, {cls.max}]" ) return self def to_ical(self) -> bytes: return str(self).encode("utf-8") @property def ical_value(self) -> int: """INTEGER property type according to :rfc:`5545#section-3.3.8`""" return int(self) @classmethod def from_ical(cls, ical: ICAL_TYPE): try: value = int(ical) except Exception as e: raise ValueError(f"Expected int, got: {ical}") from e return cls(value) @classmethod def examples(cls) -> list[Self]: """Examples of vInt.""" return [vInt(1000), vInt(-42)] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [name, self.params.to_jcal(), self.VALUE.lower(), int(self)] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) JCalParsingError.validate_value_type(jcal_property[3], int, cls, 3) return cls( jcal_property[3], params=Parameters.from_jcal_property(jcal_property), ) @classmethod def parse_jcal_value(cls, value: Any) -> int: """Parse a jCal value for vInt. Raises: ~error.JCalParsingError: If the value is not an int. """ JCalParsingError.validate_value_type(value, int, cls) return cls(value) __all__ = ["vInt"] collective-icalendar-138c845/src/icalendar/prop/n.py000066400000000000000000000123311524134033600223450ustar00rootroot00000000000000"""N property from :rfc:`6350`.""" from typing import Any, ClassVar, NamedTuple from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class NFields(NamedTuple): """Named fields for vCard N (Name) property per :rfc:`6350#section-6.2.2`. Provides named access to the five name components. """ family: str """Family names (also known as surnames).""" given: str """Given names.""" additional: str """Additional names.""" prefix: str """Honorific prefixes.""" suffix: str """Honorific suffixes.""" class vN: r"""vCard N (Name) structured property per :rfc:`6350#section-6.2.2`. The N property represents a person's name. It consists of a single structured text value. Each component in the structure may have multiple values, separated by commas. The structured property value corresponds, in sequence, to the following fields: - family names (also known as surnames) - given names - additional names - honorific prefixes - honorific suffixes Semicolons are field separators and are NOT escaped. Commas and backslashes within field values ARE escaped per :rfc:`6350`. Examples: .. code-block:: pycon >>> from icalendar.prop import vN >>> n = vN(("Doe", "John", "M.", "Dr.", "Jr.,M.D.,A.C.P.")) >>> n.to_ical() b'Doe;John;M.;Dr.;Jr.\\,M.D.\\,A.C.P.' >>> vN.from_ical(r"Doe;John;M.;Dr.;Jr.\,M.D.\,A.C.P.") NFields(family='Doe', given='John', additional='M.', prefix='Dr.', suffix='Jr.,M.D.,A.C.P.') """ default_value: ClassVar[str] = "TEXT" params: Parameters fields: NFields def __init__( self, fields: tuple[str, ...] | list[str] | str | NFields, /, params: dict[str, Any] | None = None, ) -> None: """Initialize N with five fields or parse from vCard format string. Parameters: fields: Either an NFields, tuple, or list of five strings, one per field, or a vCard format string with semicolon-separated fields params: Optional property parameters """ if isinstance(fields, str): fields = self.from_ical(fields) if isinstance(fields, NFields): self.fields = fields else: if len(fields) != 5: raise ValueError(f"N must have exactly 5 fields, got {len(fields)}") self.fields = NFields(*(str(f) for f in fields)) self.params = Parameters(params) def to_ical(self) -> bytes: """Generate vCard format with semicolon-separated fields.""" from icalendar.prop.text import vText parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] return ";".join(parts).encode(DEFAULT_ENCODING) @staticmethod def from_ical(ical: str | bytes) -> NFields: """Parse vCard N format into an NFields named tuple. Parameters: ical: vCard format string with semicolon-separated fields Returns: NFields named tuple with five field values. """ from icalendar.parser import split_on_unescaped_semicolon ical = to_unicode(ical) fields = split_on_unescaped_semicolon(ical) if len(fields) != 5: raise ValueError(f"N must have exactly 5 fields, got {len(fields)}: {ical}") return NFields(*fields) def __eq__(self, other: object) -> bool: """self == other""" return isinstance(other, vN) and self.fields == other.fields def __repr__(self) -> str: """String representation.""" return f"{self.__class__.__name__}({self.fields}, params={self.params})" @property def ical_value(self) -> NFields: """The name fields as a named tuple.""" return self.fields from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" result = [name, self.params.to_jcal(), self.VALUE.lower()] result.extend(self.fields) return result @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) if len(jcal_property) != 8: # name, params, value_type, 5 fields raise JCalParsingError( f"N must have 8 elements (name, params, value_type, 5 fields), " f"got {len(jcal_property)}" ) for i, field in enumerate(jcal_property[3:], start=3): JCalParsingError.validate_value_type(field, str, cls, i) return cls( tuple(jcal_property[3:]), Parameters.from_jcal_property(jcal_property), ) @classmethod def examples(cls) -> list[Self]: """Examples of vN.""" return [cls(("Doe", "John", "M.", "Dr.", "Jr."))] __all__ = ["NFields", "vN"] collective-icalendar-138c845/src/icalendar/prop/org.py000066400000000000000000000116131524134033600227010ustar00rootroot00000000000000"""The ORG property from :rfc:`6350`.""" from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class vOrg: r"""vCard ORG (Organization) structured property per :rfc:`6350#section-6.6.4`. The ORG property specifies the organizational name and units associated with the vCard. Its value is a structured type consisting of components separated by semicolons. The components are the organization name, followed by zero or more levels of organizational unit names. .. code-block:: text organization-name; organizational-unit-1; organizational-unit-2; ... Semicolons are field separators and are NOT escaped. Commas and backslashes within field values ARE escaped per :rfc:`6350`. Examples: A property value consisting of an organizational name, organizational unit #1 name, and organizational unit #2 name. .. code-block:: ics ORG:ABC\, Inc.;North American Division;Marketing The same example in icalendar. .. code-block:: pycon >>> from icalendar.prop import vOrg >>> org = vOrg(("ABC, Inc.", "North American Division", "Marketing")) >>> org.to_ical() b'ABC\\, Inc.;North American Division;Marketing' >>> vOrg.from_ical(r"ABC\, Inc.;North American Division;Marketing") ('ABC, Inc.', 'North American Division', 'Marketing') """ default_value: ClassVar[str] = "TEXT" params: Parameters fields: tuple[str, ...] def __init__( self, fields: tuple[str, ...] | list[str] | str, /, params: dict[str, Any] | None = None, ) -> None: """Initialize ORG with variable fields or parse from vCard format string. Parameters: fields: Either a tuple or list of one or more strings, or a vCard format string with semicolon-separated fields params: Optional property parameters """ if isinstance(fields, str): fields = self.from_ical(fields) if len(fields) < 1: raise ValueError("ORG must have at least 1 field (organization name)") self.fields = tuple(str(f) for f in fields) self.params = Parameters(params) def to_ical(self) -> bytes: """Generate vCard format with semicolon-separated fields.""" from icalendar.prop.text import vText parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] return ";".join(parts).encode(DEFAULT_ENCODING) @staticmethod def from_ical(ical: str | bytes) -> tuple[str, ...]: """Parse vCard ORG format into a tuple of fields. Parameters: ical: vCard format string with semicolon-separated fields Returns: Tuple of field values with one or more fields """ from icalendar.parser import split_on_unescaped_semicolon ical = to_unicode(ical) fields = split_on_unescaped_semicolon(ical) return tuple(fields) def __eq__(self, other: object) -> bool: """self == other""" return isinstance(other, vOrg) and self.fields == other.fields def __repr__(self) -> str: """String representation.""" return f"{self.__class__.__name__}({self.fields}, params={self.params})" @property def name(self) -> str: """The organization name (first field).""" return self.fields[0] @property def units(self) -> tuple[str, ...]: """The organizational unit names (remaining fields after the name).""" return self.fields[1:] @property def ical_value(self) -> tuple[str, ...]: """The organization fields as a tuple.""" return self.fields from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" result = [name, self.params.to_jcal(), self.VALUE.lower()] result.extend(self.fields) return result @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) for i, field in enumerate(jcal_property[3:], start=3): JCalParsingError.validate_value_type(field, str, cls, i) return cls( tuple(jcal_property[3:]), Parameters.from_jcal_property(jcal_property), ) @classmethod def examples(cls) -> list[Self]: """Examples of vOrg.""" return [cls(("ABC Inc.", "North American Division", "Marketing"))] __all__ = ["vOrg"] collective-icalendar-138c845/src/icalendar/prop/recur/000077500000000000000000000000001524134033600226565ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/prop/recur/__init__.py000066400000000000000000000004221524134033600247650ustar00rootroot00000000000000"""Classes for the RECUR property type.""" from .frequency import vFrequency from .month import vMonth from .recur import vRecur from .skip import vSkip from .weekday import vWeekday __all__ = [ "vFrequency", "vMonth", "vRecur", "vSkip", "vWeekday", ] collective-icalendar-138c845/src/icalendar/prop/recur/frequency.py000066400000000000000000000042351524134033600252350ustar00rootroot00000000000000"""FREQ value type of RECUR from :rfc:`5545`.""" from typing import Any from icalendar.caselessdict import CaselessDict from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class vFrequency(str): """A simple class that catches illegal values.""" params: Parameters __slots__ = ("params",) frequencies = CaselessDict( { "SECONDLY": "SECONDLY", "MINUTELY": "MINUTELY", "HOURLY": "HOURLY", "DAILY": "DAILY", "WEEKLY": "WEEKLY", "MONTHLY": "MONTHLY", "YEARLY": "YEARLY", } ) def __new__( cls, value, encoding=DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ): value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) if self not in vFrequency.frequencies: raise ValueError(f"Expected frequency, got: {self}") self.params = Parameters(params) return self def to_ical(self): return self.encode(DEFAULT_ENCODING).upper() @classmethod def from_ical(cls, ical): try: return cls(ical.upper()) except Exception as e: raise ValueError(f"Expected frequency, got: {ical}") from e @classmethod def parse_jcal_value(cls, value: Any) -> Self: """Parse a jCal value for vFrequency. Raises: ~error.JCalParsingError: If the value is not a valid frequency. """ JCalParsingError.validate_value_type(value, str, cls) try: return cls(value) except ValueError as e: raise JCalParsingError( "The value must be a valid frequency.", cls, value=value ) from e @property def ical_value(self) -> str: """Returns the frequency value as a string, for example, ``WEEKLY`` or ``DAILY``. See Also: :rfc:`5545#section-3.3.10` for the ``FREQ`` rule grammar. """ return str(self) __all__ = ["vFrequency"] collective-icalendar-138c845/src/icalendar/prop/recur/month.py000066400000000000000000000061171524134033600243620ustar00rootroot00000000000000"""BYMONTH value type of RECUR from :rfc:`5545` and :rfc:`7529`.""" from typing import Any from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters class vMonth(int): """The number of the month for recurrence. In :rfc:`5545`, this is just an int. In :rfc:`7529`, this can be followed by `L` to indicate a leap month. .. code-block:: pycon >>> from icalendar import vMonth >>> vMonth(1) # first month January vMonth('1') >>> vMonth("5L") # leap month in Hebrew calendar vMonth('5L') >>> vMonth(1).leap False >>> vMonth("5L").leap True Definition from RFC: .. code-block:: text type-bymonth = element bymonth { xsd:positiveInteger | xsd:string } """ params: Parameters def __new__(cls, month: str | int, /, params: dict[str, Any] | None = None): if isinstance(month, vMonth): return cls(month.to_ical().decode()) if isinstance(month, str): # ``str.isdigit`` is True for non-ASCII digits (e.g. "١٢") that # ``int`` then either accepts as a different value or rejects, so # gate on ASCII digits to match what ``int`` parses below. if month.isascii() and month.isdigit(): month_index = int(month) leap = False else: digits = month[:-1] if ( not month or month[-1] != "L" or not (digits.isascii() and digits.isdigit()) ): raise ValueError(f"Invalid month: {month!r}") month_index = int(digits) leap = True else: leap = False month_index = int(month) self = super().__new__(cls, month_index) self.leap = leap self.params = Parameters(params) return self def to_ical(self) -> bytes: """The ical representation.""" return str(self).encode("utf-8") @classmethod def from_ical(cls, ical: str): return cls(ical) @property def leap(self) -> bool: """Whether this is a leap month.""" return self._leap @leap.setter def leap(self, value: bool) -> None: self._leap = value def __repr__(self) -> str: """repr(self)""" return f"{self.__class__.__name__}({str(self)!r})" def __str__(self) -> str: """str(self)""" return f"{int(self)}{'L' if self.leap else ''}" @classmethod def parse_jcal_value(cls, value: Any) -> Self: """Parse a jCal value for vMonth. Raises: ~error.JCalParsingError: If the value is not a valid month. """ JCalParsingError.validate_value_type(value, (str, int), cls) try: return cls(value) except ValueError as e: raise JCalParsingError( "The value must be a string or an integer.", cls, value=value ) from e __all__ = ["vMonth"] collective-icalendar-138c845/src/icalendar/prop/recur/recur.py000066400000000000000000000257041524134033600243600ustar00rootroot00000000000000"""RECUR property type from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.caselessdict import CaselessDict from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, SEQUENCE_TYPES from icalendar.prop.dt import vDDDTypes from icalendar.prop.integer import vInt from icalendar.prop.recur.frequency import vFrequency from icalendar.prop.recur.month import vMonth from icalendar.prop.recur.skip import vSkip from icalendar.prop.recur.weekday import vWeekday from icalendar.prop.text import vText class vRecur(CaselessDict): """Recurrence definition. Property Name: RRULE Purpose: This property defines a rule or repeating pattern for recurring events, to-dos, journal entries, or time zone definitions. Value Type: RECUR Property Parameters: IANA and non-standard property parameters can be specified on this property. Conformance: This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once. The recurrence set generated with multiple "RRULE" properties is undefined. Description: The recurrence rule, if specified, is used in computing the recurrence set. The recurrence set is the complete set of recurrence instances for a calendar component. The recurrence set is generated by considering the initial "DTSTART" property along with the "RRULE", "RDATE", and "EXDATE" properties contained within the recurring component. The "DTSTART" property defines the first instance in the recurrence set. The "DTSTART" property value SHOULD be synchronized with the recurrence rule, if specified. The recurrence set generated with a "DTSTART" property value not synchronized with the recurrence rule is undefined. The final recurrence set is generated by gathering all of the start DATE-TIME values generated by any of the specified "RRULE" and "RDATE" properties, and then excluding any start DATE-TIME values specified by "EXDATE" properties. This implies that start DATE- TIME values specified by "EXDATE" properties take precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only one recurrence is considered. Duplicate instances are ignored. The "DTSTART" property specified within the iCalendar object defines the first instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value type used with a recurrence rule, should be specified as a date with local time and time zone reference to make sure all the recurrence instances start at the same local time regardless of time zone changes. If the duration of the recurring component is specified with the "DTEND" or "DUE" property, then the same exact duration will apply to all the members of the generated recurrence set. Else, if the duration of the recurring component is specified with the "DURATION" property, then the same nominal duration will apply to all the members of the generated recurrence set and the exact duration of each recurrence instance will depend on its specific start time. For example, recurrence instances of a nominal duration of one day will have an exact duration of more or less than 24 hours on a day where a time zone shift occurs. The duration of a specific recurrence may be modified in an exception component or simply by using an "RDATE" property of PERIOD value type. Examples: The following RRULE specifies daily events for 10 occurrences. .. code-block:: ics RRULE:FREQ=DAILY;COUNT=10 Below, we parse the RRULE ical string. .. code-block:: pycon >>> from icalendar.prop import vRecur >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10') >>> rrule vRecur({'FREQ': ['DAILY'], 'COUNT': [10]}) You can choose to add an rrule to an :class:`icalendar.cal.Event` or :class:`icalendar.cal.Todo`. .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10') >>> event.rrules [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})] """ default_value: ClassVar[str] = "RECUR" params: Parameters frequencies = [ "SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY", ] # Mac iCal ignores RRULEs where FREQ is not the first rule part. # Sorts parts according to the order listed in RFC 5545, section 3.3.10. canonical_order = ( "RSCALE", "FREQ", "UNTIL", "COUNT", "INTERVAL", "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", "BYWEEKDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS", "WKST", "SKIP", ) types = CaselessDict( { "COUNT": vInt, "INTERVAL": vInt, "BYSECOND": vInt, "BYMINUTE": vInt, "BYHOUR": vInt, "BYWEEKNO": vInt, "BYMONTHDAY": vInt, "BYYEARDAY": vInt, "BYMONTH": vMonth, "UNTIL": vDDDTypes, "BYSETPOS": vInt, "WKST": vWeekday, "BYDAY": vWeekday, "FREQ": vFrequency, "BYWEEKDAY": vWeekday, "SKIP": vSkip, # RFC 7529 "RSCALE": vText, # RFC 7529 } ) # for reproducible serialization: # RULE: if and only if it can be a list it will be a list # look up in RFC jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"} def __init__( self, *args: Any, params: dict[str, Any] | None = None, **kwargs: Any ) -> None: if args and isinstance(args[0], str): # we have a string as an argument. args = (self.from_ical(args[0]),) + args[1:] for k, v in kwargs.items(): if not isinstance(v, SEQUENCE_TYPES): kwargs[k] = [v] super().__init__(*args, **kwargs) self.params = Parameters(params) def to_ical(self): result = [] for key, vals in self.sorted_items(): typ = self.types.get(key, vText) if not isinstance(vals, SEQUENCE_TYPES): vals = [vals] param_vals = b",".join(typ(val).to_ical() for val in vals) # CaselessDict keys are always unicode param_key = key.encode(DEFAULT_ENCODING) result.append(param_key + b"=" + param_vals) return b";".join(result) @classmethod def parse_type(cls, key, values): # integers parser = cls.types.get(key, vText) return [parser.from_ical(v) for v in values.split(",")] @classmethod def from_ical(cls, ical: str): if isinstance(ical, cls): return ical try: recur = cls() for pairs in ical.split(";"): try: key, vals = pairs.split("=") except ValueError: # E.g. incorrect trailing semicolon, like (issue #157): # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; continue recur[key] = cls.parse_type(key, vals) return cls(recur) except ValueError: raise except Exception as e: raise ValueError(f"Error in recurrence rule: {ical}") from e @classmethod def examples(cls) -> list[Self]: """Examples of vRecur.""" return [cls.from_ical("FREQ=DAILY;COUNT=10")] from icalendar.param import VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" recur = {} for k, v in self.items(): key = k.lower() if key.upper() in self.jcal_not_a_list: value = v[0] if isinstance(v, list) and len(v) == 1 else v elif not isinstance(v, list): value = [v] else: value = v recur[key] = value if "until" in recur: until = recur["until"] until_jcal = vDDDTypes(until).to_jcal("until") recur["until"] = until_jcal[-1] return [name, self.params.to_jcal(), self.VALUE.lower(), recur] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) params = Parameters.from_jcal_property(jcal_property) if not isinstance(jcal_property[3], dict) or not all( isinstance(k, str) for k in jcal_property[3] ): raise JCalParsingError( "The recurrence rule must be a mapping with string keys.", cls, 3, value=jcal_property[3], ) recur = {} for key, value in jcal_property[3].items(): JCalParsingError.validate_jcal_token( key, "recurrence rule part name", cls, path=[3, key] ) value_type = cls.types.get(key, vText) with JCalParsingError.reraise_with_path_added(3, key): if isinstance(value, list): recur[key.lower()] = values = [] for i, v in enumerate(value): with JCalParsingError.reraise_with_path_added(i): values.append(value_type.parse_jcal_value(v)) else: recur[key] = value_type.parse_jcal_value(value) until = recur.get("until") if until is not None and not isinstance(until, list): recur["until"] = [until] return cls(recur, params=params) def __eq__(self, other: object) -> bool: """self == other""" if not isinstance(other, vRecur): return super().__eq__(other) if self.keys() != other.keys(): return False for key in self.keys(): v1 = self[key] v2 = other[key] if not isinstance(v1, SEQUENCE_TYPES): v1 = [v1] if not isinstance(v2, SEQUENCE_TYPES): v2 = [v2] if v1 != v2: return False return True __hash__ = None __all__ = ["vRecur"] collective-icalendar-138c845/src/icalendar/prop/recur/skip.py000066400000000000000000000024341524134033600242010ustar00rootroot00000000000000"""SKIP value type of RECUR from :rfc:`7529`.""" from typing import Any from icalendar.compatibility import Self from icalendar.enums import Enum from icalendar.error import JCalParsingError from icalendar.prop.text import vText class vSkip(vText, Enum): """Skip values for RRULE. These are defined in :rfc:`7529`. OMIT is the default value. Examples: .. code-block:: pycon >>> from icalendar import vSkip >>> vSkip.OMIT vSkip('OMIT') >>> vSkip.FORWARD vSkip('FORWARD') >>> vSkip.BACKWARD vSkip('BACKWARD') """ OMIT = "OMIT" FORWARD = "FORWARD" BACKWARD = "BACKWARD" __reduce_ex__ = Enum.__reduce_ex__ def __repr__(self): return f"{self.__class__.__name__}({self._name_!r})" @classmethod def parse_jcal_value(cls, value: Any) -> Self: """Parse a jCal value for vSkip. Raises: ~error.JCalParsingError: If the value is not a valid skip value. """ JCalParsingError.validate_value_type(value, str, cls) try: return cls[value.upper()] except KeyError as e: raise JCalParsingError( "The value must be a valid skip value.", cls, value=value ) from e __all__ = ["vSkip"] collective-icalendar-138c845/src/icalendar/prop/recur/weekday.py000066400000000000000000000075441524134033600246730ustar00rootroot00000000000000"""BYWEEKDAY, BYDAY, and WKST value type of RECUR from :rfc:`5545`.""" import re from typing import Any from icalendar.caselessdict import CaselessDict from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode # Use ``[0-9]`` to explicitly specify that only ASCII digits should be # matched, instead of ``\d``. In Python, ``\d`` matches a digit zero through # nine in any script except ideographic scripts, and is equivalent to # ``\p{Nd}``. For example, the Arabic-Indic "١٢" would match. Here, a # malformed ``ordwk`` would get parsed into a valid ``relative`` number. # RFC 5545, section 3.3.10, allows only ASCII characters per its definition # of ``DIGIT``. WEEKDAY_RULE = re.compile( r"(?P[+-]?)(?P[0-9]{0,2})(?P[\w]{2})\Z" ) class vWeekday(str): """Either a ``weekday`` or a ``weekdaynum``. .. code-block:: pycon >>> from icalendar import vWeekday >>> vWeekday("MO") # Simple weekday 'MO' >>> vWeekday("2FR").relative # Second friday 2 >>> vWeekday("2FR").weekday 'FR' >>> vWeekday("-1SU").relative # Last Sunday -1 Definition from :rfc:`5545#section-3.3.10`: .. code-block:: text weekdaynum = [[plus / minus] ordwk] weekday plus = "+" minus = "-" ordwk = 1*2DIGIT ;1 to 53 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, ;FRIDAY, and SATURDAY days of the week. """ params: Parameters __slots__ = ("params", "relative", "weekday") week_days = CaselessDict( { "SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6, } ) def __new__( cls, value, encoding=DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ): value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) match = WEEKDAY_RULE.match(self) if match is None: raise ValueError(f"Expected weekday abbreviation, got: {self}") match = match.groupdict() sign = match["signal"] weekday = match["weekday"] relative = match["relative"] if weekday not in vWeekday.week_days or sign not in "+-": raise ValueError(f"Expected weekday abbreviation, got: {self}") self.weekday = weekday or None self.relative = (relative and int(relative)) or None if sign == "-" and self.relative: self.relative *= -1 self.params = Parameters(params) return self def to_ical(self): return self.encode(DEFAULT_ENCODING).upper() @classmethod def from_ical(cls, ical): try: return cls(ical.upper()) except Exception as e: raise ValueError(f"Expected weekday abbreviation, got: {ical}") from e @property def ical_value(self) -> str: """Returns the weekday value as a string, for example, ``MO``, ``+2TH``, or ``-1SU``. See Also: :rfc:`5545#section-3.3.10` for the ``BYDAY`` rule grammar. """ return str(self) @classmethod def parse_jcal_value(cls, value: Any) -> Self: """Parse a jCal value for vWeekday. Raises: ~error.JCalParsingError: If the value is not a valid weekday. """ JCalParsingError.validate_value_type(value, str, cls) try: return cls(value) except ValueError as e: raise JCalParsingError( "The value must be a valid weekday.", cls, value=value ) from e __all__ = ["vWeekday"] collective-icalendar-138c845/src/icalendar/prop/text.py000066400000000000000000000124431524134033600231000ustar00rootroot00000000000000"""TEXT values from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters, _escape_char from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode class vText(str): r"""vText is a data type that contains human-readable text values. The vText property uses the :rfc:`5545#section-3.3.11` TEXT value type in various icalendar properties to show free-form text that others can read. This class can be created from Python strings, and can be used to add text descriptions to calendar events. To create a TEXT object, pass in the string you want when creating the object. To add a line break, use ``\n`` or ``\N``. Use the LANGUAGE property parameter to set the language of the text. When the TEXT object is serialized to an icalendar stream, certain characters are escaped or changed. These characters include the COMMA, SEMICOLON, BACKSLASH, and line breaks. Contrast TEXT with the UNKNOWN value data type specified in :rfc:`7265#section-5`. UNKNOWN is implemented in the Python class :class:`~icalendar.prop.unknown.vUnknown`, which does **not** apply this escaping and preserves its value verbatim, because the escaping rules of an unrecognized value type are not known. :class:`~icalendar.prop.unknown.vUnknown` deliberately does not inherit from ``vText``, so the two don't share escaping behavior. Examples: vText property as a TEXT value type. .. code-block:: text Project XYZ Final Review\nConference Room - 3B\nCome Prepared. Create a vText property, and display it in a readable format. .. code-block:: pycon >>> from icalendar.prop import vText >>> desc = 'Project XYZ Final Review\nConference Room - 3B\nCome Prepared.' >>> text = vText(desc) >>> text vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.') >>> print(text.ical_value) Project XYZ Final Review Conference Room - 3B Come Prepared. Add a SUMMARY to an event, then display its value as a vText property then in a readable format: .. code-block:: pycon >>> from icalendar import Event >>> event = Event() >>> event.add('SUMMARY', desc) >>> event['SUMMARY'] vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.') >>> print(event.to_ical().decode()) BEGIN:VEVENT SUMMARY:Project XYZ Final Review\nConference Room - 3B\nCome Prepared. END:VEVENT """ default_value: ClassVar[str] = "TEXT" params: Parameters __slots__ = ("encoding", "params") def __new__( cls, value: ICAL_TYPE, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ) -> Self: value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.encoding = encoding self.params = Parameters(params) return self def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_ical()!r})" def to_ical(self) -> bytes: return _escape_char(self).encode(self.encoding) @classmethod def from_ical(cls, ical: ICAL_TYPE) -> Self: return cls(ical) @property def ical_value(self) -> str: """The string value of the text.""" return str(self) from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" if name == "request-status": # TODO: maybe add a vRequestStatus class? return [name, {}, "text", self.split(";", 2)] return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] @classmethod def examples(cls) -> list[Self]: """Examples of vText.""" return [cls("Hello World!")] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) name = jcal_property[0] if name == "categories": from icalendar.prop import vCategory return vCategory.from_jcal(jcal_property) string = jcal_property[3] # TODO: accept list or string but join with ; if name == "request-status": # TODO: maybe add a vRequestStatus class? JCalParsingError.validate_list_type(jcal_property[3], str, cls, 3) string = ";".join(jcal_property[3]) JCalParsingError.validate_value_type(string, str, cls, 3) return cls( string, params=Parameters.from_jcal_property(jcal_property), ) @classmethod def parse_jcal_value(cls, jcal_value: Any) -> Self: """Parse a jCal value into a vText.""" JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls) return cls(str(jcal_value)) __all__ = ["vText"] collective-icalendar-138c845/src/icalendar/prop/uid.py000066400000000000000000000021711524134033600226720ustar00rootroot00000000000000"""UID values from :rfc:`9253`.""" import uuid from typing import ClassVar from icalendar.compatibility import Self from .text import vText class vUid(vText): """A UID of a component. This is defined in :rfc:`9253`, Section 7. """ default_value: ClassVar[str] = "UID" @classmethod def new(cls) -> Self: """Create a new UID for convenience. .. code-block:: pycon >>> from icalendar import vUid >>> vUid.new() vUid('d755cef5-2311-46ed-a0e1-6733c9e15c63') """ return vUid(uuid.uuid4()) @property def uid(self) -> str: """The UID of this property.""" return str(self) @property def ical_value(self) -> str: """The UID of this property.""" return self.uid def __repr__(self) -> str: """repr(self)""" return f"{self.__class__.__name__}({self.uid!r})" from icalendar.param import FMTTYPE, LABEL, LINKREL @classmethod def examples(cls) -> list[Self]: """Examples of vUid.""" return [cls("d755cef5-2311-46ed-a0e1-6733c9e15c63")] __all__ = ["vUid"] collective-icalendar-138c845/src/icalendar/prop/unknown.py000066400000000000000000000115101524134033600236050ustar00rootroot00000000000000"""UNKNOWN values from :rfc:`7265`.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode if TYPE_CHECKING: from icalendar.compatibility import Self from icalendar.parser.content_line import Contentline class vUnknown(str): """A property value of the :rfc:`7265#section-5` reserved UNKNOWN value data type. .. versionchanged:: 7.2.0 Previously ``vUnknown`` inherited from ``vText``, which unescapes values. Now ``vUnknown`` doesn't unescape its values, which is the correct behavior. Unlike :class:`~icalendar.prop.text.vText`, the value is preserved verbatim when imported from and exported to iCalendar data, without :rfc:`5545` escaping or unescaping. When the value type of an unrecognized property is not known, then no escaping rules can be applied, and the value must be preserved as is round-trip. See also: :rfc:`7265#section-5.1` """ default_value: ClassVar[str] = "UNKNOWN" params: Parameters __slots__ = ("encoding", "params") def __new__( cls, value: ICAL_TYPE, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ) -> Self: value = to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.encoding = encoding self.params = Parameters(params) return self def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_ical()!r})" def to_ical(self) -> bytes: r"""Return the value verbatim, without :rfc:`5545` escaping. This method's implementation is different from that in :class:`~icalendar.prop.text.vText`, whose :meth:`~icalendar.prop.text.vText.to_ical` method escapes ``;``, ``,``, ``\``, and newlines. Example: The semicolon is kept verbatim for UNKNOWN, unlike a TEXT value which would escape it as ``\\;``. .. code-block:: pycon >>> from icalendar.prop import vText, vUnknown >>> vUnknown("a;b").to_ical() b'a;b' >>> vText("a;b").to_ical() b'a\\;b' See also: :rfc:`7265#section-5.2` """ return self.encode(self.encoding) @classmethod def from_ical(cls, ical: ICAL_TYPE) -> Self: """Take the value verbatim, without unescaping.""" return cls(ical) @classmethod def get_value_from_content_line(cls, line: Contentline) -> str: """Return this type's value from ``line``, taken verbatim. Value types that must not have their value unescaped provide this method, and the parser uses it to obtain the value instead of :meth:`~icalendar.parser.content_line.Contentline.parts`. For :rfc:`7265` ``UNKNOWN`` values, the escaping rules of the real value type are not known, so no unescaping can be applied. """ return line.raw_parts()[2] @property def ical_value(self) -> str: """The string value of the property.""" return str(self) from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE def to_jcal(self, name: str) -> list: """The jCal representation of this property, according to :rfc:`7265#section-5.1`. If the property doesn't include a VALUE property parameter and its value type is not known, then its value type is set to ``"unknown"``. Else the property's value type is converted to lowercase. The property's value is the unprocessed value text, aside from standard JSON string escaping. """ return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] @classmethod def examples(cls) -> list[Self]: """Examples of vUnknown.""" return [cls("Some property text.")] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`, taking the value verbatim. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) string = jcal_property[3] JCalParsingError.validate_value_type(string, str, cls, 3) return cls( string, params=Parameters.from_jcal_property(jcal_property), ) @classmethod def parse_jcal_value(cls, jcal_value: Any) -> Self: """Parse a jCal value into a vUnknown.""" JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls) return cls(str(jcal_value)) __all__ = ["vUnknown"] collective-icalendar-138c845/src/icalendar/prop/uri.py000066400000000000000000000070771524134033600227220ustar00rootroot00000000000000"""URI values from :rfc:`5545`.""" from typing import Any, ClassVar from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode class vUri(str): """URI Value Name: URI Purpose: This value type is used to identify values that contain a uniform resource identifier (URI) type of reference to the property value. Format Definition: This value type is defined by the following notation: .. code-block:: text uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] Description: This value type might be used to reference binary information, for values that are large, or otherwise undesirable to include directly in the iCalendar object. Property values with this value type MUST follow the generic URI syntax defined in [RFC3986]. When a property parameter value is a URI value type, the URI MUST be specified as a quoted-string value. Examples: The following is a URI for a network file: .. code-block:: ics http://example.com/my-report.txt .. code-block:: pycon >>> from icalendar.prop import vUri >>> uri = vUri.from_ical('http://example.com/my-report.txt') >>> uri vUri('http://example.com/my-report.txt') >>> uri.uri 'http://example.com/my-report.txt' """ default_value: ClassVar[str] = "URI" params: Parameters __slots__ = ("params",) def __new__( cls, value: str, encoding: str = DEFAULT_ENCODING, /, params: dict[str, Any] | None = None, ) -> Self: value = to_unicode(value, encoding=encoding) if "\r" in value or "\n" in value: raise ValueError( f"A URI value may not contain CR or LF characters: {value!r}" ) self = super().__new__(cls, value) self.params = Parameters(params) return self def to_ical(self) -> bytes: return self.encode(DEFAULT_ENCODING) @classmethod def from_ical(cls, ical: str | bytes) -> Self: try: return cls(ical) except Exception as e: raise ValueError(f"Expected , got: {ical}") from e @classmethod def examples(cls) -> list[Self]: """Examples of vUri.""" return [cls("http://example.com/my-report.txt")] def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) return cls( jcal_property[3], params=Parameters.from_jcal_property(jcal_property), ) @property def ical_value(self) -> str: """The URI.""" return self.uri @property def uri(self) -> str: """The URI.""" return str(self) def __repr__(self) -> str: """repr(self)""" return f"{self.__class__.__name__}({self.uri!r})" from icalendar.param import FMTTYPE, GAP, LABEL, LANGUAGE, LINKREL, RELTYPE, VALUE __all__ = ["vUri"] collective-icalendar-138c845/src/icalendar/prop/xml_reference.py000066400000000000000000000030061524134033600247250ustar00rootroot00000000000000"""XML-REFERENCE values from :rfc:`9253`.""" from typing import ClassVar from urllib.parse import unquote, urlparse from icalendar.compatibility import Self from icalendar.prop.uri import vUri class vXmlReference(vUri): """An XML-REFERENCE. The associated value references an associated XML artifact and is a URI with an XPointer anchor value. This is defined in :rfc:`9253`, Section 7. """ default_value: ClassVar[str] = "XML-REFERENCE" @property def xml_reference(self) -> str: """The XML reference URI of this property.""" return self.uri @property def x_pointer(self) -> str | None: """The XPointer of the URI. The XPointer is defined in `W3C.WD-xptr-xpointer-20021219 `_, and its use as an anchor is defined in `W3C.REC-xptr-framework-20030325 `_. Returns: The decoded x-pointer or ``None`` if no valid x-pointer is found. """ parsed = urlparse(self.xml_reference) fragment = unquote(parsed.fragment) if not fragment.startswith("xpointer(") or not fragment.endswith(")"): return None return fragment[9:-1] @classmethod def examples(cls) -> list[Self]: """Examples of vXmlReference.""" return [cls("http://example.com/doc.xml#xpointer(/doc/element)")] __all__ = ["vXmlReference"] collective-icalendar-138c845/src/icalendar/py.typed000066400000000000000000000000001524134033600222430ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/000077500000000000000000000000001524134033600217205ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/__init__.py000066400000000000000000000000001524134033600240170ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/alarms/000077500000000000000000000000001524134033600231775ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/alarms/example.ics000066400000000000000000000002541524134033600253330ustar00rootroot00000000000000BEGIN:VALARM TRIGGER;VALUE=DATE-TIME:19970317T133000Z REPEAT:4 DURATION:PT15M ACTION:AUDIO ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/ sounds/bell-01.aud END:VALARM collective-icalendar-138c845/src/icalendar/tests/alarms/rfc_5545_absolute_alarm_example.ics000066400000000000000000000002521524134033600317170ustar00rootroot00000000000000BEGIN:VALARM TRIGGER;VALUE=DATE-TIME:19970317T133000Z REPEAT:4 DURATION:PT15M ACTION:AUDIO ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud END:VALARM collective-icalendar-138c845/src/icalendar/tests/alarms/rfc_5545_end.ics000066400000000000000000000006311524134033600257610ustar00rootroot00000000000000BEGIN:VALARM TRIGGER;RELATED=END:-P2D ACTION:EMAIL ATTENDEE:mailto:john_doe@example.com SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING *** DESCRIPTION:A draft agenda needs to be sent out to the attendees to the weekly managers meeting (MGR-LIST). Attached is a pointer the document template for the agenda file. ATTACH;FMTTYPE=application/msword:http://example.com/templates/agenda.doc END:VALARM collective-icalendar-138c845/src/icalendar/tests/alarms/start_date.ics000066400000000000000000000006331524134033600260330ustar00rootroot00000000000000BEGIN:VALARM TRIGGER;RELATED=START:-P2D ACTION:EMAIL ATTENDEE:mailto:john_doe@example.com SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING *** DESCRIPTION:A draft agenda needs to be sent out to the attendees to the weekly managers meeting (MGR-LIST). Attached is a pointer the document template for the agenda file. ATTACH;FMTTYPE=application/msword:http://example.com/templates/agenda.doc END:VALARM collective-icalendar-138c845/src/icalendar/tests/attr/000077500000000000000000000000001524134033600226725ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/attr/__init__.py000066400000000000000000000000561524134033600250040ustar00rootroot00000000000000"""Test the getters/setters of components.""" collective-icalendar-138c845/src/icalendar/tests/attr/test_alarm.py000066400000000000000000000031571524134033600254050ustar00rootroot00000000000000"""Test the properties of the alarm.""" import pytest from icalendar.cal.alarm import Alarm from icalendar.error import InvalidCalendar def test_ACTION_absent(): """An absent ACTION has the documented default.""" assert Alarm().ACTION == "" @pytest.mark.parametrize("action", ["AUDIO", "DISPLAY", "EMAIL"]) def test_ACTION_round_trip(action): """Each RFC 5545 ACTION can be read, serialized, and deleted.""" alarm = Alarm() alarm.ACTION = action assert alarm.ACTION == action assert alarm["ACTION"] == action assert f"ACTION:{action}\r\n".encode() in alarm.to_ical() del alarm.ACTION assert "ACTION" not in alarm def test_repeat_absent(): """Test the absence of REPEAT.""" assert Alarm().REPEAT == 0 def test_repeat_number(): """Test the absence of REPEAT.""" assert Alarm({"REPEAT": 10}).REPEAT == 10 def test_set_REPEAT(): """Check setting the value.""" a = Alarm() a.REPEAT = 10 assert a.REPEAT == 10 def test_set_REPEAT_twice(): """Check setting the value.""" a = Alarm() a.REPEAT = 10 a.REPEAT = 20 assert a.REPEAT == 20 def test_add_REPEAT(): """Check setting the value.""" a = Alarm() a.add("REPEAT", 10) assert a.REPEAT == 10 def test_invalid_repeat_value(): """Check setting the value.""" a = Alarm() with pytest.raises(TypeError): a.REPEAT = "asd" a["REPEAT"] = "asd" with pytest.raises(InvalidCalendar): a.REPEAT # noqa: B018, RUF100 def test_alarm_to_string(): a = Alarm() a.REPEAT = 11 assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n" collective-icalendar-138c845/src/icalendar/tests/attr/test_alarm_factory_methods.py000066400000000000000000000162351524134033600306600ustar00rootroot00000000000000"""Tests for Alarm.new_display(), Alarm.new_audio(), and Alarm.new_email().""" from datetime import datetime, timedelta, timezone import pytest from icalendar.cal.alarm import Alarm from icalendar.error import InvalidCalendar from icalendar.prop import vCalAddress from icalendar.prop.binary import vBinary def test_new_display_sets_action(): alarm = Alarm.new_display("Reminder", timedelta(minutes=-15)) assert alarm["ACTION"] == "DISPLAY" def test_new_display_sets_description(): alarm = Alarm.new_display("Stand-up in 15 min", timedelta(minutes=-15)) assert alarm.description == "Stand-up in 15 min" def test_new_display_sets_relative_trigger(): trigger = timedelta(minutes=-30) alarm = Alarm.new_display("desc", trigger) assert alarm.TRIGGER == trigger def test_new_display_sets_absolute_trigger(): trigger = datetime(2025, 1, 1, 9, 0, tzinfo=timezone.utc) alarm = Alarm.new_display("desc", trigger) assert alarm.TRIGGER == trigger def test_new_display_with_repeat(): alarm = Alarm.new_display( "desc", timedelta(minutes=-10), duration=timedelta(minutes=5), repeat=2 ) assert alarm.DURATION == timedelta(minutes=5) assert alarm.REPEAT == 2 def test_new_display_requires_description(): with pytest.raises(InvalidCalendar, match="description"): Alarm.new_display("", timedelta(minutes=-5)) def test_new_display_requires_trigger(): with pytest.raises(InvalidCalendar, match="trigger"): Alarm.new_display("desc", None) @pytest.mark.parametrize( "alarm_fn", [ lambda: Alarm.new_display( "desc", timedelta(minutes=-5), duration=timedelta(minutes=1) ), lambda: Alarm.new_audio(timedelta(minutes=-5), duration=timedelta(minutes=1)), lambda: Alarm.new_email( "S", "D", timedelta(minutes=-30), vCalAddress("mailto:a@example.com"), duration=timedelta(minutes=5), ), ], ids=["display", "audio", "email"], ) def test_duration_without_repeat_raises(alarm_fn): with pytest.raises(InvalidCalendar, match="DURATION and REPEAT"): alarm_fn() @pytest.mark.parametrize( "alarm_fn", [ lambda: Alarm.new_display("desc", timedelta(minutes=-5), repeat=3), lambda: Alarm.new_audio(timedelta(minutes=-5), repeat=3), lambda: Alarm.new_email( "S", "D", timedelta(minutes=-30), vCalAddress("mailto:a@example.com"), repeat=2, ), ], ids=["display", "audio", "email"], ) def test_repeat_without_duration_raises(alarm_fn): with pytest.raises(InvalidCalendar, match="DURATION and REPEAT"): alarm_fn() def test_new_audio_sets_action(): alarm = Alarm.new_audio(timedelta(minutes=-5)) assert alarm["ACTION"] == "AUDIO" def test_new_audio_sets_trigger(): trigger = timedelta(minutes=-5) alarm = Alarm.new_audio(trigger) assert alarm.TRIGGER == trigger def test_new_audio_without_attach(): alarm = Alarm.new_audio(timedelta(minutes=-5)) assert alarm.get("ATTACH") is None def test_new_audio_with_attach(): uri = "ftp://example.com/pub/sounds/bell-01.aud" alarm = Alarm.new_audio(timedelta(minutes=-5), attach=uri) assert str(alarm["ATTACH"]) == uri def test_new_audio_with_empty_attach_ignored(): alarm = Alarm.new_audio(timedelta(minutes=-5), attach="") assert alarm.get("ATTACH") is None def test_new_audio_with_repeat(): alarm = Alarm.new_audio( timedelta(minutes=-5), duration=timedelta(minutes=2), repeat=3 ) assert alarm.DURATION == timedelta(minutes=2) assert alarm.REPEAT == 3 def test_new_audio_requires_trigger(): with pytest.raises(InvalidCalendar, match="trigger"): Alarm.new_audio(None) def test_new_audio_with_bytes_attach(): data = b"\x00\x01\x02\x03" alarm = Alarm.new_audio(timedelta(minutes=-5), attach=data) assert isinstance(alarm["ATTACH"], vBinary) assert alarm["ATTACH"] == vBinary(data) def test_new_email_sets_action(): alarm = Alarm.new_email( summary="Reminder", description="Your meeting starts soon.", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], ) assert alarm["ACTION"] == "EMAIL" def test_new_email_sets_summary_and_description(): alarm = Alarm.new_email( summary="Subject", description="Body", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], ) assert alarm.summary == "Subject" assert alarm.description == "Body" def test_new_email_sets_attendees(): attendees = [ vCalAddress("mailto:a@example.com"), vCalAddress("mailto:b@example.com"), ] alarm = Alarm.new_email( summary="S", description="D", trigger=timedelta(minutes=-30), attendees=attendees, ) assert len(alarm.attendees) == 2 assert str(alarm.attendees[0]) == "mailto:a@example.com" assert str(alarm.attendees[1]) == "mailto:b@example.com" def test_new_email_with_attachment(): alarm = Alarm.new_email( summary="S", description="D", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], attachments=["https://example.com/file.pdf"], ) assert "ATTACH" in alarm assert str(alarm["ATTACH"]) == "https://example.com/file.pdf" def test_new_email_requires_summary(): with pytest.raises(InvalidCalendar, match="summary"): Alarm.new_email( summary="", description="D", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], ) def test_new_email_requires_description(): with pytest.raises(InvalidCalendar, match="description"): Alarm.new_email( summary="S", description="", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], ) def test_new_email_requires_trigger(): with pytest.raises(InvalidCalendar, match="trigger"): Alarm.new_email( summary="S", description="D", trigger=None, attendees=[vCalAddress("mailto:user@example.com")], ) def test_new_email_requires_attendees(): with pytest.raises(InvalidCalendar, match="attendee"): Alarm.new_email( summary="S", description="D", trigger=timedelta(minutes=-30), attendees=[], ) def test_new_email_single_attendee(): alarm = Alarm.new_email( summary="S", description="D", trigger=timedelta(minutes=-30), attendees=vCalAddress("mailto:user@example.com"), ) assert len(alarm.attendees) == 1 assert str(alarm.attendees[0]) == "mailto:user@example.com" def test_new_email_single_attachment(): alarm = Alarm.new_email( summary="S", description="D", trigger=timedelta(minutes=-30), attendees=[vCalAddress("mailto:user@example.com")], attachments="https://example.com/file.pdf", ) assert "ATTACH" in alarm assert str(alarm["ATTACH"]) == "https://example.com/file.pdf" collective-icalendar-138c845/src/icalendar/tests/attr/test_component.py000066400000000000000000000114101524134033600263020ustar00rootroot00000000000000"""Test common properties of components.""" from datetime import date, datetime, timedelta import pytest from icalendar import vDDDTypes from icalendar.cal.component import Component from icalendar.cal.event import Event from icalendar.cal.free_busy import FreeBusy from icalendar.cal.journal import Journal from icalendar.cal.todo import Todo from icalendar.error import InvalidCalendar @pytest.fixture(params=[Event, Todo, Journal, FreeBusy]) def dtstamp_comp(request): """a component to test""" return request.param() def test_no_dtstamp(dtstamp_comp): """We have None as a value.""" assert dtstamp_comp.DTSTAMP is None def set_dtstamp_attribute(component: Component, value: date): """Use the setter.""" component.DTSTAMP = value def set_dtstamp_item(component: Component, value: date): """Use setitem.""" component["DTSTAMP"] = vDDDTypes(value) def set_dtstamp_add(component: Component, value: date): """Use add.""" component.add("DTSTAMP", value) @pytest.mark.parametrize( ("value", "timezone", "expected"), [ (datetime(2024, 10, 11, 23, 1), None, datetime(2024, 10, 11, 23, 1)), (datetime(2024, 10, 11, 23, 1), "Europe/Berlin", datetime(2024, 10, 11, 21, 1)), (datetime(2024, 10, 11, 22, 1), "UTC", datetime(2024, 10, 11, 22, 1)), (date(2024, 10, 10), None, datetime(2024, 10, 10)), ], ) @pytest.mark.parametrize( "set_dtstamp", [set_dtstamp_add, set_dtstamp_attribute, set_dtstamp_item] ) def test_set_value_and_get_it( dtstamp_comp, value, timezone, expected, tzp, set_dtstamp ): """Set and get the DTSTAMP value.""" dtstamp = value if timezone is None else tzp.localize(value, timezone) set_dtstamp(dtstamp_comp, dtstamp) in_utc = tzp.localize_utc(expected) get_value = dtstamp_comp.get("DTSTAMP").dt assert in_utc == get_value or set_dtstamp != set_dtstamp_attribute assert in_utc == dtstamp_comp.DTSTAMP @pytest.mark.parametrize("invalid_value", ["asdasd", timedelta()]) def test_set_invalid_value(invalid_value, dtstamp_comp): """Check handling of invalid values.""" with pytest.raises(TypeError) as e: dtstamp_comp.DTSTAMP = invalid_value assert e.value.args[0] == f"DTSTAMP takes a datetime in UTC, not {invalid_value}" @pytest.mark.parametrize("invalid_value", ["ashdkjasjkdkd", vDDDTypes(timedelta())]) def test_get_invalid_value(invalid_value, dtstamp_comp): """Check handling of invalid values.""" dtstamp_comp["DTSTAMP"] = invalid_value with pytest.raises(InvalidCalendar) as e: dtstamp_comp.DTSTAMP # noqa: B018, RUF100 assert ( e.value.args[0] == f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}" ) def test_set_twice(dtstamp_comp, tzp): """Set the value twice.""" dtstamp_comp.DTSTAMP = date(2014, 1, 1) dtstamp_comp.DTSTAMP = date(2014, 1, 2) assert tzp.localize_utc(datetime(2014, 1, 2)) == dtstamp_comp.DTSTAMP def test_last_modified(dtstamp_comp, tzp): """Check we can set LAST_MODIFIED in the same way as DTSTAMP""" dtstamp_comp.LAST_MODIFIED = date(2014, 1, 2) assert tzp.localize_utc(datetime(2014, 1, 2)) == dtstamp_comp.LAST_MODIFIED @pytest.fixture(params=["last_modified", "created"]) def attr_depending_on_dtstamp(request): """These parameters default to the DTSTAMP attribute.""" return request.param @pytest.mark.parametrize( "dt", [datetime(2011, 10, 5, 13, 32, 25), datetime(2011, 10, 5)] ) def test_last_modified_defaults_to_dtstamp(dt, tzp, c, attr_depending_on_dtstamp): """In the case of an iCalendar object that doesn't specify a "METHOD" property, "DTSTAMP" property is equivalent to the "LAST-MODIFIED" property.""" c.stamp = dt value = getattr(c, attr_depending_on_dtstamp) assert value == c.stamp def test_set_last_modified(c, attr_depending_on_dtstamp): """Setting last modified does not override the dtstamp.""" setattr(c, attr_depending_on_dtstamp, datetime(2011, 10, 5, 13, 32, 25)) assert c.DTSTAMP is None def test_set_last_modified_2(c, tzp, attr_depending_on_dtstamp): """Setting last modified does not override the dtstamp.""" c.DTSTAMP = datetime(2011, 10, 5) setattr(c, attr_depending_on_dtstamp, datetime(2011, 10, 5, 13, 32, 25)) assert c.DTSTAMP == tzp.localize_utc(datetime(2011, 10, 5)) def test_delete_last_modified_1(c, attr_depending_on_dtstamp): """Delete the absent value.""" delattr(c, attr_depending_on_dtstamp) assert getattr(c, attr_depending_on_dtstamp) is None def test_delete_last_modified_2(c, attr_depending_on_dtstamp): """Delete the present value.""" setattr(c, attr_depending_on_dtstamp, datetime(2011, 10, 5, 13, 32, 25)) delattr(c, attr_depending_on_dtstamp) assert getattr(c, attr_depending_on_dtstamp) is None collective-icalendar-138c845/src/icalendar/tests/attr/test_exdates.py000066400000000000000000000052451524134033600257460ustar00rootroot00000000000000"""This tests the exdate property.""" from __future__ import annotations from datetime import date, datetime import pytest from icalendar import ( Event, Journal, TimezoneDaylight, TimezoneStandard, Todo, ) C_EXDATE = Event | Todo | Journal | TimezoneDaylight | TimezoneStandard @pytest.fixture(params=[Event, Todo, Journal, TimezoneDaylight, TimezoneStandard]) def c_exdate(request) -> C_EXDATE: """Return a component that uses exdate.""" return request.param() @pytest.fixture( params=[ lambda _tzp: date(2019, 10, 11), lambda _tzp: datetime(2000, 1, 13, 12, 1), lambda tzp: tzp.localize_utc(datetime(2031, 12, 1, 23, 59)), lambda tzp: tzp.localize(datetime(1984, 1, 13, 13, 1), "Europe/Athens"), ] ) def exdate(request, tzp): """Possible values for an exdate.""" return request.param(tzp) def test_no_exdates_by_default(c_exdate): """We expect no exdate by default.""" assert c_exdate.exdates == [] def test_set_and_retrieve_exdate(exdate, c_exdate): """Set the attribute and get the value.""" c_exdate.add("exdate", [exdate]) result = [exdate] assert c_exdate.exdates == result def test_set_and_retrieve_exdates_in_list(exdate, c_exdate): """Set the attribute and get the value.""" c_exdate.add("exdate", [exdate, exdate]) result = [exdate, exdate] assert c_exdate.exdates == result def test_set_and_retrieve_exdates_twice(exdate, c_exdate): """Set the attribute and get the value.""" c_exdate.add("exdate", [exdate]) c_exdate.add("exdate", [exdate]) result = [exdate, exdate] assert c_exdate.exdates == result def test_set_exdates_via_property(exdate, c_exdate): """``.exdates`` is settable and replaces any existing EXDATE (#1442).""" c_exdate.add("exdate", [datetime(2020, 1, 1, 12, 0)]) c_exdate.exdates = [exdate] assert c_exdate.exdates == [exdate] def test_set_exdates_round_trips(exdate, c_exdate): """Assigning ``.exdates`` its own value leaves it unchanged (#1442).""" c_exdate.add("exdate", [exdate]) before = c_exdate.exdates c_exdate.exdates = before assert c_exdate.exdates == before def test_del_exdates(exdate, c_exdate): """Deleting ``.exdates`` removes the EXDATE property (#1442).""" c_exdate.add("exdate", [exdate]) del c_exdate.exdates assert c_exdate.exdates == [] assert "EXDATE" not in c_exdate @pytest.mark.parametrize("empty", [[], None]) def test_set_exdates_empty_clears(exdate, c_exdate, empty): """Setting ``.exdates`` to an empty list or ``None`` clears it (#1442).""" c_exdate.add("exdate", [exdate]) c_exdate.exdates = empty assert c_exdate.exdates == [] collective-icalendar-138c845/src/icalendar/tests/attr/test_issue_1594.py000066400000000000000000000100731524134033600261160ustar00rootroot00000000000000"""Tests for validating integer properties with a minimum value. See https://github.com/collective/icalendar/issues/1594 """ from datetime import timedelta import pytest from icalendar import Alarm, Event, InvalidCalendar from icalendar.attr import single_int_property from icalendar.cal.component import Component class NumberComponent(Component): """A component with a single integer property that has a minimum.""" name = "X-NUMBER" number = single_int_property("NUMBER", 0, "A number", min_value=5) @pytest.mark.parametrize("value", [0, 1, 10]) def test_set_non_negative_value(value): """Setting a non-negative value works.""" alarm = Alarm() alarm.REPEAT = value assert alarm.REPEAT == value @pytest.mark.parametrize( ("value", "message"), [ (-1, "REPEAT must be >= 0, got -1"), (-10, "REPEAT must be >= 0, got -10"), ], ) def test_set_negative_value_raises(value, message): """Setting a negative value raises an error with the correct message.""" alarm = Alarm() with pytest.raises(InvalidCalendar, match=message): alarm.REPEAT = value @pytest.mark.parametrize("value", ["asd", 1.5, object()]) def test_set_non_integer_raises(value): """Setting a non-integer value raises a TypeError.""" alarm = Alarm() with pytest.raises(TypeError, match="REPEAT must be an int, got"): alarm.REPEAT = value @pytest.mark.parametrize("value", [True, False]) def test_set_bool_raises(value): """Booleans are rejected too, even though bool is a subclass of int.""" alarm = Alarm() with pytest.raises(TypeError, match="REPEAT must be an int, got"): alarm.REPEAT = value def test_set_non_integer_raises_for_sequence(): """The same TypeError is raised for other integer properties.""" event = Event() with pytest.raises(TypeError, match="SEQUENCE must be an int, got"): event.sequence = "1" def test_set_none_deletes(): """Setting None deletes the property, like before.""" alarm = Alarm() alarm.REPEAT = 3 alarm.REPEAT = None assert alarm.REPEAT == 0 def test_custom_min_value_boundary(): """Exactly min_value is accepted.""" component = NumberComponent() component.number = 5 assert component.number == 5 def test_custom_min_value_below_boundary_raises(): """One below min_value raises with the correct message.""" component = NumberComponent() with pytest.raises(InvalidCalendar, match="NUMBER must be >= 5, got 4"): component.number = 4 @pytest.mark.parametrize("min_value", [0, 5, 10]) def test_different_min_values(min_value): """Different minimal values are enforced at their boundary.""" prop = single_int_property("VALUE", 0, "A value", min_value=min_value) component = type("C", (Component,), {"name": "X-VALUE", "value": prop})() component.value = min_value assert component.value == min_value with pytest.raises(InvalidCalendar, match=f"VALUE must be >= {min_value}"): component.value = min_value - 1 def test_lowercase_repeat_validation(): """The lowercase repeat attribute validates too.""" alarm = Alarm() alarm.repeat = 2 assert alarm.repeat == 2 with pytest.raises(InvalidCalendar, match="REPEAT must be >= 0"): alarm.repeat = -1 def test_sequence_validation(): """The sequence attribute validates too.""" event = Event() event.sequence = 3 assert event.sequence == 3 with pytest.raises(InvalidCalendar, match="SEQUENCE must be >= 0"): event.sequence = -1 def test_priority_validation(): """The priority attribute validates too.""" event = Event() event.priority = 5 assert event.priority == 5 with pytest.raises(InvalidCalendar, match="PRIORITY must be >= 0"): event.priority = -1 def test_factory_methods_reject_negative_repeat(): """Factory methods reject a negative repeat.""" with pytest.raises(InvalidCalendar, match="REPEAT must be >= 0"): Alarm.new_display( "desc", timedelta(minutes=-5), duration=timedelta(minutes=1), repeat=-1, ) collective-icalendar-138c845/src/icalendar/tests/attr/test_rdate.py000066400000000000000000000254751524134033600254170ustar00rootroot00000000000000"""This tests the RDATE property.""" from __future__ import annotations from datetime import date, datetime, timedelta from pprint import pprint from typing import TYPE_CHECKING import pytest from icalendar import vDDDLists, vDDDTypes from icalendar.cal.event import Event from icalendar.cal.journal import Journal from icalendar.cal.timezone import TimezoneDaylight, TimezoneStandard from icalendar.cal.todo import Todo if TYPE_CHECKING: from icalendar.cal.calendar import ( Calendar, ) C_RDATE = Event | Todo | Journal | TimezoneDaylight | TimezoneStandard @pytest.fixture(params=[Event, Todo, Journal, TimezoneDaylight, TimezoneStandard]) def c_rdate(request) -> C_RDATE: """Return a component that uses RDATE.""" return request.param() @pytest.fixture( params=[ lambda _tzp: date(2019, 10, 11), lambda _tzp: datetime(2000, 1, 13, 12, 1), lambda tzp: tzp.localize_utc(datetime(2031, 12, 1, 23, 59)), lambda tzp: tzp.localize(datetime(1984, 1, 13, 13, 1), "Europe/Athens"), lambda _tzp: ((datetime(2000, 1, 13, 12, 1), datetime(2000, 1, 13, 12, 2))), lambda tzp: ( ( tzp.localize_utc(datetime(2001, 1, 13, 12, 1)), tzp.localize_utc(datetime(2001, 1, 13, 12, 2)), ) ), ] ) def rdate(request, tzp): """Possible values for an rdate.""" return request.param(tzp) def test_no_rdates_by_default(c_rdate): """We expect no rdate by default.""" assert c_rdate.rdates == [] def test_set_and_retrieve_rdate(rdate, c_rdate): """Set the attribute and get the value.""" c_rdate.add("RDATE", [rdate]) result = [rdate if isinstance(rdate, tuple) else (rdate, None)] assert c_rdate.rdates == result def test_get_example_0(calendars): """Test an example rdate.""" cal: Calendar = calendars.rfc_5545_RDATE_example event = cal.events[0] assert event.rdates == [(datetime(1997, 7, 14, 12, 30), None)] def test_get_example_1(calendars, tzp): """Test an example rdate.""" cal: Calendar = calendars.rfc_5545_RDATE_example event = cal.events[1] assert event.rdates == [(tzp.localize_utc(datetime(1997, 7, 14, 12, 30)), None)] def test_get_example_2(calendars, tzp): """Test an example rdate.""" cal: Calendar = calendars.rfc_5545_RDATE_example event = cal.events[2] assert event.rdates == [ (tzp.localize(datetime(1997, 7, 14, 8, 30), "America/New_York"), None) ] def test_get_example_3(calendars, tzp): """Test an example rdate.""" cal: Calendar = calendars.rfc_5545_RDATE_example event = cal.events[3] rdates_3 = [ ( tzp.localize_utc(datetime(1996, 4, 3, 2)), tzp.localize_utc(datetime(1996, 4, 3, 4)), ), ( tzp.localize_utc(datetime(1996, 4, 4, 1)), tzp.localize_utc(datetime(1996, 4, 4, 4)), ), ] pprint(event.rdates) pprint(rdates_3) assert event.rdates == rdates_3 def d(i: int) -> tuple[date, None]: s = str(i) return (date(int(s[:4]), int(s[4:6].lstrip("0")), int(s[6:].lstrip("0"))), None) RDATES_4 = list( map( d, ( 19970101, 19970120, 19970217, 19970421, 19970526, 19970704, 19970901, 19971014, 19971128, 19971129, 19971225, ), ) ) def test_get_example_4(calendars, tzp): """Test an example rdate.""" cal: Calendar = calendars.rfc_5545_RDATE_example event = cal.events[4] pprint(event.rdates) pprint(RDATES_4) assert event.rdates == RDATES_4 # Equivalent ways to write a single date that all mean that one date after #1439: # the ``(dt, None)`` rdates form, that form wrapped in a list as ``.rdates`` # returns it, and the same pair as a plain list ``[dt, None]``. SINGLE_DATE = datetime(2025, 4, 28, 16, 5) SINGLE_DATE_FORMS = [(SINGLE_DATE, None), [(SINGLE_DATE, None)], [SINGLE_DATE, None]] @pytest.mark.parametrize("value", SINGLE_DATE_FORMS) def test_add_rdate_with_none_end_sets_a_single_date(c_rdate, value): """``(dt, None)`` is the rdates form of a single date, so adding it sets that one date instead of raising (#1439).""" c_rdate.add("RDATE", value) assert c_rdate.rdates == [(SINGLE_DATE, None)] @pytest.mark.parametrize("value", SINGLE_DATE_FORMS) def test_add_rdate_with_none_end_serializes_like_the_bare_date(c_rdate, value): """Adding ``(dt, None)`` serializes identically to adding the bare ``dt`` — the bug surfaced only at ``to_ical`` time (#1439).""" bare = c_rdate.__class__() bare.add("RDATE", SINGLE_DATE) c_rdate.add("RDATE", value) assert c_rdate.to_ical() == bare.to_ical() def test_rdates_output_can_be_added_back(c_rdate): """The value returned by ``.rdates`` can be fed straight back into ``add`` (#1439).""" c_rdate.add("RDATE", SINGLE_DATE) restored = c_rdate.__class__() restored.add("RDATE", c_rdate.rdates) assert restored.rdates == c_rdate.rdates def test_rdates_output_added_back_serializes_identically(c_rdate): """Re-adding ``.rdates`` also round-trips through serialization (#1439).""" c_rdate.add("RDATE", SINGLE_DATE) restored = c_rdate.__class__() restored.add("RDATE", c_rdate.rdates) assert restored.to_ical() == c_rdate.to_ical() def test_add_rdate_two_datetimes_stays_two_values(c_rdate): """A ``(dt1, dt2)`` tuple keeps its meaning of two separate dates, so only the ``None`` second-element case is treated as a single date (#1439).""" second = datetime(2025, 4, 29, 16, 5) c_rdate.add("RDATE", (SINGLE_DATE, second)) assert c_rdate.rdates == [(SINGLE_DATE, None), (second, None)] @pytest.mark.parametrize("value", [(SINGLE_DATE, None), [SINGLE_DATE, None]]) def test_vdddtypes_treats_none_end_pair_as_a_bare_date(value): """``vDDDTypes`` builds the date itself rather than an invalid period from a ``(dt, None)`` pair, given as a tuple or a list — the lower-level half of #1439.""" assert vDDDTypes(value).dt == SINGLE_DATE @pytest.mark.parametrize("value", [(SINGLE_DATE, None), [SINGLE_DATE, None]]) def test_vdddlists_treats_none_end_pair_as_a_single_date(value): """``vDDDLists`` treats a ``(dt, None)`` pair, tuple or list, as one date instead of iterating it into two values — the other half of #1439.""" assert vDDDLists(value).to_ical() == vDDDLists([SINGLE_DATE]).to_ical() def test_set_all_rdate_value_types_then_round_trip(c_rdate, tzp): """Setting RDATE with one of every accepted value type works, and feeding the resulting ``.rdates`` straight back in leaves value, length and content unchanged (#1439).""" utc_dt = tzp.localize_utc(datetime(2031, 12, 1, 23, 59)) zoned_dt = tzp.localize(datetime(1984, 1, 13, 13, 1), "Europe/Athens") period_start, period_end = ( datetime(2000, 1, 13, 12, 1), datetime(2000, 1, 13, 12, 2), ) dur_start = datetime(2032, 6, 1, 9, 0) # Each accepted value paired with the (start, end) ``.rdates`` should yield. cases = [ (date(2019, 10, 11), (date(2019, 10, 11), None)), # a bare date (datetime(2000, 1, 13, 12, 1), (datetime(2000, 1, 13, 12, 1), None)), # naive (utc_dt, (utc_dt, None)), # a UTC datetime (zoned_dt, (zoned_dt, None)), # a zoned datetime ((period_start, period_end), (period_start, period_end)), # a period ( (dur_start, timedelta(hours=2)), (dur_start, dur_start + timedelta(hours=2)), ), # period as (start, duration) ((SINGLE_DATE, None), (SINGLE_DATE, None)), # the (dt, None) single-date form ] # (1) Setting every value type at once produces the expected rdates. c_rdate.add("RDATE", [value for value, _ in cases]) assert c_rdate.rdates == [result for _, result in cases] # (2) Setting rdates to its own output round-trips without changing anything. restored = c_rdate.__class__() restored.add("RDATE", c_rdate.rdates[:]) assert restored.rdates == c_rdate.rdates def test_set_rdates_via_property(c_rdate): """``.rdates`` is settable and replaces any existing RDATE (#1442).""" c_rdate.add("RDATE", datetime(2020, 1, 1, 12, 0)) c_rdate.rdates = [SINGLE_DATE] assert c_rdate.rdates == [(SINGLE_DATE, None)] def test_set_rdates_via_property_all_value_types(c_rdate, tzp): """Assigning ``.rdates`` a list with one of every accepted value type sets them all through the property setter (#1442, part 1).""" utc_dt = tzp.localize_utc(datetime(2031, 12, 1, 23, 59)) zoned_dt = tzp.localize(datetime(1984, 1, 13, 13, 1), "Europe/Athens") period_start, period_end = ( datetime(2000, 1, 13, 12, 1), datetime(2000, 1, 13, 12, 2), ) dur_start = datetime(2032, 6, 1, 9, 0) values = [ date(2019, 10, 11), # a bare date datetime(2000, 1, 13, 12, 1), # a naive datetime utc_dt, # a UTC datetime zoned_dt, # a zoned datetime (period_start, period_end), # a period (dur_start, timedelta(hours=2)), # a period given as (start, duration) (SINGLE_DATE, None), # the (dt, None) single-date form ] c_rdate.rdates = values assert c_rdate.rdates == [ (date(2019, 10, 11), None), (datetime(2000, 1, 13, 12, 1), None), (utc_dt, None), (zoned_dt, None), (period_start, period_end), (dur_start, dur_start + timedelta(hours=2)), (SINGLE_DATE, None), ] def test_set_rdates_via_property_its_own_output_round_trips(c_rdate, tzp): """Assigning ``.rdates`` its own getter output (``c.rdates = c.rdates[:]``) after setting every value type leaves value, length and content unchanged (#1442, part 2).""" c_rdate.rdates = [ date(2019, 10, 11), datetime(2000, 1, 13, 12, 1), tzp.localize_utc(datetime(2031, 12, 1, 23, 59)), (datetime(2000, 1, 13, 12, 1), datetime(2000, 1, 13, 12, 2)), (SINGLE_DATE, None), ] before = c_rdate.rdates c_rdate.rdates = c_rdate.rdates[:] assert c_rdate.rdates == before assert len(c_rdate.rdates) == len(before) def test_set_rdates_round_trips(c_rdate, rdate): """Assigning ``.rdates`` its own value leaves it unchanged for every value type (#1442).""" c_rdate.add("RDATE", [rdate]) before = c_rdate.rdates c_rdate.rdates = before assert c_rdate.rdates == before def test_del_rdates(c_rdate): """Deleting ``.rdates`` removes the RDATE property (#1442).""" c_rdate.add("RDATE", SINGLE_DATE) del c_rdate.rdates assert c_rdate.rdates == [] assert "RDATE" not in c_rdate @pytest.mark.parametrize("empty", [[], None]) def test_set_rdates_empty_clears(c_rdate, empty): """Setting ``.rdates`` to an empty list or ``None`` clears it (#1442).""" c_rdate.add("RDATE", SINGLE_DATE) c_rdate.rdates = empty assert c_rdate.rdates == [] collective-icalendar-138c845/src/icalendar/tests/attr/test_rrule.py000066400000000000000000000026271524134033600254430ustar00rootroot00000000000000"""Test getting the rrules from a component.""" import pytest from icalendar import ( vRecur, ) from icalendar.cal.component import Component from icalendar.cal.event import Event from icalendar.cal.journal import Journal from icalendar.cal.timezone import TimezoneDaylight, TimezoneStandard from icalendar.cal.todo import Todo RRULE_0 = vRecur.from_ical("FREQ=DAILY;COUNT=10") RRULE_1 = vRecur.from_ical("FREQ=DAILY;UNTIL=19971224T000000Z") RRULE_2 = vRecur.from_ical("FREQ=DAILY;INTERVAL=2") RRULE_3 = vRecur.from_ical("FREQ=DAILY;INTERVAL=10;COUNT=5") RRULE_4 = vRecur.from_ical( "FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA" ) @pytest.fixture(params=[RRULE_0, RRULE_1, RRULE_2, RRULE_3, RRULE_4]) def rrule(request) -> str: """An rrule.""" return request.param @pytest.fixture(params=[Event, Todo, Journal, TimezoneDaylight, TimezoneStandard]) def c_rrule(request) -> Component: """Return a component that uses RDATE.""" return request.param() def test_no_rrules_by_default(c_rrule): """We expect no rdate by default.""" assert c_rrule.rrules == [] def test_one_rrule(c_rrule, rrule): """Add one rrule.""" c_rrule.add("rrule", rrule) assert c_rrule.rrules == [rrule] def test_two_rrules(c_rrule, rrule): """Add two rrules.""" c_rrule.add("rrule", rrule) c_rrule.add("rrule", RRULE_3) assert c_rrule.rrules == [rrule, RRULE_3] collective-icalendar-138c845/src/icalendar/tests/availabilities/000077500000000000000000000000001524134033600247025ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/availabilities/rfc_7953_1.ics000066400000000000000000000006011524134033600270600ustar00rootroot00000000000000BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:0428C7D2-688E-4D2E-AC52-CD112E2469DF DTSTAMP:20111005T133225Z BEGIN:AVAILABLE UID:34EDA59B-6BB1-4E94-A66C-64999089C0AF SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111002T090000 DTEND;TZID=America/Montreal:20111002T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR END:AVAILABLE END:VAVAILABILITY collective-icalendar-138c845/src/icalendar/tests/availabilities/rfc_7953_2.ics000066400000000000000000000013331524134033600270640ustar00rootroot00000000000000BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:84D0F948-7FC6-4C1D-BBF3-BA9827B424B5 DTSTAMP:20111005T133225Z DTSTART;TZID=America/Montreal:20111002T000000 DTEND;TZID=America/Montreal:20111202T000000 BEGIN:AVAILABLE UID:7B33093A-7F98-4EED-B381-A5652530F04D SUMMARY:Monday to Thursday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111002T090000 DTEND;TZID=America/Montreal:20111002T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH LOCATION:Main Office END:AVAILABLE BEGIN:AVAILABLE UID:DF39DC9E-D8C3-492F-9101-0434E8FC1896 SUMMARY:Friday from 9:00 to 12:00 DTSTART;TZID=America/Montreal:20111006T090000 DTEND;TZID=America/Montreal:20111006T120000 RRULE:FREQ=WEEKLY LOCATION:Branch Office END:AVAILABLE END:VAVAILABILITY collective-icalendar-138c845/src/icalendar/tests/calendars/000077500000000000000000000000001524134033600236545ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_etar_future.ics000066400000000000000000000120721524134033600300570ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN VERSION:2.0 METHOD:PUBLISH CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:Europe/London TZURL:http://tzurl.org/zoneinfo/Europe/London X-LIC-LOCATION:Europe/London BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19961027T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:-000115 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:18471201T000115 RDATE:18471201T000115 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19160521T020000 RDATE:19160521T020000 RDATE:19170408T020000 RDATE:19180324T020000 RDATE:19190330T020000 RDATE:19200328T020000 RDATE:19210403T020000 RDATE:19220326T020000 RDATE:19230422T020000 RDATE:19240413T020000 RDATE:19250419T020000 RDATE:19260418T020000 RDATE:19270410T020000 RDATE:19280422T020000 RDATE:19290421T020000 RDATE:19300413T020000 RDATE:19310419T020000 RDATE:19320417T020000 RDATE:19330409T020000 RDATE:19340422T020000 RDATE:19350414T020000 RDATE:19360419T020000 RDATE:19370418T020000 RDATE:19380410T020000 RDATE:19390416T020000 RDATE:19400225T020000 RDATE:19460414T020000 RDATE:19470316T020000 RDATE:19480314T020000 RDATE:19490403T020000 RDATE:19500416T020000 RDATE:19510415T020000 RDATE:19520420T020000 RDATE:19530419T020000 RDATE:19540411T020000 RDATE:19550417T020000 RDATE:19560422T020000 RDATE:19570414T020000 RDATE:19580420T020000 RDATE:19590419T020000 RDATE:19600410T020000 RDATE:19610326T020000 RDATE:19620325T020000 RDATE:19630331T020000 RDATE:19640322T020000 RDATE:19650321T020000 RDATE:19660320T020000 RDATE:19670319T020000 RDATE:19680218T020000 RDATE:19720319T020000 RDATE:19730318T020000 RDATE:19740317T020000 RDATE:19750316T020000 RDATE:19760321T020000 RDATE:19770320T020000 RDATE:19780319T020000 RDATE:19790318T020000 RDATE:19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19161001T030000 RDATE:19161001T030000 RDATE:19170917T030000 RDATE:19180930T030000 RDATE:19190929T030000 RDATE:19201025T030000 RDATE:19211003T030000 RDATE:19221008T030000 RDATE:19230916T030000 RDATE:19240921T030000 RDATE:19251004T030000 RDATE:19261003T030000 RDATE:19271002T030000 RDATE:19281007T030000 RDATE:19291006T030000 RDATE:19301005T030000 RDATE:19311004T030000 RDATE:19321002T030000 RDATE:19331008T030000 RDATE:19341007T030000 RDATE:19351006T030000 RDATE:19361004T030000 RDATE:19371003T030000 RDATE:19381002T030000 RDATE:19391119T030000 RDATE:19451007T030000 RDATE:19461006T030000 RDATE:19471102T030000 RDATE:19481031T030000 RDATE:19491030T030000 RDATE:19501022T030000 RDATE:19511021T030000 RDATE:19521026T030000 RDATE:19531004T030000 RDATE:19541003T030000 RDATE:19551002T030000 RDATE:19561007T030000 RDATE:19571006T030000 RDATE:19581005T030000 RDATE:19591004T030000 RDATE:19601002T030000 RDATE:19611029T030000 RDATE:19621028T030000 RDATE:19631027T030000 RDATE:19641025T030000 RDATE:19651024T030000 RDATE:19661023T030000 RDATE:19671029T030000 RDATE:19711031T030000 RDATE:19721029T030000 RDATE:19731028T030000 RDATE:19741027T030000 RDATE:19751026T030000 RDATE:19761024T030000 RDATE:19771023T030000 RDATE:19781029T030000 RDATE:19791028T030000 RDATE:19801026T030000 RDATE:19811025T020000 RDATE:19821024T020000 RDATE:19831023T020000 RDATE:19841028T020000 RDATE:19851027T020000 RDATE:19861026T020000 RDATE:19871025T020000 RDATE:19881023T020000 RDATE:19891029T020000 RDATE:19901028T020000 RDATE:19911027T020000 RDATE:19921025T020000 RDATE:19931024T020000 RDATE:19941023T020000 RDATE:19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:BDST DTSTART:19410504T010000 RDATE:19410504T010000 RDATE:19420405T010000 RDATE:19430404T010000 RDATE:19440402T010000 RDATE:19450402T010000 RDATE:19470413T010000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19410810T030000 RDATE:19410810T030000 RDATE:19420809T030000 RDATE:19430815T030000 RDATE:19440917T030000 RDATE:19450715T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19681026T230000 RDATE:19681026T230000 END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+0000 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19960101T000000 RDATE:19960101T000000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20241005T112701Z UID:17281276213728ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org SUMMARY:event with alarms android STATUS:CONFIRMED DTSTART;TZID=Europe/London:20241005T130000 DTEND:20241005T130000Z LAST-MODIFIED:20241005T112701Z BEGIN:VALARM TRIGGER:-PT30M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM BEGIN:VALARM TRIGGER:-PT25M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM BEGIN:VALARM TRIGGER:-PT5M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_etar_notification.ics000066400000000000000000000120721524134033600312330ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN VERSION:2.0 METHOD:PUBLISH CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:Europe/London TZURL:http://tzurl.org/zoneinfo/Europe/London X-LIC-LOCATION:Europe/London BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19961027T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:-000115 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:18471201T000115 RDATE:18471201T000115 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19160521T020000 RDATE:19160521T020000 RDATE:19170408T020000 RDATE:19180324T020000 RDATE:19190330T020000 RDATE:19200328T020000 RDATE:19210403T020000 RDATE:19220326T020000 RDATE:19230422T020000 RDATE:19240413T020000 RDATE:19250419T020000 RDATE:19260418T020000 RDATE:19270410T020000 RDATE:19280422T020000 RDATE:19290421T020000 RDATE:19300413T020000 RDATE:19310419T020000 RDATE:19320417T020000 RDATE:19330409T020000 RDATE:19340422T020000 RDATE:19350414T020000 RDATE:19360419T020000 RDATE:19370418T020000 RDATE:19380410T020000 RDATE:19390416T020000 RDATE:19400225T020000 RDATE:19460414T020000 RDATE:19470316T020000 RDATE:19480314T020000 RDATE:19490403T020000 RDATE:19500416T020000 RDATE:19510415T020000 RDATE:19520420T020000 RDATE:19530419T020000 RDATE:19540411T020000 RDATE:19550417T020000 RDATE:19560422T020000 RDATE:19570414T020000 RDATE:19580420T020000 RDATE:19590419T020000 RDATE:19600410T020000 RDATE:19610326T020000 RDATE:19620325T020000 RDATE:19630331T020000 RDATE:19640322T020000 RDATE:19650321T020000 RDATE:19660320T020000 RDATE:19670319T020000 RDATE:19680218T020000 RDATE:19720319T020000 RDATE:19730318T020000 RDATE:19740317T020000 RDATE:19750316T020000 RDATE:19760321T020000 RDATE:19770320T020000 RDATE:19780319T020000 RDATE:19790318T020000 RDATE:19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19161001T030000 RDATE:19161001T030000 RDATE:19170917T030000 RDATE:19180930T030000 RDATE:19190929T030000 RDATE:19201025T030000 RDATE:19211003T030000 RDATE:19221008T030000 RDATE:19230916T030000 RDATE:19240921T030000 RDATE:19251004T030000 RDATE:19261003T030000 RDATE:19271002T030000 RDATE:19281007T030000 RDATE:19291006T030000 RDATE:19301005T030000 RDATE:19311004T030000 RDATE:19321002T030000 RDATE:19331008T030000 RDATE:19341007T030000 RDATE:19351006T030000 RDATE:19361004T030000 RDATE:19371003T030000 RDATE:19381002T030000 RDATE:19391119T030000 RDATE:19451007T030000 RDATE:19461006T030000 RDATE:19471102T030000 RDATE:19481031T030000 RDATE:19491030T030000 RDATE:19501022T030000 RDATE:19511021T030000 RDATE:19521026T030000 RDATE:19531004T030000 RDATE:19541003T030000 RDATE:19551002T030000 RDATE:19561007T030000 RDATE:19571006T030000 RDATE:19581005T030000 RDATE:19591004T030000 RDATE:19601002T030000 RDATE:19611029T030000 RDATE:19621028T030000 RDATE:19631027T030000 RDATE:19641025T030000 RDATE:19651024T030000 RDATE:19661023T030000 RDATE:19671029T030000 RDATE:19711031T030000 RDATE:19721029T030000 RDATE:19731028T030000 RDATE:19741027T030000 RDATE:19751026T030000 RDATE:19761024T030000 RDATE:19771023T030000 RDATE:19781029T030000 RDATE:19791028T030000 RDATE:19801026T030000 RDATE:19811025T020000 RDATE:19821024T020000 RDATE:19831023T020000 RDATE:19841028T020000 RDATE:19851027T020000 RDATE:19861026T020000 RDATE:19871025T020000 RDATE:19881023T020000 RDATE:19891029T020000 RDATE:19901028T020000 RDATE:19911027T020000 RDATE:19921025T020000 RDATE:19931024T020000 RDATE:19941023T020000 RDATE:19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:BDST DTSTART:19410504T010000 RDATE:19410504T010000 RDATE:19420405T010000 RDATE:19430404T010000 RDATE:19440402T010000 RDATE:19450402T010000 RDATE:19470413T010000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19410810T030000 RDATE:19410810T030000 RDATE:19420809T030000 RDATE:19430815T030000 RDATE:19440917T030000 RDATE:19450715T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19681026T230000 RDATE:19681026T230000 END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+0000 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19960101T000000 RDATE:19960101T000000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20241005T113036Z UID:17281276213728ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org SUMMARY:event with alarms android STATUS:CONFIRMED DTSTART;TZID=Europe/London:20241005T130000 DTEND:20241005T130000Z LAST-MODIFIED:20241005T112701Z BEGIN:VALARM TRIGGER:-PT30M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM BEGIN:VALARM TRIGGER:-PT25M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM BEGIN:VALARM TRIGGER:-PT5M ACTION:DISPLAY DESCRIPTION:event with alarms android END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_etar_notification_clicked.ics000066400000000000000000000116011524134033600327060ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Offline Calendar//iCal Import/Export 2.8.1//EN VERSION:2.0 METHOD:PUBLISH CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:Europe/London TZURL:http://tzurl.org/zoneinfo/Europe/London X-LIC-LOCATION:Europe/London BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19961027T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:-000115 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:18471201T000115 RDATE:18471201T000115 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0000 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19160521T020000 RDATE:19160521T020000 RDATE:19170408T020000 RDATE:19180324T020000 RDATE:19190330T020000 RDATE:19200328T020000 RDATE:19210403T020000 RDATE:19220326T020000 RDATE:19230422T020000 RDATE:19240413T020000 RDATE:19250419T020000 RDATE:19260418T020000 RDATE:19270410T020000 RDATE:19280422T020000 RDATE:19290421T020000 RDATE:19300413T020000 RDATE:19310419T020000 RDATE:19320417T020000 RDATE:19330409T020000 RDATE:19340422T020000 RDATE:19350414T020000 RDATE:19360419T020000 RDATE:19370418T020000 RDATE:19380410T020000 RDATE:19390416T020000 RDATE:19400225T020000 RDATE:19460414T020000 RDATE:19470316T020000 RDATE:19480314T020000 RDATE:19490403T020000 RDATE:19500416T020000 RDATE:19510415T020000 RDATE:19520420T020000 RDATE:19530419T020000 RDATE:19540411T020000 RDATE:19550417T020000 RDATE:19560422T020000 RDATE:19570414T020000 RDATE:19580420T020000 RDATE:19590419T020000 RDATE:19600410T020000 RDATE:19610326T020000 RDATE:19620325T020000 RDATE:19630331T020000 RDATE:19640322T020000 RDATE:19650321T020000 RDATE:19660320T020000 RDATE:19670319T020000 RDATE:19680218T020000 RDATE:19720319T020000 RDATE:19730318T020000 RDATE:19740317T020000 RDATE:19750316T020000 RDATE:19760321T020000 RDATE:19770320T020000 RDATE:19780319T020000 RDATE:19790318T020000 RDATE:19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19161001T030000 RDATE:19161001T030000 RDATE:19170917T030000 RDATE:19180930T030000 RDATE:19190929T030000 RDATE:19201025T030000 RDATE:19211003T030000 RDATE:19221008T030000 RDATE:19230916T030000 RDATE:19240921T030000 RDATE:19251004T030000 RDATE:19261003T030000 RDATE:19271002T030000 RDATE:19281007T030000 RDATE:19291006T030000 RDATE:19301005T030000 RDATE:19311004T030000 RDATE:19321002T030000 RDATE:19331008T030000 RDATE:19341007T030000 RDATE:19351006T030000 RDATE:19361004T030000 RDATE:19371003T030000 RDATE:19381002T030000 RDATE:19391119T030000 RDATE:19451007T030000 RDATE:19461006T030000 RDATE:19471102T030000 RDATE:19481031T030000 RDATE:19491030T030000 RDATE:19501022T030000 RDATE:19511021T030000 RDATE:19521026T030000 RDATE:19531004T030000 RDATE:19541003T030000 RDATE:19551002T030000 RDATE:19561007T030000 RDATE:19571006T030000 RDATE:19581005T030000 RDATE:19591004T030000 RDATE:19601002T030000 RDATE:19611029T030000 RDATE:19621028T030000 RDATE:19631027T030000 RDATE:19641025T030000 RDATE:19651024T030000 RDATE:19661023T030000 RDATE:19671029T030000 RDATE:19711031T030000 RDATE:19721029T030000 RDATE:19731028T030000 RDATE:19741027T030000 RDATE:19751026T030000 RDATE:19761024T030000 RDATE:19771023T030000 RDATE:19781029T030000 RDATE:19791028T030000 RDATE:19801026T030000 RDATE:19811025T020000 RDATE:19821024T020000 RDATE:19831023T020000 RDATE:19841028T020000 RDATE:19851027T020000 RDATE:19861026T020000 RDATE:19871025T020000 RDATE:19881023T020000 RDATE:19891029T020000 RDATE:19901028T020000 RDATE:19911027T020000 RDATE:19921025T020000 RDATE:19931024T020000 RDATE:19941023T020000 RDATE:19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:BDST DTSTART:19410504T010000 RDATE:19410504T010000 RDATE:19420405T010000 RDATE:19430404T010000 RDATE:19440402T010000 RDATE:19450402T010000 RDATE:19470413T010000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19410810T030000 RDATE:19410810T030000 RDATE:19420809T030000 RDATE:19430815T030000 RDATE:19440917T030000 RDATE:19450715T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0100 TZNAME:BST DTSTART:19681026T230000 RDATE:19681026T230000 END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+0000 TZOFFSETTO:+0000 TZNAME:GMT DTSTART:19960101T000000 RDATE:19960101T000000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20241005T130738Z UID:17281336589228ad54d03afa44d1ca60b8c52afaece9e@sufficientlysecure.org SUMMARY:event with alarm acknowledged STATUS:CONFIRMED DTSTART;TZID=Europe/London:20241005T141700 DTEND:20241005T141700Z LAST-MODIFIED:20241005T130738Z BEGIN:VALARM TRIGGER:-PT10M ACTION:DISPLAY DESCRIPTION:event with alarm acknowledged END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_google_acknowledged.ics000066400000000000000000000024561524134033600315220ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Nicco Kunzmann X-WR-TIMEZONE:Europe/London BEGIN:VTIMEZONE TZID:Europe/Berlin X-LIC-LOCATION:Europe/Berlin BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:GMT+2 DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:GMT+1 DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART:20241004T181500Z DTEND:20241004T190000Z DTSTAMP:20241004T180026Z UID:79fs7pkqvht9m5igs0vjv1sfra@google.com CREATED:20241004T175920Z LAST-MODIFIED:20241004T175928Z SEQUENCE:0 STATUS:CONFIRMED SUMMARY:event with alarms TRANSP:OPAQUE BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H10M0S DESCRIPTION:This is an event reminder END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H14M0S DESCRIPTION:This is an event reminder END:VALARM BEGIN:VALARM ACTION:EMAIL ATTENDEE:mailto:niccokunzmann@googlemail.com TRIGGER:-P0DT0H15M0S DESCRIPTION:This is an event reminder SUMMARY:Alarm notification END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H15M0S DESCRIPTION:This is an event reminder END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_google_future.ics000066400000000000000000000024561524134033600304050ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Nicco Kunzmann X-WR-TIMEZONE:Europe/London BEGIN:VTIMEZONE TZID:Europe/Berlin X-LIC-LOCATION:Europe/Berlin BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:GMT+2 DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:GMT+1 DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART:20241004T181500Z DTEND:20241004T190000Z DTSTAMP:20241004T175945Z UID:79fs7pkqvht9m5igs0vjv1sfra@google.com CREATED:20241004T175920Z LAST-MODIFIED:20241004T175928Z SEQUENCE:0 STATUS:CONFIRMED SUMMARY:event with alarms TRANSP:OPAQUE BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H10M0S DESCRIPTION:This is an event reminder END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H14M0S DESCRIPTION:This is an event reminder END:VALARM BEGIN:VALARM ACTION:EMAIL ATTENDEE:mailto:niccokunzmann@googlemail.com TRIGGER:-P0DT0H15M0S DESCRIPTION:This is an event reminder SUMMARY:Alarm notification END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-P0DT0H15M0S DESCRIPTION:This is an event reminder END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_thunderbird_2_future.ics000066400000000000000000000335541524134033600316670ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T173412Z LAST-MODIFIED:20241023T173453Z DTSTAMP:20241023T173453Z UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 SUMMARY:event DTSTART;TZID=Europe/London:20241023T190000 DTEND;TZID=Europe/London:20241023T200000 TRANSP:OPAQUE X-MOZ-GENERATION:2 BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT1M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT24M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR alarm_thunderbird_2_notification_5_min_postponed.ics000066400000000000000000000336601524134033600361440ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T173412Z LAST-MODIFIED:20241023T173630Z DTSTAMP:20241023T173630Z UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 SUMMARY:event X-MOZ-LASTACK:20241023T173630Z DTSTART;TZID=Europe/London:20241023T190000 DTEND;TZID=Europe/London:20241023T200000 TRANSP:OPAQUE X-MOZ-GENERATION:3 X-MOZ-SNOOZE-TIME:20241023T174130Z BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT1M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT24M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR alarm_thunderbird_2_notification_5_min_postponed_and_closed.ics000066400000000000000000000336141524134033600403160ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T173412Z LAST-MODIFIED:20241023T174207Z DTSTAMP:20241023T174207Z UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 SUMMARY:event X-MOZ-LASTACK:20241023T174207Z DTSTART;TZID=Europe/London:20241023T190000 DTEND;TZID=Europe/London:20241023T200000 TRANSP:OPAQUE X-MOZ-GENERATION:4 BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT1M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT24M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR alarm_thunderbird_2_notification_5_min_postponed_and_popped_up.ics000066400000000000000000000336601524134033600410410ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T173412Z LAST-MODIFIED:20241023T173630Z DTSTAMP:20241023T173630Z UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 SUMMARY:event X-MOZ-LASTACK:20241023T173630Z DTSTART;TZID=Europe/London:20241023T190000 DTEND;TZID=Europe/London:20241023T200000 TRANSP:OPAQUE X-MOZ-GENERATION:3 X-MOZ-SNOOZE-TIME:20241023T174130Z BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT1M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT24M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR alarm_thunderbird_2_notification_popped_up.ics000066400000000000000000000335541524134033600350370ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T173412Z LAST-MODIFIED:20241023T173453Z DTSTAMP:20241023T173453Z UID:731b9b91-cf72-499b-bbc9-c53c28e21fc7 SUMMARY:event DTSTART;TZID=Europe/London:20241023T190000 DTEND;TZID=Europe/London:20241023T200000 TRANSP:OPAQUE X-MOZ-GENERATION:2 BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT1M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT24M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_thunderbird_closed.ics000066400000000000000000000336311524134033600314010ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T131035Z LAST-MODIFIED:20241023T141941Z DTSTAMP:20241023T141941Z UID:b9a23b47-f109-4e7a-908c-75e925b27def SUMMARY:event with alarms X-MOZ-LASTACK:20241023T141941Z DTSTART;TZID=Europe/London:20241023T150000 DTEND;TZID=Europe/London:20241023T160000 TRANSP:OPAQUE X-MOZ-GENERATION:6 BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT15M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT45M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_thunderbird_future.ics000066400000000000000000000335711524134033600314450ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T131035Z LAST-MODIFIED:20241023T131141Z DTSTAMP:20241023T131141Z UID:b9a23b47-f109-4e7a-908c-75e925b27def SUMMARY:event with alarms DTSTART;TZID=Europe/London:20241023T150000 DTEND;TZID=Europe/London:20241023T160000 TRANSP:OPAQUE X-MOZ-GENERATION:2 BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT15M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT45M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/alarm_thunderbird_snoozed_until_1457.ics000066400000000000000000000336751524134033600335140ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/London X-TZINFO:Europe/London[2024a] BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:-000115 TZNAME:Europe/London(STD) DTSTART:18471201T000000 RDATE:18471201T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19160521T020000 RDATE:19160521T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19161001T030000 RDATE:19161001T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19170408T020000 RDATE:19170408T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19170917T030000 RDATE:19170917T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19180324T020000 RDATE:19180324T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19180930T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1MO;UNTIL=19190929T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19190330T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19201025T030000 RDATE:19201025T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19210403T020000 RDATE:19210403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19211003T030000 RDATE:19211003T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19220326T020000 RDATE:19220326T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19221008T030000 RDATE:19221008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19230422T020000 RDATE:19230422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19240413T020000 RDATE:19240413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19230916T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3SU;UNTIL=19240921T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19250419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19260418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19270410T020000 RDATE:19270410T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19280422T020000 RDATE:19280422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19290421T020000 RDATE:19290421T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19300413T020000 RDATE:19300413T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19310419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19320417T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19251004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19330409T020000 RDATE:19330409T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19331008T030000 RDATE:19331008T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19340422T020000 RDATE:19340422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19350414T020000 RDATE:19350414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19360419T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19370418T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19380410T020000 RDATE:19380410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19341007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19390416T020000 RDATE:19390416T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19400225T020000 RDATE:19400225T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19410504T020000 RDATE:19410504T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19410810T030000 RDATE:19410810T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19420405T020000 RDATE:19420405T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19420809T030000 RDATE:19420809T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19430404T020000 RDATE:19430404T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19430815T030000 RDATE:19430815T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19440402T020000 RDATE:19440402T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19440917T030000 RDATE:19440917T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19450402T020000 RDATE:19450402T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19391119T030000 RDATE:19391119T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19450715T030000 RDATE:19450715T030000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19460414T020000 RDATE:19460414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19470316T020000 RDATE:19470316T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+020000 TZOFFSETFROM:+010000 TZNAME:Europe/London(DST) DTSTART:19470413T020000 RDATE:19470413T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19451007T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19461006T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+020000 TZNAME:Europe/London(DST) DTSTART:19470810T030000 RDATE:19470810T030000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19471102T030000 RDATE:19471102T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19480314T020000 RDATE:19480314T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19490403T020000 RDATE:19490403T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19481031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19491030T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19501022T030000 RDATE:19501022T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19511021T030000 RDATE:19511021T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19521026T030000 RDATE:19521026T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19500416T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19530419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19540411T020000 RDATE:19540411T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19550417T020000 RDATE:19550417T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19560422T020000 RDATE:19560422T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19570414T020000 RDATE:19570414T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19580420T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3SU;UNTIL=19590419T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19600410T020000 RDATE:19600410T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19531004T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19601002T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19610326T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19630331T020000 END:DAYLIGHT BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19640322T020000 RDATE:19640322T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19611029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19641025T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19651024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19661023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19650321T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19670319T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19671029T030000 RDATE:19671029T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+010000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19681027T000000 RDATE:19681027T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19680218T020000 RDATE:19680218T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19711031T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19751026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19761024T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19771023T030000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19720319T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=3SU;UNTIL=19800316T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19781029T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19801026T030000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19811025T020000 RDATE:19811025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19821024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19831023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19841028T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19871025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19881023T020000 RDATE:19881023T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19891029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19921025T020000 END:STANDARD BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19931024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=4SU;UNTIL=19951022T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:Europe/London(DST) DTSTART:19810329T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T010000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:Europe/London(STD) DTSTART:19961027T020000 RDATE:19961027T020000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETTO:+010000 TZOFFSETFROM:+000000 TZNAME:(DST) DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETTO:+000000 TZOFFSETFROM:+010000 TZNAME:(STD) DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241023T131035Z LAST-MODIFIED:20241023T135202Z DTSTAMP:20241023T135202Z UID:b9a23b47-f109-4e7a-908c-75e925b27def SUMMARY:event with alarms X-MOZ-LASTACK:20241023T135202Z DTSTART;TZID=Europe/London:20241023T150000 DTEND;TZID=Europe/London:20241023T160000 TRANSP:OPAQUE X-MOZ-GENERATION:4 X-MOZ-SNOOZE-TIME:20241023T135702Z BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT15M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM BEGIN:VALARM ACTION:DISPLAY TRIGGER:-PT45M DESCRIPTION:Mozilla Standardbeschreibung END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/america_new_york.ics000066400000000000000000000025341524134033600276760ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:custom_America/New_York LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T080000 DTEND;TZID=custom_America/New_York;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/america_new_york_forward_reference.ics000066400000000000000000000026241524134033600334400ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_America/New_York_Forward_reference;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_America/New_York_Forward_reference;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT BEGIN:VTIMEZONE TZID:custom_America/New_York_Forward_reference LAST-MODIFIED:20050809T050000Z BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19740106T020000 RDATE:19750223T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD END:VTIMEZONE END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/big_bad_calendar.ics000066400000000000000000000007601524134033600275570ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT collective-icalendar-138c845/src/icalendar/tests/calendars/bom_calendar.ics000066400000000000000000000000431524134033600267570ustar00rootroot00000000000000BEGIN:VCALENDAR END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/broken_dtstart.ics000066400000000000000000000001601524134033600273760ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:INVALID-DATE END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/broken_ical.ics000066400000000000000000000002201524134033600266160ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:An Event with too many semicolons DTSTART;;VALUE=DATE-TIME:20140409T093000 UID:abc END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/calendar_with_unicode.ics000066400000000000000000000003061524134033600306650ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Plönë.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:äöü ÄÖÜ € X-WR-CALDESC:test non ascii: äöü ÄÖÜ € X-WR-RELCALID:12345 END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/created_calendar_with_unicode_fields.ics000066400000000000000000000010631524134033600337030ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Plönë.org//NONSGML plone.app.event//EN X-WR-CALDESC:test non ascii: äöü ÄÖÜ € X-WR-CALNAME:äöü ÄÖÜ € X-WR-RELCALID:12345 BEGIN:VEVENT SUMMARY:Non-ASCII Test: ÄÖÜ äöü € DTSTART:20101010T100000Z DTEND:20101010T120000Z UID:123456 CREATED:20101010T000000Z DESCRIPTION:icalendar should be able to de/serialize non-ascii. LOCATION:Tribstrül END:VEVENT BEGIN:VEVENT SUMMARY:åäö DTSTART:20101010T000000Z END:VEVENT BEGIN:VEVENT DESCRIPTION:äöüßÄÖÜ END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/empty.ics000066400000000000000000000000361524134033600255110ustar00rootroot00000000000000BEGIN:VCALENDAR END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/empty_RDATE.ics000066400000000000000000000004131524134033600264270ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Events Calendar//iCal4j 1.0//EN CALSCALE:GREGORIAN VERSION:2.0 METHOD:PUBLISH BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19160430T230000 RDATE: RDATE: RDATE: RDATE: RDATE: RDATE: RDATE: END:DAYLIGHT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/example.ics000066400000000000000000000015071524134033600260120ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:collective/icalendar CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Holidays X-WR-TIMEZONE:Etc/GMT BEGIN:VEVENT SUMMARY:New Year's Day DTSTART:20220101 DTEND:20220101 DESCRIPTION:Happy New Year! UID:636a0cc1dbd5a1667894465@icalendar DTSTAMP:20221108T080105Z STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT BEGIN:VEVENT SUMMARY:Orthodox Christmas DTSTART:20220107 DTEND:20220107 LOCATION:Russia DESCRIPTION:It is Christmas again! UID:636a0cc1dbfd91667894465@icalendar STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT BEGIN:VEVENT SUMMARY:International Women's Day DTSTART:20220308 DTEND:20220308 DESCRIPTION:May the feminine be honoured! UID:636a0cc1dc0f11667894465@icalendar STATUS:CONFIRMED TRANSP:TRANSPARENT SEQUENCE:0 END:VEVENT END:VCALENDARfuzz_testcase_0_char_in_component_name.ics000066400000000000000000000000101524134033600341410ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBeGIN: collective-icalendar-138c845/src/icalendar/tests/calendars/fuzz_testcase_invalid_month.ics000066400000000000000000000000311524134033600321520ustar00rootroot00000000000000BEGIN:V RRULE:%n;BYMONTH=collective-icalendar-138c845/src/icalendar/tests/calendars/fuzz_testcase_vtimezone_lone_cr.ics000066400000000000000000000000551524134033600330460ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:S END:VTIMEZONE collective-icalendar-138c845/src/icalendar/tests/calendars/invalid_duration.ics000066400000000000000000000003741524134033600277130ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar//test//EN BEGIN:VEVENT UID:invalid-duration@example.com DTSTAMP:20240101T000000Z DTSTART:20240101T000000Z DURATION:P999999999999999999W SUMMARY:Event with an impractical duration END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_104_broken_calendar.ics000066400000000000000000000003531524134033600312620ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT X END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_all_components.ics000066400000000000000000000015141524134033600312470ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN UID:calendar-1 BEGIN:VTIMEZONE TZID:America/New_York BEGIN:STANDARD DTSTART:19701101T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19700308T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:event-1 DTSTART;TZID=America/New_York:20250115T100000 DTEND;TZID=America/New_York:20250115T110000 SUMMARY:Test Event 1 BEGIN:VALARM UID:alarm-1 TRIGGER:-PT15M ACTION:DISPLAY END:VALARM END:VEVENT BEGIN:VTODO UID:todo-1 SUMMARY:Test Todo 1 STATUS:NEEDS-ACTION END:VTODO BEGIN:VJOURNAL UID:journal-1 SUMMARY:Test Journal 1 END:VJOURNAL BEGIN:VAVAILABILITY UID:availability-1 SUMMARY:Test Availability 1 END:VAVAILABILITY END:VCALENDARissue_1050_calendar_with_events_and_todos.ics000066400000000000000000000015521524134033600343770ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VTIMEZONE TZID:America/New_York BEGIN:STANDARD DTSTART:19701101T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST END:STANDARD BEGIN:DAYLIGHT DTSTART:19700308T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:event-1@example.com DTSTART;TZID=America/New_York:20250115T100000 DTEND;TZID=America/New_York:20250115T110000 SUMMARY:Test Event 1 END:VEVENT BEGIN:VEVENT UID:event-2@example.com DTSTART;TZID=America/New_York:20250116T100000 DTEND;TZID=America/New_York:20250116T110000 SUMMARY:Test Event 2 END:VEVENT BEGIN:VTODO UID:todo-1@example.com SUMMARY:Test Todo 1 STATUS:NEEDS-ACTION END:VTODO BEGIN:VJOURNAL UID:journal-1@example.com SUMMARY:Test Journal 1 END:VJOURNAL END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_empty_calendar.ics000066400000000000000000000001031524134033600312120ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_forward_timezone_reference.ics000066400000000000000000000005721524134033600336310ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:event@example.com DTSTART;TZID=Europe/Berlin:20250115T100000 SUMMARY:Event with forward TZ ref END:VEVENT BEGIN:VTIMEZONE TZID:Europe/Berlin BEGIN:STANDARD DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET END:STANDARD END:VTIMEZONE END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_multiple_calendars.ics000066400000000000000000000003721524134033600321020ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test1//EN BEGIN:VEVENT UID:event1@example.com SUMMARY:Event 1 END:VEVENT END:VCALENDAR BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test2//EN BEGIN:VEVENT UID:event2@example.com SUMMARY:Event 2 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_simple_calendar.ics000066400000000000000000000002461524134033600313550ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:simple-event@example.com DTSTART:20250101T100000Z SUMMARY:Simple Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_timezone_only_calendar.ics000066400000000000000000000003771524134033600327640ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VTIMEZONE TZID:Europe/London BEGIN:STANDARD DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT END:STANDARD END:VTIMEZONE END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1050_uid_in_description.ics000066400000000000000000000002631524134033600321040ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:abc SUMMARY:target END:VEVENT BEGIN:VEVENT UID:xyz DESCRIPTION:contains abc in text END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_empty_rdate.ics000066400000000000000000000002161524134033600305510ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:20250101T100000Z RDATE: SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_event_with_rrule.ics000066400000000000000000000002411524134033600316170ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:20250101T100000Z RRULE:FREQ=DAILY;COUNT=10 SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_freebusy_comma_separated.ics000066400000000000000000000003361524134033600332670ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VFREEBUSY UID:test-123 DTSTART:20250101T000000Z DTEND:20250102T000000Z FREEBUSY:20250101T100000Z/20250101T120000Z,20250101T140000Z/20250101T160000Z END:VFREEBUSY END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_invalid_rrule_freq.ics000066400000000000000000000002541524134033600321120ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:20250101T100000Z RRULE:FREQ=INVALID_TYPE_CAUSES_ERROR SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_invalid_start_and_end.ics000066400000000000000000000002201524134033600325420ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:INVALID-DATE DTEND:ALSO-INVALID SUMMARY:Test END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_invalid_start_valid_end.ics000066400000000000000000000002321524134033600331020ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:INVALID-DATE DTEND:20250102T120000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_list_of_properties.ics000066400000000000000000000002671524134033600321550ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART:20250101T100000Z EXDATE:20250105T100000Z EXDATE:20250106T100000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_tzid_param.ics000066400000000000000000000002341524134033600303660ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 DTSTART;TZID=America/New_York:20250101T100000 SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1081_with_summary.ics000066400000000000000000000001561524134033600307670ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-123 SUMMARY:Test Event END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1231_recurrence.ics000066400000000000000000000002741524134033600303720ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//icalendar test//EN BEGIN:VEVENT UID:test-uid DTSTART:20250428T160500Z RECURRENCE-ID:20250428T160500Z SUMMARY:Test recurrence END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1237_x_property.jcal000066400000000000000000000001521524134033600306040ustar00rootroot00000000000000["vcalendar", [["x-filter-date-range", {}, "period", ["2025-02-02T00:00:00", "2025-02-03T00:00:00"]]], []]collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1238.ics000066400000000000000000000001361524134033600261610ustar00rootroot00000000000000BEGIN:VCALENDAR X-FILTER-DATE-RANGE;VALUE=PERIOD:20250202T000000/20250203T000000 END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/issue_127_categories_with_commas.ics000066400000000000000000000004201524134033600326700ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:issue-127-test@example.com DTSTAMP:20140204T120000Z DTSTART:20140204T140000Z SUMMARY:Test Event with Categories Containing Commas CATEGORIES:Meeting\, John,Work\, Sarah,Project END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1426.ics000066400000000000000000000006121524134033600261570ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//EN BEGIN:VEVENT UID:77D80D14-906B-4257-963F-85B1E734DBB6 DTSTART:19961230T020000Z DTEND:19961230T060000Z RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970901T180000Z/PT5H30M BEGIN:VALARM UID:alarm-1 ACTION:DISPLAY TRIGGER;VALUE=DATE-TIME:19760401T005545Z DESCRIPTION:Remember to buy milk END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1426_value_parameters.ics000066400000000000000000000002771524134033600316050ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//EN BEGIN:VEVENT UID:issue-1426 DTSTART;VALUE=DATE:19970101 CONFERENCE;VALUE=URI:https://example.com/meet END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1549_binary_attachment.ics000066400000000000000000000005321524134033600317420ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//test//test//EN BEGIN:VEVENT UID:binary-attachment-event ATTACH;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1561_image_value.ics000066400000000000000000000004611524134033600305170ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:uri IMAGE;VALUE=URI:https://example.com/a.png END:VEVENT BEGIN:VEVENT UID:binary IMAGE;ENCODING=BASE64;VALUE=BINARY:AP+A END:VEVENT BEGIN:VEVENT UID:text IMAGE;VALUE=TEXT:a\;b\,c END:VEVENT BEGIN:VEVENT UID:unknown IMAGE:https://example.com/b.png END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal.ics000066400000000000000000000022731524134033600327500ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-387410353 VERSION:2.0 PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN BEGIN:VEVENT SUMMARY:Event DTSTART;TZID=America/Chicago;VALUE=DATE-TIME:20180327T080000 DTEND;TZID=America/Chicago;VALUE=DATE-TIME:20180327T090000 DTSTAMP:20180323T200333Z RECURRENCE-ID;RANGE=THISANDFUTURE:20180327T130000Z SEQUENCE:10 RDATE;TZID="Central Standard Time";VALUE=PERIOD:20180327T080000/20180327T0 90000,20180403T080000/20180403T090000,20180410T080000/20180410T090000,2018 0417T080000/20180417T090000,20180424T080000/20180424T090000,20180501T08000 0/20180501T090000,20180508T080000/20180508T090000,20180515T080000/20180515 T090000,20180522T080000/20180522T090000,20180529T080000/20180529T090000,20 180605T080000/20180605T090000,20180612T080000/20180612T090000,20180619T080 000/20180619T090000,20180626T080000/20180626T090000,20180703T080000/201807 03T090000,20180710T080000/20180710T090000,20180717T080000/20180717T090000, 20180724T080000/20180724T090000,20180731T080000/20180731T090000 ATTENDEE;CN="XYZ";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP= FALSE:mailto:xyz@xyz.com CLASS:PUBLIC END:VEVENT END:VCALENDAR issue_156_RDATE_with_PERIOD_TZID_khal_2.ics000066400000000000000000000041161524134033600331100ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-933635248 VERSION:2.0 PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN BEGIN:VTIMEZONE TZID:Western/Central Europe BEGIN:STANDARD DTSTART:19501029T020000 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 END:STANDARD BEGIN:DAYLIGHT DTSTART:19500326T020000 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY:(omitted) DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z UID:BF5109494E67AAE20025875100566D31-Lotus_Notes_Generated RECURRENCE-ID;RANGE=THISANDFUTURE:20211101T150000Z SEQUENCE:0 RDATE;TZID="Western/Central Europe";VALUE=PERIOD:20211101T160000/20211101T 163000,20211206T160000/20211206T163000,20220103T160000/20220103T163000,202 20207T160000/20220207T163000 ATTENDEE;CN="(omitted)";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP=FAL SE:mailto:omitted@example.com CLASS:PUBLIC TRANSP:OPAQUE X-LOTUS-APPTTYPE:3 X-LOTUS-AUDIOVIDEOFLAGS:0 X-LOTUS-BROADCAST:FALSE X-LOTUS-CHANGE-INST-DATES:20211101T150000Z\,20211206T150000Z\,20220103T150 000Z\,20220207T150000Z X-LOTUS-CHILD-UID:567EFBAF6CBD07FC0025875100566D3B X-LOTUS-INITIAL-RDATES:20211101T150000Z\,20211206T150000Z\,20220103T150000 Z\,20220207T150000Z X-LOTUS-LASTALL-RDATES;TZID="Western/Central Europe":20211101T160000\,2021 1206T160000\,20220103T160000\,20220207T160000 X-LOTUS-NOTESVERSION:2 X-LOTUS-NOTICETYPE:I X-LOTUS-RECURID;RANGE=THISANDFUTURE:20211101T150000Z X-LOTUS-UPDATE-SEQ:2 X-LOTUS-UPDATE-WISL:$W:1\;$O:1\;$M:1\;RequiredAttendees:1\;INetRequiredNam es:1\;AltRequiredNames:1\;StorageRequiredNames:1\;OptionalAttendees:1\;INe tOptionalNames:1\;AltOptionalNames:1\;StorageOptionalNames:1\;ApptUNIDURL: 1\;STUnyteConferenceURL:1\;STUnyteConferenceID:1\;SametimeType:1\;WhiteBoa rdContent:1\;STRoomName:1\;$S:2\;$B:2\;$L:2\;$E:2\;$R:2 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1633_freebusy_with_dates.ics000066400000000000000000000001661524134033600323020ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VFREEBUSY UID:1 DTSTAMP:19970101T180000Z FREEBUSY:19970101/19970102 END:VFREEBUSY END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1633_rdate_with_dates.ics000066400000000000000000000001721524134033600315520ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:1 DTSTART:19970101T180000Z RDATE;VALUE=PERIOD:19970101/19970102 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_1633_rdate_with_dates_and_tzid.ics000066400000000000000000000002451524134033600334270ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:1 DTSTART;TZID=America/New_York:19970101T180000 RDATE;TZID=America/New_York;VALUE=PERIOD:19970101/19970102 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_165_missing_event.ics000066400000000000000000000014241524134033600310320ustar00rootroot00000000000000BEGIN:VCALENDAR METHOD:REQUEST PRODID:Microsoft CDO for Microsoft Exchange VERSION:2.0 BEGIN:VTIMEZONE TZID:GMT +0100 (Standard) / GMT +0200 (Daylight) BEGIN:STANDARD DTSTART:16010101T030000 TZOFFSETFROM:+0200 TZOFFSETTO:+0100 RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0200 RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20150703T071009Z DTSTART;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T100000 SUMMARY:Sprint 25 Daily Standup DTEND;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T103000 RRULE:FREQ=DAILY;UNTIL=20150722T080000Z;INTERVAL=1;BYDAY=MO, TU, WE, TH, FR ;WKST=SU END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_168_expected_output.ics000066400000000000000000000001651524134033600314050ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20150905T090000Z DTEND:20150905T100000Z UID:123 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_168_input.ics000066400000000000000000000002271524134033600273220ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20150905T090000Z DTEND:20150905T100000Z UID:123 X-APPLE-RADIUS=49.91307046514149 END:VEVENT END:VCALENDAR issue_178_component_with_invalid_name_represented.ics000066400000000000000000000000321524134033600362420ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:MYCOMP END:MYCOMP issue_178_custom_component_contains_other.ics000066400000000000000000000001601524134033600345740ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:MYCOMPTOO DTSTAMP:20150121T080000 BEGIN:VEVENT DTSTART:20150122 UID:12345 END:VEVENT END:MYCOMPTOO issue_178_custom_component_inside_other.ics000066400000000000000000000001061524134033600342310ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR BEGIN:UNKNOWN UID:1234 END:UNKNOWN END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_218_bad_tzid.ics000066400000000000000000000006761524134033600277470ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//TEST//TEST//EN BEGIN:VTIMEZONE TZID:UTC+11 BEGIN:STANDARD DTSTART;VALUE=DATE:20170101 TZNAME:UTC+11 TZOFFSETFROM:+1100 TZOFFSETTO:+1100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DESCRIPTION:TESTING DTEND;TZID=UTC+11:20170228T233000 DTSTAMP:20170227T064302Z DTSTART;TZID=UTC+11:20170228T230000 RESOURCES:Court 4 SEQUENCE:0 STATUS:Confirmed SUMMARY:TESTIN UID:1961094_636238800000000000 END:VEVENT END:VCALENDAR issue_237_fail_to_parse_timezone_with_non_ascii_tzid.ics000066400000000000000000000010651524134033600367320ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:(UTC-03:00) Brasília BEGIN:STANDARD TZNAME:Brasília standard DTSTART:16010101T235959 TZOFFSETFROM:-0200 TZOFFSETTO:-0300 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 END:STANDARD BEGIN:DAYLIGHT TZNAME:Brasília daylight DTSTART:16010101T235959 TZOFFSETFROM:-0300 TZOFFSETTO:-0200 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="(UTC-03:00) Brasília":20170511T133000 DTEND;TZID="(UTC-03:00) Brasília":20170511T140000 END:VEVENT END:VCALENDAR issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics000066400000000000000000000013441524134033600401200ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN METHOD:REPLY BEGIN:VFREEBUSY DTSTAMP:20120131T123000Z ORGANIZER:MAILTO:organizer@domain.tld DTSTART:20120101T000000Z DTEND:20120201T000000Z UID:null ATTENDEE:MAILTO:attendee@domain.tld FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z FREEBUSY;FBTYPE=BUSY:20120113T130000Z/20120113T150000Z FREEBUSY;FBTYPE=BUSY:20120116T130000Z/20120116T150000Z FREEBUSY;FBTYPE=BUSY:20120117T091500Z/20120117T101500Z FREEBUSY;FBTYPE=BUSY:20120118T160000Z/20120118T163000Z FREEBUSY;FBTYPE=BUSY:20120124T083000Z/20120124T093000Z FREEBUSY;FBTYPE=BUSY:20120124T123000Z/20120124T143000Z FREEBUSY;FBTYPE=BUSY:20120131T091500Z/20120131T101500Z END:VFREEBUSY END:VCALENDAR issue_27_multiple_periods_in_freebusy_one_freebusy.ics000066400000000000000000000011211524134033600365270ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN METHOD:REPLY BEGIN:VFREEBUSY DTSTAMP:20120131T123000Z ORGANIZER:MAILTO:organizer@domain.tld DTSTART:20120101T000000Z DTEND:20120201T000000Z UID:null ATTENDEE:MAILTO:attendee@domain.tld FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z,20120113T130000Z/20120113T150000Z,20120116T130000Z/20120116T150000Z,20120117T091500Z/20120117T101500Z,20120118T160000Z/20120118T163000Z,20120124T083000Z/20120124T093000Z,20120124T123000Z/20120124T143000Z,20120131T091500Z/20120131T101500Z END:VFREEBUSY END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_313_globally_unique_tzid.ics000066400000000000000000000016251524134033600324030ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//issue 313//EN BEGIN:VEVENT UID:libical-evolution@issue-313 SUMMARY:libical / Evolution globally unique TZID DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin:20200426T140000 DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin:20200426T150000 END:VEVENT BEGIN:VEVENT UID:mozilla-lightning@issue-313 SUMMARY:Mozilla Lightning globally unique TZID DTSTART;TZID=/mozilla.org/20070129_1/America/New_York:20200426T140000 DTEND;TZID=/mozilla.org/20070129_1/America/New_York:20200426T150000 END:VEVENT BEGIN:VEVENT UID:multipart-olson@issue-313 SUMMARY:vendor prefix in front of a multi-part Olson name DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/America/Argentina/Buenos_Aires:20200426T140000 DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/America/Argentina/Buenos_Aires:20200426T150000 END:VEVENT END:VCALENDAR issue_321_assert_dst_offset_is_not_false.ics000066400000000000000000000010311524134033600343330ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN BEGIN:VTIMEZONE TZID:Europe/Berlin BEGIN:DAYLIGHT DTSTART;VALUE=DATE-TIME:20200329T030000 TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY:Reifenwechsel DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:20200403T102000 DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:20200403T113000 DTSTAMP;VALUE=DATE-TIME:20200326T165608Z UID:TCK6EMQBQHQ8KB4PDQPT04Q7HBYNS2MEPZMO SEQUENCE:0 LOCATION:Musterhausen END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/issue_322_expected_calendar.ics000066400000000000000000000002111524134033600315760ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:Event with bare string as argument for categories CATEGORIES:Lecture END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_348_exception_parsing_value.ics000066400000000000000000000026421524134033600331030ustar00rootroot00000000000000BEGIN:VCALENDAR CALSCALE:GREGORIAN METHOD:PUBLISH VERSION:2.0 PRODID:-//Sixt//RAC//EN BEGIN:VFREEBUSY FREEBUSY;FBTYPE=FREE:20190624T063000Z/20190624T163000Z ORGANIZER;CN=Sixt SE X-ORGANIZER2;CN=Sixt SE;CN2=Test! UID:SIXT_9879691160 DTSTAMP:20190612T104813Z END:VFREEBUSY BEGIN:VEVENT CLASS:PUBLIC DESCRIPTION:\nVotre véhicule avec le numéro de réservation xxxxxxxxxxx sera à votre disposition le 24.06.2019 à 08:30 heures (heure locale). Retour prévu : Ferney-Voltaire AP de Genève Cointrin Sect(F), le 24.06.2019 à 18:30 heures.\nVous trouverez des informations relatives à la modification ou à l'annulation de votre réservation sous http://www.sixt.fr\n DTSTART:20190624T063000Z DTSTAMP:20190612T104813Z DTEND:20190624T163000Z LOCATION:Sixt Genève Aéroport secteur France, AP de Genève Cointrin Sect(F), 01210 Ferney-Voltaire, FR PRIORITY:5 TRANSP:TRANSPARENT SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Sixt : détails de votre réservation UID:SIXT_9879691160 BEGIN:VALARM TRIGGER:-PT30M ACTION:DISPLAY DESCRIPTION:\nLe véhicule avec la n° de réservation xxxxxxxxxxx sera à votre disposition entre 24.06.2019 et 08:30 heures (heure locale) à l'agence Genève Aéroport secteur France. Retour prévu : Ferney-Voltaire AP de Genève Cointrin Sect(F), le 24.06.2019 à 18:30 heures.\nTrouver des informations de localisation et modifier / annuler votre réservation ici: http://www.sixt.fr\n END:VALARM END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_350.ics000066400000000000000000000025061524134033600260760ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Podio API//EN//view-exporter//52593453 METHOD:REQUEST X-WR-CALNAME:view52593586 X-WR-TIMEZONE:Europe/Berlin CALSCALE:GREGORIAN X-COMMENT-USAGE:This calendar does not contain recurring events! X-COMMENT-GENERATOR:PHP Version 8.0.16 BEGIN:VEVENT STATUS:CONFIRMED SEQUENCE:0 TRANSP:OPAQUE CLASS:PUBLIC UID:20055546456446 SUMMARY:Termin 4353 und"so" DESCRIPTION;ALTREP="data:text/html,%3Cbody%3E%3Cp%3EToller%20%3Cstron g%20class%3D%22text-bold%22%3ETermin%3C%2Fstrong%3E%20f%C3%BCr%3C%2Fp %3E%3Cblockquote%3E%3Cp%3Emal%20%3Cem%20class%3D%22text-italic%22%3Ez u%22gucken%22%3C%2Fem%3E%3C%2Fp%3E%3C%2Fblockquote%3E%3Cp%3E%3Cu%20cl ass%3D%22text-underline%22%3Eund%3C%2Fu%3E%20%3Cdel%3Eso%3C%2Fdel%3E% 3Cbr%2F%3E%3C%2Fp%3E%3C%2Fbody%3E":Toller Termin fürmal zu\"gucken\" und so X-ALT-DESC;FMTTYPE=text/html:

Toller Termin fĂĽr

mal zu"gucken"

und so

URL:https://podio.com/xxxxxxyyyyyy/zpodio-testgelande/apps/calen dar/items/5 LOCATION:online DTSTART:20220222T183000Z DTEND:20220222T193000Z DTSTAMP:20220220T142821Z END:VEVENT END:VCALENDAR X-COMMENT:Cached from 2022-02-20 14:28:21 - new at most every 1800sec. issue_351_whitespace_in_property_and_params.ics000066400000000000000000000003561524134033600350540ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:-//Issue 351 Fixture//EN REFRESH - INTERVAL; VALUE = DURATION:PT48H BEGIN:VEVENT UID:issue-351@example.com DTSTAMP:20260318T000000Z DTSTART:20260319T090000Z SUMMARY:Waste Pickup END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_466_convert_tzid_with_slash.ics000066400000000000000000000005051524134033600331220ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:0cab49a0-1167-40f0-bfed-ecb4d117047d DTSTAMP:20221019T102950Z DTSTART;TZID=/Europe/Stockholm:20221021T200000 DTEND;TZID=/Europe/Stockholm:20221021T210000 SUMMARY:Just chatting DESCRIPTION:Just Chatting. CATEGORIES:Just Chatting RRULE:FREQ=WEEKLY;BYDAY=FR END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_466_respect_unique_timezone.ics000066400000000000000000000011731524134033600331320ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:/Europe/CUSTOM BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:0cab49a0-1167-40f0-bfed-ecb4d117047d DTSTAMP:20221019T102950Z DTSTART;TZID=/Europe/CUSTOM:20221021T200000 DTEND;TZID=/Europe/CUSTOM:20221021T210000 SUMMARY:Just chatting DESCRIPTION:Just Chatting. CATEGORIES:Just Chatting RRULE:FREQ=WEEKLY;BYDAY=FR END:VEVENT END:VCALENDAR issue_526_calendar_with_different_events.ics000066400000000000000000000010601524134033600343140ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:ical-jacadzaca-3 SUMMARY: Some very different event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:ical-jacadzaca-4 SUMMARY: Some very different other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics000066400000000000000000000004311524134033600335500ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics000066400000000000000000000007641524134033600323570ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:2 SUMMARY: Some other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR issue_526_calendar_with_shuffeled_events.ics000066400000000000000000000007641524134033600343250ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-2023 BEGIN:VEVENT UID:2 SUMMARY: Some other event DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 DTSTAMP:20211004T150245Z END:VEVENT BEGIN:VEVENT UID:1 SUMMARY: Some event ':' DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 DTSTAMP:20211004T150245Z END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_722_missing_VTIMEZONE_custom.ics000066400000000000000000000001571524134033600326640ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;TZID=CUSTOM_tzid;VALUE=DATE-TIME:20140829T080000 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/issue_722_missing_timezones.ics000066400000000000000000000012111524134033600317170ustar00rootroot00000000000000BEGIN:VCALENDAR DESCRIPTION:We leave the timezones out but we use common names so that they are added. BEGIN:VEVENT DTSTART;TZID=America/New_York;VALUE=DATE-TIME:20140829T080000 DTEND;TZID=America/Los_Angeles;VALUE=DATE-TIME:20140829T080000 RDATE;VALUE=PERIOD;TZID=Europe/Berlin:20240913T120000/PT2H SUMMARY:an event with a custom tz name END:VEVENT BEGIN:VEVENT RECURRENCE-ID;TZID=Europe/Moscow:20190309T020000 END:VEVENT BEGIN:VTODO DUE;TZID=Asia/Singapore;VALUE=DATE-TIME:20140829T080000 END:VTODO BEGIN:VEVENT RDATE;TZID=Mexico/General:20190309T020000 RDATE;TZID=America/Noronha:20190309T020000 DTSTAMP:19920901T130000Z END:VEVENT END:VCALENDAR issue_722_timezone_transition_ambiguity.ics000066400000000000000000000020311524134033600342510ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/calendarsBEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:MyTimezone BEGIN:STANDARD COMMENT:The timezone starts at 2024-01-01 with +12h offset TZOFFSETFROM:+1000 TZOFFSETTO:+1200 DTSTART:20240101T000000 TZNAME:winter END:STANDARD BEGIN:DAYLIGHT COMMENT:The timezone goes from +12h to +10h at 8 am TZOFFSETFROM:+1200 TZOFFSETTO:+1000 DTSTART:20240505T080000 TZNAME:summer END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:0 SUMMARY:This event is clearly in winter DTSTART;TZID=MyTimezone;VALUE=DATE-TIME:20240303T080000 X-TZNAME:winter END:VEVENT BEGIN:VEVENT UID:1 SUMMARY:This event is clearly in summer X-TZNAME:summer DTSTART;TZID=MyTimezone;VALUE=DATE-TIME:20240803T080000 END:VEVENT BEGIN:VEVENT UID:2 SUMMARY:Transition is from 8am -> 6am, so 8:00:01 is summer X-TZNAME:summer DTSTART;TZID=MyTimezone;VALUE=DATE-TIME:20240505T080001 END:VEVENT BEGIN:VEVENT UID:3 SUMMARY:Transition is from 8am -> 6am, so 7:00:01 is winter RFC5545 does not allow us to be at the later TZ. X-TZNAME:winter DTSTART;TZID=MyTimezone;VALUE=DATE-TIME:20240505T070001 END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/issue_798_freebusy.ics000066400000000000000000000005131524134033600300160ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VFREEBUSY FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:19970308T160000Z/PT8H30M END:VFREEBUSY BEGIN:VFREEBUSY FREEBUSY:19970308T160000Z/PT3H,19970308T200000Z/PT1H END:VFREEBUSY BEGIN:VFREEBUSY FREEBUSY;FBTYPE=FREE:19970308T160000Z/PT3H,19970308T200000Z/PT1H ,19970308T230000Z/19970309T000000Z END:VFREEBUSY END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/issue_798_related_to.ics000066400000000000000000000003141524134033600303130ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT RELATED-TO:jsmith.part7.19960817T083000.xyzMail@example.com END:VEVENT BEGIN:VEVENT RELATED-TO;RELTYPE=SIBLING:19960401-080045-4000F192713@example.com END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/issue_82_expected_output.ics000066400000000000000000000001331524134033600313130ustar00rootroot00000000000000BEGIN:VEVENT ATTACH;ENCODING=BASE64;FMTTYPE=text/plain;VALUE=BINARY:dGV4dA== END:VEVENT collective-icalendar-138c845/src/icalendar/tests/calendars/issue_836_do_not_quote_tzid.ics000066400000000000000000000012261524134033600317160ustar00rootroot00000000000000BEGIN:VCALENDAR METHOD:PUBLISH PRODID:Microsoft Exchange Server 2010 VERSION:2.0 BEGIN:VTIMEZONE TZID:Eastern Standard Time BEGIN:STANDARD DTSTART:16010101T020000 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:minimal-demo-event-est-20241028@example.com SUMMARY:Anonymous Test Event for TZID DTSTART;TZID=Eastern Standard Time:20241028T170000 DTEND;TZID=Eastern Standard Time:20241028T180000 DTSTAMP:20250514T023916Z END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/multiple_calendar_components.ics000066400000000000000000000016641524134033600323140ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :956630271 SUMMARY :Christmas Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20031225 DTEND ;VALUE=DATE :20031226 DTSTAMP :20020430T114937Z END:VEVENT END:VCALENDAR BEGIN:VCALENDAR VERSION :2.0 PRODID :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN METHOD :PUBLISH BEGIN:VEVENT UID :911737808 SUMMARY :Boxing Day CLASS :PUBLIC X-MOZILLA-ALARM-DEFAULT-UNITS :minutes X-MOZILLA-ALARM-DEFAULT-LENGTH :15 X-MOZILLA-RECUR-DEFAULT-UNITS :weeks X-MOZILLA-RECUR-DEFAULT-INTERVAL :1 DTSTART ;VALUE=DATE :20030501 DTSTAMP :20020430T114937Z END:VEVENT BEGIN:VEVENT UID :wh4t3v3r DTSTART;VALUE=DATE:20031225 SUMMARY:Christmas again! END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/multiple_timezones.ics000066400000000000000000000013001524134033600302760ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:America/New_York BEGIN:STANDARD TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:DAYLIGHT TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:test-vtime-named-timezone DTSTART;TZID=America/New_York:20210302T000000 RDATE;VALUE=TIME;TZID=America/New_York:083000 END:VEVENT BEGIN:VEVENT UID:test-vtime-utc DTSTART:20210302T000000Z RDATE;VALUE=TIME:133000Z END:VEVENT BEGIN:VEVENT UID:test-vtime-floating DTSTART:20210302T000000 RDATE;VALUE=TIME:083000 END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/pacific_fiji.ics000066400000000000000000000023061524134033600267540ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//tzurl.org//NONSGML Olson 2014g//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:noend123 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T080000 DTSTART;TZID=custom_Pacific/Fiji;VALUE=DATE-TIME:20140829T100000 SUMMARY:an event with a custom tz name END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/parsing_error.ics000066400000000000000000000007521524134033600272340ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH BEGIN:VEVENT DESCRIPTION:Perfectly OK event DTSTART;VALUE=DATE:20080303 DTEND;VALUE=DATE:20080304 RRULE:FREQ=DAILY;UNTIL=20080323T235959Z EXDATE;VALUE=DATE:20080311 END:VEVENT BEGIN:VEVENT DESCRIPTION:Wrong event DTSTART;VALUE=DATE:20080303 DTEND;VALUE=DATE:20080304 RRULE:FREQ=DAILY;UNTIL=20080323T235959Z EXDATE;VALUE=DATE:20080311 EXDATE;VALUE=DATE: END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/parsing_error_in_UTC_offset.ics000066400000000000000000000002751524134033600320030ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VTIMEZONE TZID:Europe/Prague BEGIN:STANDARD DTSTART:18500101T000000 TZNAME:PMT TZOFFSETFROM:+5744 TZOFFSETTO:+5744 END:STANDARD END:VTIMEZONE END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/period_with_timezone.ics000066400000000000000000000013501524134033600306020ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 X-WR-CALNAME;VALUE=TEXT:Test RDATE BEGIN:VTIMEZONE TZID:America/Vancouver BEGIN:STANDARD DTSTART:20221106T020000 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 RDATE:20231105T020000 TZNAME:PST END:STANDARD BEGIN:DAYLIGHT DTSTART:20230312T020000 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 RDATE:20240310T020000 TZNAME:PDT END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:1 DESCRIPTION:Test RDATE DTSTART;TZID=America/Vancouver:20230920T120000 DTEND;TZID=America/Vancouver:20230920T140000 EXDATE;TZID=America/Vancouver:20231220T120000 RDATE;VALUE=PERIOD;TZID=America/Vancouver:20231213T120000/20231213T150000 RRULE:FREQ=MONTHLY;COUNT=9;INTERVAL=1;BYDAY=+3WE;BYMONTH=1,2,3,4,5,9,10,11, 12;WKST=MO SUMMARY:Test RDATE END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/pr_480_summary_with_colon.ics000066400000000000000000000002451524134033600313730ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:Example calendar with a ': ' in the summary END:VEVENT BEGIN:VEVENT SUMMARY:Another event with a ': ' in the summary END:VEVENT collective-icalendar-138c845/src/icalendar/tests/calendars/property_params.ics000066400000000000000000000012241524134033600276020ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID://RESEARCH IN MOTION//BIS 3.0 METHOD:REQUEST BEGIN:VEVENT SEQUENCE:2 X-RIM-REVISION:0 SUMMARY:Test meeting from BB X-MICROSOFT-CDO-ALLDAYEVENT:TRUE CLASS:PUBLIC ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandXS":MAILTO:rembrand@xs4all.nl ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandDX":MAILTO:rembrand@daxlab.com ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="RembrandSB":MAILTO:rembspam@xs4all.nl UID:XRIMCAL-628059586-522954492-9750559 DTSTART;VALUE=DATE:20120814 DTEND;VALUE=DATE:20120815 DESCRIPTION:Test meeting from BB DTSTAMP:20120813T151458Z ORGANIZER:mailto:rembrand@daxlab.com END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_5545_RDATE_example.ics000066400000000000000000000013641524134033600302460ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:0 RDATE:19970714T123000 END:VEVENT BEGIN:VEVENT UID:1 RDATE:19970714T123000Z END:VEVENT BEGIN:VEVENT UID:2 RDATE;TZID=America/New_York:19970714T083000 END:VEVENT BEGIN:VEVENT UID:3 RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z, 19960404T010000Z/PT3H END:VEVENT BEGIN:VEVENT UID:4 RDATE;VALUE=DATE:19970101,19970120,19970217,19970421, 19970526,19970704,19970901,19971014,19971128,19971129,19971225 END:VEVENT BEGIN:VEVENT UID:5 RDATE;VALUE=DATE:19970101,19970120,19970217,19970421, 19970526,19970704,19970901,19971014,19971128,19971129,19971225 RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z, 19960404T010000Z/PT3H RDATE:19970714T123000Z RDATE;TZID=America/New_York:19970714T083000 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_6868.ics000066400000000000000000000002511524134033600256170ustar00rootroot00000000000000BEGIN:VCALENDAR X-PARAM;NEWLINE=^n;ALL=^^^'^n;UNKNOWN=^a^ ^asd:asd BEGIN:VEVENT ATTENDEE;CN=George Herman ^'Babe^' Ruth:mailto:babe@example.com END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7256_multi_value_parameters.ics000066400000000000000000000014411524134033600324420ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com","mailto:jqpublic@example.com":mailto:jsmith@example.com ATTENDEE;DELEGATED-FROM="mailto:jsmith@example.com","mailto:jdoe@example.com":mailto:jdoe@example.com ATTENDEE;MEMBER="mailto:projectA@example.com","mailto:projectB@example.com":mailto:janedoe@example.com UID:list DESCRIPTION:These are all the parameters which might come as a list. END:VEVENT BEGIN:VEVENT ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com":mailto:jsmith@example.com ATTENDEE;DELEGATED-FROM="mailto:jsmith@example.com":mailto:jdoe@example.com ATTENDEE;MEMBER="mailto:projectA@example.com":mailto:janedoe@example.com UID:single DESCRIPTION:These are all the parameters which might come as a list but here they only have one value. END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_appendix_example_1_ical.ics000066400000000000000000000003421524134033600324230ustar00rootroot00000000000000BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Example Inc.//Example Calendar//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20080205T191224Z DTSTART:20081006 SUMMARY:Planning meeting UID:4088E990AD89CB3DBB484909 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_appendix_example_1_jcal.jcal000066400000000000000000000007621524134033600325650ustar00rootroot00000000000000 ["vcalendar", [ ["calscale", {}, "text", "GREGORIAN"], ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], ["version", {}, "text", "2.0"] ], [ ["vevent", [ ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], ["dtstart", {}, "date", "2008-10-06"], ["summary", {}, "text", "Planning meeting"], ["uid", {}, "text", "4088E990AD89CB3DBB484909"] ], [] ] ] ]collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_appendix_example_2_ical.ics000066400000000000000000000020661524134033600324310ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//Example Client//EN BEGIN:VTIMEZONE LAST-MODIFIED:20040110T032845Z TZID:US/Eastern BEGIN:DAYLIGHT DTSTART:20000404T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20001026T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20060206T001121Z DTSTART;TZID=US/Eastern:20060102T120000 DURATION:PT1H RRULE:FREQ=DAILY;COUNT=5 RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H SUMMARY:Event #2 DESCRIPTION:We are having a meeting all this week at 12 pm fo r one hour\, with an additional meeting on the first day 2 h ours long.\nPlease bring your own lunch for the 12 pm meetin gs. UID:00959BC664CA650E933C892C@example.com END:VEVENT BEGIN:VEVENT DTSTAMP:20060206T001121Z DTSTART;TZID=US/Eastern:20060104T140000 DURATION:PT1H RECURRENCE-ID;TZID=US/Eastern:20060104T120000 SUMMARY:Event #2 UID:00959BC664CA650E933C892C@example.com END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_appendix_example_2_jcal.jcal000066400000000000000000000055731524134033600325730ustar00rootroot00000000000000 ["vcalendar", [ ["version", {}, "text", "2.0"], ["prodid", {}, "text", "-//Example Corp.//Example Client//EN"] ], [ ["vtimezone", [ ["last-modified", {}, "date-time", "2004-01-10T03:28:45Z"], ["tzid", {}, "text", "US/Eastern"] ], [ ["daylight", [ ["dtstart", {}, "date-time", "2000-04-04T02:00:00"], ["rrule", {}, "recur", { "freq": "YEARLY", "byday": "1SU", "bymonth": 4 } ], ["tzname", {}, "text", "EDT"], ["tzoffsetfrom", {}, "utc-offset", "-05:00"], ["tzoffsetto", {}, "utc-offset", "-04:00"] ], [] ], ["standard", [ ["dtstart", {}, "date-time", "2000-10-26T02:00:00"], ["rrule", {}, "recur", { "freq": "YEARLY", "byday": "-1SU", "bymonth": 10 } ], ["tzname", {}, "text", "EST"], ["tzoffsetfrom", {}, "utc-offset", "-04:00"], ["tzoffsetto", {}, "utc-offset", "-05:00"] ], [] ] ] ], ["vevent", [ ["dtstamp", {}, "date-time", "2006-02-06T00:11:21Z"], ["dtstart", { "tzid": "US/Eastern" }, "date-time", "2006-01-02T12:00:00" ], ["duration", {}, "duration", "PT1H"], ["rrule", {}, "recur", { "freq": "DAILY", "count": 5 } ], ["rdate", { "tzid": "US/Eastern" }, "period", "2006-01-02T15:00:00/PT2H" ], ["summary", {}, "text", "Event #2"], ["description", {}, "text", "We are having a meeting all this week at 12 pm for one hour, with an additional meeting on the first day 2 hours long.\nPlease bring your own lunch for the 12 pm meetings." ], ["uid", {}, "text", "00959BC664CA650E933C892C@example.com"] ], [] ], ["vevent", [ ["dtstamp", {}, "date-time", "2006-02-06T00:11:21Z"], ["dtstart", { "tzid": "US/Eastern" }, "date-time", "2006-01-04T14:00:00" ], ["duration", {}, "duration", "PT1H"], ["recurrence-id", { "tzid": "US/Eastern" }, "date-time", "2006-01-04T12:00:00" ], ["summary", {}, "text", "Event #2"], ["uid", {}, "text", "00959BC664CA650E933C892C@example.com"] ], [] ] ] ]collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_example_1.ics000066400000000000000000000001031524134033600275360ustar00rootroot00000000000000BEGIN:VCALENDAR X-COMPLAINT-DEADLINE:20110512T120000Z END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_example_2.jcal000066400000000000000000000001651524134033600277020ustar00rootroot00000000000000[ "vcalendar", [ ["x-coffee-data", {}, "unknown", "Stenophylla;Guinea\\,Africa"] ], [ ] ]collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7265_example_3.jcal000066400000000000000000000001331524134033600276760ustar00rootroot00000000000000 [ "vtodo", [ ["percent-complete", {}, "integer", 95] ], [ ] ] collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7529.ics000066400000000000000000000011731524134033600256160ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID://RESEARCH IN MOTION//BIS 3.0 METHOD:REQUEST BEGIN:VEVENT UID:4.3.1 DTSTART;VALUE=DATE:20130210 RRULE:RSCALE=CHINESE;FREQ=YEARLY SUMMARY:Chinese New Year END:VEVENT BEGIN:VEVENT UID:4.3.2 DTSTART;VALUE=DATE:20130906 RRULE:RSCALE=ETHIOPIC;FREQ=MONTHLY;BYMONTH=13 SUMMARY:First day of 13th month END:VEVENT BEGIN:VEVENT UID:4.3.3 DTSTART;VALUE=DATE:20140208 RRULE:RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=8;SKIP=FORWARD SUMMARY:Anniversary END:VEVENT BEGIN:VEVENT UID:4.3.4 DTSTART;VALUE=DATE:20120229 RRULE:RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD SUMMARY:Anniversary END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7953_3.ics000066400000000000000000000026571524134033600260510ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:BE082249-7BDD-4FE0-BDBA-DE6598C32FC9 DTSTAMP:20111005T133225Z DTSTART;TZID=America/Montreal:20111002T000000 DTEND;TZID=America/Montreal:20111023T030000 BEGIN:AVAILABLE UID:54602321-CEDB-4620-9099-757583263981 SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111002T090000 DTEND;TZID=America/Montreal:20111002T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR LOCATION:Montreal END:AVAILABLE END:VAVAILABILITY BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:A1FF55E3-555C-433A-8548-BF4864B5621E DTSTAMP:20111005T133225Z DTSTART;TZID=America/Denver:20111023T000000 DTEND;TZID=America/Denver:20111030T000000 BEGIN:AVAILABLE UID:57DD4AAF-3835-46B5-8A39-B3B253157F01 SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Denver:20111023T090000 DTEND;TZID=America/Denver:20111023T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR LOCATION:Denver END:AVAILABLE END:VAVAILABILITY BEGIN:VAVAILABILITY ORGANIZER:mailto:bernard@example.com UID:1852F9E1-E0AA-4572-B4C4-ED1680A4DA40 DTSTAMP:20111005T133225Z DTSTART;TZID=America/Montreal:20111030T030000 BEGIN:AVAILABLE UID:D27C421F-16C2-4ECB-8352-C45CA352C72A SUMMARY:Monday to Friday from 9:00 to 17:00 DTSTART;TZID=America/Montreal:20111030T090000 DTEND;TZID=America/Montreal:20111030T170000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR LOCATION:Montreal END:AVAILABLE END:VAVAILABILITY END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7986_conferences.ics000066400000000000000000000010171524134033600301740ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR; LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123 CONFERENCE;VALUE=URI;FEATURE=PHONE; LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123 CONFERENCE;VALUE=URI;FEATURE=CHAT; LABEL=Chat room:xmpp:chat-123@conference.example.com CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO; LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456 END:VEVENT END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7986_image.ics000066400000000000000000000023251524134033600267670ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT UID:uri-event IMAGE;VALUE=URI;DISPLAY=BADGE;FMTTYPE=image/png:http://example.com/images/party.png END:VEVENT BEGIN:VTODO UID:uri-todo IMAGE;VALUE=URI;DISPLAY=BADGE:http://example.com/images/party.jpg END:VTODO BEGIN:VJOURNAL UID:uri-journal IMAGE;VALUE=URI;DISPLAY=ICON;FMTTYPE=image/jpg:http://example.com/images/party.jpg END:VJOURNAL BEGIN:VEVENT UID:data-event IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= END:VEVENT BEGIN:VTODO UID:data-todo IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;ALTREP="http://example.com/images/party.jpg":iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= END:VTODO BEGIN:VJOURNAL UID:data-journal IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;DISPLAY=BADGE:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= END:VJOURNAL END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_7986_properties.ics000066400000000000000000000005071524134033600301010ustar00rootroot00000000000000BEGIN:VCALENDAR LAST-MODIFIED:20161029T121229Z REFRESH-INTERVAL:PT3H NAME:RFC 7986 calendar DESCRIPTION:We want a lot of RFC 7986 parameters in here! COLOR:black UID:5FC53010-1267-4F8E-BC28-1D7AE55A7C99 SOURCE:https://github.com/collective/icalendar/tree/main/src/icalendar/tests/calendars/rfc_7986_properties.ics END:VCALENDARcollective-icalendar-138c845/src/icalendar/tests/calendars/rfc_9253_examples.ics000066400000000000000000000013471524134033600275130ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//6.3.2.dev310//EN BEGIN:VEVENT DTSTAMP:20251028T092239Z DTSTART:20251028T092239Z UID:links-rfc-9253-section-8.2 LINK;LINKREL=SOURCE;LABEL=Venue;VALUE=URI: https://example.com/events LINK;LINKREL="https://example.com/linkrel/derivedFrom"; VALUE=URI: https://example.com/tasks/01234567-abcd1234.ics LINK;LINKREL="https://example.com/linkrel/costStructure"; VALUE=XML-REFERENCE: https://example.com/xmlDocs/bidFramework.xml #xpointer(descendant::CostStruc/range-to( following::CostStrucEND[1])) END:VEVENT BEGIN:VEVENT DTSTAMP:20251028T092239Z DTSTART:20251028T092239Z UID:links-rfc-9253-uid LINK;LINKREL=REFERENCE;VALUE=UID: links-rfc-9253-section-8.2 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_9253_gap.ics000066400000000000000000000004711524134033600264410ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//6.3.2.dev310//EN BEGIN:VEVENT UID:1 DTSTAMP:19960817T083000Z SUMMARY:Event 1 END:VEVENT BEGIN:VEVENT UID:2 DTSTAMP:19960817T083000Z SUMMARY:This starts one week after Event 1 RELATED-TO;VALUE=UID;RELTYPE=STARTTOSTART;GAP=P1W:1 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/rfc_9253_related_to.ics000066400000000000000000000007041524134033600300130ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:-//collective//icalendar//6.3.2.dev310//EN BEGIN:VEVENT UID:19960401-080045-4000F192713.ics DTSTAMP:19960817T083000Z SUMMARY:Examples from RFC 9253 RELATED-TO:jsmith.part7.19960817T083000.xyzMail@example.com RELATED-TO;VALUE=UID:19960401-080045-4000F192713-0052@example.com RELATED-TO;VALUE=URI;RELTYPE=STARTTOFINISH: https://example.com/caldav/user/jb/cal/ 19960401-080045-4000F192713.ics END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/small_bad_calendar.ics000066400000000000000000000000501524134033600301160ustar00rootroot00000000000000BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT collective-icalendar-138c845/src/icalendar/tests/calendars/time.ics000066400000000000000000000000731524134033600253120ustar00rootroot00000000000000BEGIN:VCALENDAR X-SOMETIME;VALUE=TIME:172010 END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/timezone_rdate.ics000066400000000000000000000021251524134033600273650ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTIMEZONE TZID:posix/Europe/Vaduz BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+002946 TZOFFSETTO:+0100 DTSTART:19011213T211538 RDATE;VALUE=DATE-TIME:19011213T211538 END:STANDARD BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:DAYLIGHT TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 DTSTART:19410505T010000 RDATE;VALUE=DATE-TIME:19410505T010000 RDATE;VALUE=DATE-TIME:19420504T010000 END:DAYLIGHT BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19810927T030000 RRULE:FREQ=YEARLY;COUNT=15;BYDAY=-1SU;BYMONTH=9 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:STANDARD TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 DTSTART:19411006T020000 RDATE;VALUE=DATE-TIME:19411006T020000 RDATE;VALUE=DATE-TIME:19421005T020000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:123 DTSTART;TZID=posix/Europe/Vaduz:20120213T100000 SUMMARY=testevent END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/timezone_same_start.ics000066400000000000000000000012551524134033600304330ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 METHOD:REQUEST BEGIN:VTIMEZONE TZID:Pacific Standard Time BEGIN:STANDARD DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T020000 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT SUMMARY;LANGUAGE=en-US:Test 4 DTSTART;TZID="Pacific Standard Time":20170224T120000 DTEND;TZID="Pacific Standard Time":20170224T123000 DTSTAMP:20170224T180431Z UID:040000008200E00074C5B7101A82E0080000000090E19664858ED20100000000000000 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/timezone_same_start_and_offset.ics000066400000000000000000000007331524134033600326230ustar00rootroot00000000000000BEGIN:VCALENDAR VERSION:2.0 PRODID:Microsoft Exchange Server 2010 BEGIN:VTIMEZONE TZID:Tokyo Standard Time BEGIN:STANDARD DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010101T000000 TZOFFSETFROM:+0900 TZOFFSETTO:+0900 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID="Tokyo Standard Time":20170224T120000 DTEND;TZID="Tokyo Standard Time":20170224T123000 UID:blafoobar SUMMARY:this is an event END:VEVENT END:VCALENDARD collective-icalendar-138c845/src/icalendar/tests/calendars/timezoned.ics000066400000000000000000000014541524134033600263560ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Plone.org//NONSGML plone.app.event//EN VERSION:2.0 X-WR-CALNAME:test create calendar X-WR-CALDESC:icalendar test X-WR-RELCALID:12345 X-WR-TIMEZONE:Europe/Vienna BEGIN:VTIMEZONE TZID:Europe/Vienna X-LIC-LOCATION:Europe/Vienna BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120213T100000 DTEND;TZID=Europe/Vienna:20120217T180000 DTSTAMP:20101010T091010Z CREATED:20101010T091010Z UID:123456 SUMMARY:artsprint 2012 DESCRIPTION:sprinting at the artsprint LOCATION:aka bild, wien END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/calendars/x_location.ics000066400000000000000000000025721524134033600265210ustar00rootroot00000000000000BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:ITC X-WR-TIMEZONE:Europe/Zurich X-WR-CALDESC:ITC Bookings BEGIN:VTIMEZONE TZID:Europe/Zurich X-LIC-LOCATION:Europe/Zurich BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTART;TZID=Europe/Zurich:20161028T140000 DTEND;TZID=Europe/Zurich:20161028T143000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR DTSTAMP:20161031T192828Z UID:BFE33ADD-5553-48B5-B5A5-F9DA5CA4C393 CREATED:20161029T121229Z DESCRIPTION:Some Description LAST-MODIFIED:20161029T121229Z LOCATION:Roadstar 16\n12764 Happyville\nDenmark SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Daily Sync TRANSP:OPAQUE X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="Röadstar 16\n12764 Happyvi lle\nDenmark";X-APPLE-MAPKIT-HANDLE=CAESARoSCWYTYFhHQBEGfw4hQCIBDQoHRGVubW FyaxJES0hhcHB5dmlsbGUqSGFwcHl2aWxsZTIHSGFwcHl2aWxsZToEMTI3NjRCDQpSb2Fkc3Rh cloCMTZiUm9hZHN0YXIgMTYBEU1vcmRvcgENCk1vcmRvcioSUm9hZHN0YXIgMTYyUm9hZHN0YX IgMTYxMjc2NCBIYXBweXZpbGxlMgdEZW5tYXJrOThA=;X-APPLE-RADIUS=49.913058665846 98;X-APPLE-REFERENCEFRAME=1;X-TITLE=:geo:52.382762,7.528319 END:VEVENT END:VCALENDAR collective-icalendar-138c845/src/icalendar/tests/conftest.py000066400000000000000000000351671524134033600241330ustar00rootroot00000000000000"""Test configuration""" import functools import itertools import sys import uuid import zoneinfo from collections.abc import Generator from datetime import datetime, timezone from pathlib import Path from unittest.mock import Mock from zoneinfo import ZoneInfo import pytest from dateutil import tz from icalendar import ( Alarm, Availability, Calendar, Component, ComponentFactory, Event, FreeBusy, Journal, LazyCalendar, Timezone, Todo, TypesFactory, prop, vUTCOffset, ) from icalendar.tests.data import PROPERTY_NAMES from icalendar.timezone import TZP from icalendar.timezone import tzp as _tzp from . import timezone_ids try: import pytz except ImportError: pytz = None HAS_PYTZ = pytz is not None if HAS_PYTZ: PYTZ_UTC = [ pytz.utc, pytz.timezone("UTC"), ] PYTZ_IN_TIMEZONE = [ lambda dt, tzname: pytz.timezone(tzname).localize(dt), ] PYTZ_TZP = ["pytz"] else: PYTZ_UTC = [] PYTZ_IN_TIMEZONE = [] PYTZ_TZP = [] class DataSource: """A collection of parsed ICS elements (e.g calendars, timezones, events)""" extensions = [".ics", ".jcal"] def __init__( self, data_source_folder: Path, parser: type[Component], multiple: bool = False ) -> None: self._parser = parser self._data_source_folder = data_source_folder self._multiple = multiple def keys(self): """Return all the files that could be used.""" return [ p.stem for p in self._data_source_folder.iterdir() if p.suffix.lower() in self.extensions ] def __getitem__(self, attribute): """Parse a file and return the result stored in the attribute.""" # parse the file ending extensions = self.extensions if "." in attribute: # we have a file ending attribute, extension = attribute.rsplit(".", 1) extensions = ["." + extension] for extension in extensions: source_file = attribute + extension source_path = self._data_source_folder / source_file if source_path.is_file(): break if not source_path.is_file(): # usually only raised if the test is wrong raise AttributeError( f"{attribute} does not exist with these extensions: {', '.join(extensions)}." ) # check different source types raw_ics: bytes | None = None raw_jcal: str | None = None if extension == ".jcal": raw_jcal = source_path.read_text() else: raw_ics = source_path.read_bytes() def get_parsed(self): source = ( self._parser.from_ical(raw_ics, multiple=self._multiple) if raw_ics else self._parser.from_jcal(raw_jcal) ) if self._multiple: return source source.raw_ics = raw_ics source.raw_jcal = raw_jcal source.source_file = source_file return source setattr(self.__class__, attribute, property(get_parsed)) return getattr(self, attribute) def __contains__(self, key): """key in self.keys()""" key = key.rsplit(".", 1)[0] return key in self.keys() def __getattr__(self, key): return self[key] def __repr__(self): return repr(self.__dict__) @property def multiple(self): """Return a list of all components parsed.""" return self.__class__(self._data_source_folder, self._parser, multiple=True) @classmethod @functools.cache def from_folder(cls, data_source_folder: Path, parser: type[Component]): """Parse all files in a folder and return a DataSource with the results.""" new_cls = type(f"DataSource{parser.name}", (cls,), {}) return new_cls(data_source_folder, parser) HERE = Path(__file__).parent CALENDARS_FOLDER = HERE / "calendars" TIMEZONES_FOLDER = HERE / "timezones" EVENTS_FOLDER = HERE / "events" ALARMS_FOLDER = HERE / "alarms" AVAILABILITIES_FOLDER = HERE / "availabilities" TODOS_FOLDER = HERE / "todos" JOURNALS_FOLDER = HERE / "journals" FREEBUSY_FOLDER = HERE / "freebusy" @pytest.fixture(scope="module", params=[Calendar, LazyCalendar]) def calendars(tzp, request): """Return the data source for calendar files. We have two ways of parsing those: - Immediately - Lazily with LazyCalendar """ return DataSource.from_folder(CALENDARS_FOLDER, request.param) @pytest.fixture def lazy_calendars(tzp): """Return the data source for calendar files.""" return DataSource.from_folder(CALENDARS_FOLDER, LazyCalendar) @pytest.fixture(scope="module") def timezones(tzp): return DataSource.from_folder(TIMEZONES_FOLDER, Timezone) @pytest.fixture(scope="module") def events(tzp): return DataSource.from_folder(EVENTS_FOLDER, Event) @pytest.fixture(scope="module") def alarms(tzp): return DataSource.from_folder(ALARMS_FOLDER, Alarm) @pytest.fixture(scope="module") def availabilities(tzp): return DataSource.from_folder(AVAILABILITIES_FOLDER, Availability) @pytest.fixture(scope="module") def todos(tzp): return DataSource.from_folder(TODOS_FOLDER, Todo) @pytest.fixture(scope="module") def journals(tzp): return DataSource.from_folder(JOURNALS_FOLDER, Journal) @pytest.fixture(scope="module") def freebusy(tzp): return DataSource.from_folder(FREEBUSY_FOLDER, FreeBusy) @pytest.fixture( params=PYTZ_UTC + [ZoneInfo("UTC"), tz.UTC, tz.gettz("UTC"), timezone.utc] ) def utc(request, tzp): return request.param @pytest.fixture( params=PYTZ_IN_TIMEZONE + [ lambda dt, tzname: dt.replace(tzinfo=tz.gettz(tzname)), lambda dt, tzname: dt.replace(tzinfo=ZoneInfo(tzname)), ] ) def in_timezone(request, tzp): return request.param FUZZ_TESTCASES_BROKEN_CALENDARS = "fuzz_testcase" # exclude broken calendars here BROKEN_SOURCE_FILES = ( "big_bad_calendar.ics", "issue_104_broken_calendar.ics", "issue_1050_multiple_calendars.ics", "small_bad_calendar.ics", "multiple_calendar_components.ics", "pr_480_summary_with_colon.ics", "parsing_error_in_UTC_offset.ics", "parsing_error.ics", ) SOURCE_FILES = [ file.name for file in itertools.chain( CALENDARS_FOLDER.iterdir(), TIMEZONES_FOLDER.iterdir(), EVENTS_FOLDER.iterdir() ) if file.name not in BROKEN_SOURCE_FILES and file.suffix in (".ics", ".jcal") and FUZZ_TESTCASES_BROKEN_CALENDARS not in file.name ] ICS_FILES = [ file.name for file in itertools.chain( CALENDARS_FOLDER.iterdir(), TIMEZONES_FOLDER.iterdir(), EVENTS_FOLDER.iterdir() ) if file.name not in BROKEN_SOURCE_FILES and file.suffix == ".ics" and FUZZ_TESTCASES_BROKEN_CALENDARS not in file.name ] JCAL_FILES = [ file.name for file in itertools.chain( CALENDARS_FOLDER.iterdir(), TIMEZONES_FOLDER.iterdir(), EVENTS_FOLDER.iterdir() ) if file.name not in BROKEN_SOURCE_FILES and file.suffix == ".jcal" and FUZZ_TESTCASES_BROKEN_CALENDARS not in file.name ] def get_source_file(calendars, timezones, events, request) -> Component: source_file = request.param print("example file:", source_file) for data in calendars, timezones, events: if source_file in data: return data[source_file] raise ValueError(f"Could not find file {source_file}.") @pytest.fixture(params=SOURCE_FILES) def source_file(tzp, calendars, timezones, events, request) -> Component: """An example file. source_file.raw_ics - bytes if .ics file source_file.raw_jcal - str if .jcal file """ return get_source_file(calendars, timezones, events, request) @pytest.fixture(params=ICS_FILES) def ics_file(tzp, calendars, timezones, events, request) -> Component: """An example .ica file. source_file.raw_ics - bytes source_file.raw_jcal - None """ return get_source_file(calendars, timezones, events, request) @pytest.fixture(params=JCAL_FILES) def jcal_file(tzp, calendars, timezones, events, request) -> Component: """An example .jcal file. source_file.raw_ics - None source_file.raw_jcal - str """ return get_source_file(calendars, timezones, events, request) FUZZ_V1 = [ key for key in CALENDARS_FOLDER.iterdir() if FUZZ_TESTCASES_BROKEN_CALENDARS in str(key) ] @pytest.fixture(params=FUZZ_V1) def fuzz_v1_calendar_path(request): """Clusterfuzz calendars.""" return request.param @pytest.fixture def types_factory(): """Return a new types factory.""" return TypesFactory() @pytest.fixture def component_factory(): """Return a new types factory.""" return ComponentFactory() @pytest.fixture def x_sometime(types_factory): """Map x_sometime to time""" types_factory.types_map["X-SOMETIME"] = "time" yield types_factory.types_map.pop("X-SOMETIME") @pytest.fixture def factory(): """Return a new component factory.""" return ComponentFactory() @pytest.fixture def vUTCOffset_ignore_exceptions(): vUTCOffset.ignore_exceptions = True yield vUTCOffset.ignore_exceptions = False @pytest.fixture def event_component(tzp): """Return an event component.""" c = Component() c.name = "VEVENT" return c @pytest.fixture def c(tzp): """Return an empty component.""" return Component() comp = c @pytest.fixture def calendar_component(tzp): """Return an empty component.""" c = Component() c.name = "VCALENDAR" return c @pytest.fixture def dont_validate_new(): """Remove validation for new() of components.""" value = Component._validate_new Component._validate_new = False yield Component._validate_new = value @pytest.fixture def filled_event_component(c, calendar_component): """Return an event with some values and add it to calendar_component.""" e = Component(summary="A brief history of time") e.name = "VEVENT" e.add("dtend", "20000102T000000", encode=0) e.add("dtstart", "20000101T000000", encode=0) calendar_component.add_component(e) return e @pytest.fixture def calendar_with_resources(tzp): c = Calendar() c["resources"] = 'Chair, Table, "Room: 42"' return c @pytest.fixture(scope="module") def tzp(tzp_name) -> Generator[TZP, None, None]: """The timezone provider.""" _tzp.use(tzp_name) yield _tzp _tzp.use_default() @pytest.fixture(params=PYTZ_TZP + ["zoneinfo"]) def other_tzp(request, tzp): """This is annother timezone provider. The purpose here is to cross test: pytz <-> zoneinfo. tzp as parameter makes sure we test the cross product. """ return TZP(request.param) @pytest.fixture def pytz_only(tzp, tzp_name) -> str: """Skip tests that are not running under pytz.""" assert tzp.uses_pytz() return tzp_name @pytest.fixture def zoneinfo_only(tzp, request, tzp_name) -> str: """Skip tests that are not running under zoneinfo.""" assert tzp.uses_zoneinfo() return tzp_name @pytest.fixture def no_pytz(tzp_name) -> str: """Do not run tests with pytz.""" assert tzp_name != "pytz" return tzp_name @pytest.fixture def no_zoneinfo(tzp_name) -> str: """Do not run tests with zoneinfo.""" assert tzp_name != "zoneinfo" return tzp_name def pytest_generate_tests(metafunc): """Parametrize without skipping: tzp_name will be parametrized according to the use of - pytz_only - zoneinfo_only - no_pytz - no_zoneinfo See https://docs.pytest.org/en/6.2.x/example/parametrize.html#deferring-the-setup-of-parametrized-resources """ if "tzp_name" in metafunc.fixturenames: tzp_names = PYTZ_TZP + ["zoneinfo"] if "zoneinfo_only" in metafunc.fixturenames: tzp_names = ["zoneinfo"] if "pytz_only" in metafunc.fixturenames: tzp_names = PYTZ_TZP assert not ( "zoneinfo_only" in metafunc.fixturenames and "pytz_only" in metafunc.fixturenames ), "Use pytz_only or zoneinfo_only but not both!" for name in ["pytz", "zoneinfo"]: if f"no_{name}" in metafunc.fixturenames and name in tzp_names: tzp_names.remove(name) metafunc.parametrize("tzp_name", tzp_names, scope="module") class DoctestZoneInfo(ZoneInfo): """Constent ZoneInfo representation for tests.""" def __repr__(self): return f"ZoneInfo(key={self.key!r})" def doctest_print(*args): """doctest print""" args = [arg.decode("UTF-8") if isinstance(arg, bytes) else arg for arg in args] print(*(str(arg).strip().replace("\r\n", "\n").replace("\r", "\n") for arg in args)) def doctest_import(name, *args, **kw): """Replace the import mechanism to skip the whole doctest if we import pytz.""" if name == "pytz": return pytz return __import__(name, *args, **kw) NOW = datetime(2025, 5, 17, 8, 6, 12) NOW_UTC = NOW.replace(tzinfo=timezone.utc) UID_DEFAULT = "d755cef5-2311-46ed-a0e1-6733c9e15c63" DEFAULT_PRODID = "-//collective//icalendar//7.0.0//EN" @pytest.fixture def test_uid() -> str: """The UID that tests always create.""" return UID_DEFAULT @pytest.fixture(autouse=True) def fixed_env(monkeypatch): """Create a fixed test environment for the functions that are time and randomness dependent.""" # uuid is fixed for all tests uid = uuid.UUID(UID_DEFAULT, version=4) monkeypatch.setattr(uuid, "uuid4", lambda: uid) # now is fixed monkeypatch.setattr(Component, "_utc_now", staticmethod(lambda: NOW_UTC)) @pytest.fixture def env_for_doctest(monkeypatch): """Modify the environment to make doctests run.""" monkeypatch.setitem(sys.modules, "zoneinfo", zoneinfo) monkeypatch.setattr(zoneinfo, "ZoneInfo", DoctestZoneInfo) from icalendar.timezone.zoneinfo import ZONEINFO monkeypatch.setattr(ZONEINFO, "utc", zoneinfo.ZoneInfo("UTC")) from icalendar.cal import calendar monkeypatch.setattr(calendar, "DEFAULT_PRODID", DEFAULT_PRODID) return {"print": doctest_print} @pytest.fixture(params=timezone_ids.TZIDS) def tzid(request: pytest.FixtureRequest) -> str: """Return a timezone id to be used with pytz or zoneinfo. This goes through all the different timezones possible. """ return request.param @pytest.fixture(params=PROPERTY_NAMES) def v_prop_name(request): """Names of property types that occur as a property of a component.""" return request.param @pytest.fixture def v_prop(v_prop_name): """Property types that occur as a property of a component.""" return getattr(prop, v_prop_name) @pytest.fixture def v_prop_example(v_prop) -> prop.VPROPERTY: return v_prop.examples()[0] @pytest.fixture def mock(): """A mock.""" return Mock() collective-icalendar-138c845/src/icalendar/tests/data.py000066400000000000000000000007101524134033600232010ustar00rootroot00000000000000"""Shared data between tests.""" from icalendar import prop PROPERTY_NAMES = {attr for attr in dir(prop) if attr[0] == "v" and attr[1].isupper()} # remove values for now that are in parameters only PARAMETER_NAMES = {"vSkip", "vWeekday", "vFrequency", "vMonth", "vInline"} # vBroken is a fallback type, not a normal parseable property type PROPERTY_NAMES -= PARAMETER_NAMES PROPERTY_NAMES -= {"vBroken"} __all__ = ["PARAMETER_NAMES", "PROPERTY_NAMES"] collective-icalendar-138c845/src/icalendar/tests/events/000077500000000000000000000000001524134033600232245ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/events/event_with_escaped_character1.ics000066400000000000000000000000741524134033600316620ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\, 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_escaped_character2.ics000066400000000000000000000000741524134033600316630ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\\ 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_escaped_character3.ics000066400000000000000000000000741524134033600316640ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\; 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_escaped_character4.ics000066400000000000000000000000741524134033600316650ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=Society\: 2014:that END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_escaped_characters.ics000066400000000000000000000001501524134033600317570ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\: END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_recurrence.ics000066400000000000000000000003101524134033600303070ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:19960401T010000 DTEND:19960401T020000 RRULE:FREQ=DAILY;COUNT=100 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z SUMMARY:A recurring event with exdates END:VEVENT event_with_recurrence_exdates_on_different_lines.ics000066400000000000000000000007101524134033600356650ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/eventsBEGIN:VEVENT DTSTART;TZID=Europe/Vienna:20120327T100000 DTEND;TZID=Europe/Vienna:20120327T180000 RRULE:FREQ=WEEKLY;UNTIL=20120703T080000Z;BYDAY=TU EXDATE;TZID=Europe/Vienna:20120529T100000 EXDATE;TZID=Europe/Vienna:20120403T100000 EXDATE;TZID=Europe/Vienna:20120410T100000 EXDATE;TZID=Europe/Vienna:20120501T100000 EXDATE;TZID=Europe/Vienna:20120417T100000 DTSTAMP:20130716T120638Z SUMMARY:A Recurring event with multiple exdates, one per line. END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_rsvp.ics000066400000000000000000000001111524134033600271430ustar00rootroot00000000000000BEGIN:VEVENT ATTENDEE;RSVP=TRUE:mailto:someone@example.com END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_unicode_fields.ics000066400000000000000000000003761524134033600311420ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20101010T100000Z DTEND:20101010T120000Z CREATED:20101010T100000Z UID:123456 SUMMARY:Non-ASCII Test: ÄÖÜ äöü € DESCRIPTION:icalendar should be able to handle non-ascii: €äüöÄÜÖ. LOCATION:Tribstrül END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/event_with_unicode_organizer.ics000066400000000000000000000001261524134033600316650ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN="Джон До�":mailto:john.doe@example.org END:VEVENT issue_100_transformed_doctests_into_unittests.ics000066400000000000000000000000621524134033600350420ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/eventsBEGIN:VEVENT SUMMARY;LANGUAGE=ru:te END:VEVENT issue_101_icalendar_chokes_on_umlauts_in_organizer.ics000066400000000000000000000005471524134033600357360ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/eventsBEGIN:VEVENT SUMMARY:wichtiger termin 1 DTSTART:20130416T100000Z DTEND:20130416T110000Z DTSTAMP:20130416T092616Z UID:20130416112341.10064jz0k4j7uem8@acmenet.de CLASS:PUBLIC CREATED:20130416T092341Z LAST-MODIFIED:20130416T092341Z LOCATION:im büro ORGANIZER;CN="acme, ädmin":mailto:adm-acme@mydomain.de STATUS:CONFIRMED TRANSP:OPAQUE END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_104_mark_events_broken.ics000066400000000000000000000002441524134033600313760ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20140401T000000Z DTEND:20140401T010000Z DTSTAMP:20140401T000000Z SUMMARY:Broken Eevnt CLASS:PUBLIC STATUS:CONFIRMED TRANSP:OPAQUE X END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_112_missing_tzinfo_on_exdate.ics000066400000000000000000000012361524134033600326110ustar00rootroot00000000000000BEGIN:VEVENT DTSTART;TZID=America/New_York:20130907T120000 DTEND;TZID=America/New_York:20130907T170000 RRULE:FREQ=WEEKLY;BYDAY=FR,SA;UNTIL=20131025T035959Z EXDATE;TZID=America/New_York:20131012T120000 EXDATE;TZID=America/New_York:20131011T120000 DTSTAMP:20131021T025552Z UID:ak30b02u7858q1oo6ji9dm4mgg@google.com CREATED:20130903T181453Z DESCRIPTION:The Fieldhouse and Hard Rock Cafe are working with PhillyRising to provide live entertainment on Friday and Saturday afternoons throughout the Summer. LAST-MODIFIED:20131015T210927Z LOCATION:12th and Market Streets (weather permitting) SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Market East Live! TRANSP:OPAQUE END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD.ics000066400000000000000000000002521524134033600304620ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD_list.ics000066400000000000000000000003061524134033600315150ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H 30M END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_157_removes_trailing_semicolon.ics000066400000000000000000000001341524134033600331470ustar00rootroot00000000000000BEGIN:VEVENT DTSTART:20150325T101010 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; END:VEVENT issue_184_broken_representation_of_period.ics000066400000000000000000000002031524134033600340740ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/eventsBEGIN:VEVENT DTSTART:20150219T133000 DTSTAMP:20150219T133000 UID:1234567 RDATE;VALUE=PERIOD:20150219T133000/PT10H END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_355_url_escaping.ics000066400000000000000000000006731524134033600302110ustar00rootroot00000000000000BEGIN:VEVENT DTSTAMP:20220822T164528Z UID:1661186704812-62650@ical.marudot.com DTSTART;TZID=Europe/Berlin:20220822T120000 DTEND;TZID=Europe/Berlin:20220822T120000 SUMMARY:test DESCRIPTION:https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/issue_355_url_escaping_2.ics000066400000000000000000000006731524134033600304320ustar00rootroot00000000000000BEGIN:VEVENT DTSTAMP:20220822T164528Z UID:1661186704812-62650@ical.marudot.com DTSTART;TZID=Europe/Berlin:20220822T120000 DTEND;TZID=Europe/Berlin:20220822T120000 SUMMARY:test DESCRIPTION:https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/issue_355_url_escaping_empty_param.ics000066400000000000000000000000541524134033600326000ustar00rootroot00000000000000BEGIN:VEVENT ORGANIZER;CN="":that END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/issue_464_invalid_rdate.ics000066400000000000000000000003011524134033600303300ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:RDATE period DTSTART:19961230T020000Z DTEND:19961230T060000Z UID:rdate_period RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_53_description_parsed_properly.ics000066400000000000000000000013431524134033600332610ustar00rootroot00000000000000BEGIN:VEVENT DTSTAMP:20120605T003759Z DTSTART;TZID=America/New_York:20120712T183000 DTEND;TZID=America/New_York:20120712T213000 STATUS:CONFIRMED SUMMARY:DevOps DC Meetup DESCRIPTION:DevOpsDC\nThursday\, July 12 at 6:30 PM\n\nThis will be a joi nt meetup / hack night with the DC jQuery Users Group. The idea behind the hack night: Small teams consisting of at least 1 member...\n\nDeta ils: http://www.meetup.com/DevOpsDC/events/47635522/ CLASS:PUBLIC CREATED:20120111T120339Z GEO:38.90;-77.01 LOCATION:Fathom Creative\, Inc. (1333 14th Street Northwest\, Washington D.C.\, DC 20005) URL:http://www.meetup.com/DevOpsDC/events/47635522/ LAST-MODIFIED:20120522T174406Z UID:event_qtkfrcyqkbnb@meetup.com END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_64_event_with_ascii_summary.ics000066400000000000000000000000521524134033600325430ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:abcdef END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_64_event_with_non_ascii_summary.ics000066400000000000000000000000521524134033600334150ustar00rootroot00000000000000BEGIN:VEVENT SUMMARY:åäö END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_70_rrule_causes_attribute_error.ics000066400000000000000000000004271524134033600334350ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20081114T072804Z UID:D449CA84-00A3-4E55-83E1-34B58268853B DTEND:20070220T180000 RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20070619T225959 TRANSP:OPAQUE SUMMARY:Esb mellon phone conf DTSTART:20070220T170000 DTSTAMP:20070221T095412Z SEQUENCE:0 END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/issue_82_expected_output.ics000066400000000000000000000001331524134033600306630ustar00rootroot00000000000000BEGIN:VEVENT ATTACH;ENCODING=BASE64;FMTTYPE=text/plain;VALUE=BINARY:dGV4dA== END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/rfc_7265_example_4.ics000066400000000000000000000001001524134033600271060ustar00rootroot00000000000000BEGIN:VEVENT DTSTART;X-SLACK=30.3;VALUE=DATE:20110512 END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/rfc_7265_request_status.ics000066400000000000000000000002001524134033600303240ustar00rootroot00000000000000BEGIN:VEVENT REQUEST-STATUS:2.0;Success REQUEST-STATUS:3.7;Invalid calendar user;ATTENDEE: mailto:jsmith@example.org END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/rfc_9074_example_1.ics000066400000000000000000000005301524134033600271120ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20210302T151004Z UID:AC67C078-CED3-4BF5-9726-832C3749F627 DTSTAMP:20210302T151004Z DTSTART;TZID=America/New_York:20210302T103000 DTEND;TZID=America/New_York:20210302T113000 SUMMARY:Meeting BEGIN:VALARM UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 TRIGGER:-PT15M DESCRIPTION:Event reminder ACTION:DISPLAY END:VALARM END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/rfc_9074_example_2.ics000066400000000000000000000011131524134033600271110ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20210302T151004Z UID:AC67C078-CED3-4BF5-9726-832C3749F627 DTSTAMP:20210302T151516Z DTSTART;TZID=America/New_York:20210302T103000 DTEND;TZID=America/New_York:20210302T113000 SUMMARY:Meeting BEGIN:VALARM UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 TRIGGER:-PT15M DESCRIPTION:Event reminder ACTION:DISPLAY ACKNOWLEDGED:20210302T151514Z END:VALARM BEGIN:VALARM UID:DE7B5C34-83FF-47FE-BE9E-FF41AE6DD097 TRIGGER;VALUE=DATE-TIME:20210302T152000Z RELATED-TO;RELTYPE=SNOOZE:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 DESCRIPTION:Event reminder ACTION:DISPLAY END:VALARM END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/rfc_9074_example_3.ics000066400000000000000000000011131524134033600271120ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20210302T151004Z UID:AC67C078-CED3-4BF5-9726-832C3749F627 DTSTAMP:20210302T152026Z DTSTART;TZID=America/New_York:20210302T103000 DTEND;TZID=America/New_York:20210302T113000 SUMMARY:Meeting BEGIN:VALARM UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 TRIGGER:-PT15M DESCRIPTION:Event reminder ACTION:DISPLAY ACKNOWLEDGED:20210302T152024Z END:VALARM BEGIN:VALARM UID:87D690A7-B5E8-4EB4-8500-491F50AFE394 TRIGGER;VALUE=DATE-TIME:20210302T152500Z RELATED-TO;RELTYPE=SNOOZE:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 DESCRIPTION:Event reminder ACTION:DISPLAY END:VALARM END:VEVENT collective-icalendar-138c845/src/icalendar/tests/events/rfc_9074_example_4.ics000066400000000000000000000011501524134033600271140ustar00rootroot00000000000000BEGIN:VEVENT CREATED:20210302T151004Z UID:AC67C078-CED3-4BF5-9726-832C3749F627 DTSTAMP:20210302T152508Z DTSTART;TZID=America/New_York:20210302T103000 DTEND;TZID=America/New_York:20210302T113000 SUMMARY:Meeting BEGIN:VALARM UID:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 TRIGGER:-PT15M DESCRIPTION:Event reminder ACTION:DISPLAY ACKNOWLEDGED:20210302T152507Z END:VALARM BEGIN:VALARM UID:87D690A7-B5E8-4EB4-8500-491F50AFE394 TRIGGER;VALUE=DATE-TIME:20210302T152500Z RELATED-TO;RELTYPE=SNOOZE:8297C37D-BA2D-4476-91AE-C1EAA364F8E1 DESCRIPTION:Event reminder ACTION:DISPLAY ACKNOWLEDGED:20210302T152507Z END:VALARM END:VEVENTcollective-icalendar-138c845/src/icalendar/tests/events/rfc_9074_example_proximity.ics000066400000000000000000000004441524134033600310220ustar00rootroot00000000000000BEGIN:VEVENT BEGIN:VALARM UID:77D80D14-906B-4257-963F-85B1E734DBB6 ACTION:DISPLAY TRIGGER;VALUE=DATE-TIME:19760401T005545Z DESCRIPTION:Remember to buy milk PROXIMITY:DEPART BEGIN:VLOCATION UID:123456-abcdef-98765432 NAME:Office URL:geo:40.443,-79.945;u=10 END:VLOCATION END:VALARM END:VEVENT collective-icalendar-138c845/src/icalendar/tests/freebusy/000077500000000000000000000000001524134033600235445ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/freebusy/example.ics000066400000000000000000000005451524134033600257030ustar00rootroot00000000000000BEGIN:VFREEBUSY UID:19970901T115957Z-76A912@example.com DTSTAMP:19970901T120000Z ORGANIZER:jsmith@example.com DTSTART:19980313T141711Z DTEND:19980410T234500Z FREEBUSY:19980314T233000Z/19980315T003000Z FREEBUSY:19980316T153000Z/19980316T163000Z FREEBUSY:19980318T030000Z/19980318T040000Z URL:http://www.example.com/calendar/busytime/jsmith.ifb END:VFREEBUSY collective-icalendar-138c845/src/icalendar/tests/fuzzed/000077500000000000000000000000001524134033600232275ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/fuzzed/__init__.py000066400000000000000000000032101524134033600253340ustar00rootroot00000000000000"""This is a collection of test files that are generated from the fuzzer. The fuzzer finds the cases in which the icalendar module breaks. These test cases reproduce the failure. Some more tests can be added to make sure that the behavior works properly. """ _value_error_matches = [ "component", "parse", "Expected", "Wrong date format", "END encountered", "vDDD", "recurrence", "Offset must", "Invalid iCalendar", "alue MUST", "Key name", "Invalid content line", "Content line could not be parsed", "does not exist", "base 64", "must use datetime", "Unknown date type", "Wrong", "Start time", "iCalendar", "recurrence", "float, float", "utc offset", "parent", "MUST be a datetime", "Invalid month:", "must have exactly", # vCard field count validation (ADR, N) "must have at least", # vCard ORG minimum field validation "not enough values to unpack", # dateutil rejects a malformed VTIMEZONE content line ] def fuzz_v1_calendar( from_ical, calendar_string: str, multiple: bool, should_walk: bool ): """Take a from_ical function and reproduce the error. The calendar_string is a fuzzed input. """ try: cal = from_ical(calendar_string, multiple=multiple) if not multiple: cal = [cal] for c in cal: if should_walk: for event in c.walk("VEVENT"): event.to_ical() else: c.to_ical() except (ValueError, TypeError) as e: if any(m in str(e) for m in _value_error_matches): return -1 raise generate_python_test_cases_from_downloaded_clusterfuzz_test_cases.sh000077500000000000000000000031721524134033600412420ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/fuzzed#!/usr/bin/env bash # # This script generates a test case from a test case file that was downloaded. # # You will need to follow the setup instructions here: # https://google.github.io/oss-fuzz/advanced-topics/reproducing/#reproduce-using-local-source-checkout # set -e HERE="`dirname \"$0\"`" OSS_FUZZ_DIRECTORY="$HOME/oss-fuzz" DOWNLOADS_DIRECTORY="$HOME/Downloads" LOCAL_ICALENDAR_DIRECTORY="$HERE/../../../../" PYTHON_TEST_CASE_DIRECTORY="$HERE/../calendars/" PROJECT_NAME="icalendar" echo "### Building Project $PROJECT_NAME" python "$OSS_FUZZ_DIRECTORY/infra/helper.py" build_fuzzers --sanitizer undefined "$PROJECT_NAME" "$LOCAL_ICALENDAR_DIRECTORY" # we capture the output OUTPUT="`mktemp`" # test case files look like this: # clusterfuzz-testcase-minimized-ical_fuzzer-4878676239712256 for testcase in "$DOWNLOADS_DIRECTORY/clusterfuzz-testcase-"* do echo "### Reproducing $testcase" python "$OSS_FUZZ_DIRECTORY/infra/helper.py" reproduce "$PROJECT_NAME" ical_fuzzer "$testcase" | tee "$OUTPUT" if [ $PIPESTATUS -eq 0 ] then echo "### Testcase fixed! $testcase" continue fi echo "### Testcase reproduced! $testcase" TEST_FILE_CONTENT="`cat \"$OUTPUT\" | sed -n '/--- start calendar ---/,/--- end calendar ---/{/--- start calendar ---/b;/--- end calendar ---/b;p}'`" if [ -z "$TEST_FILE_CONTENT" ] then echo "### No test file content for $testcase" exit 1 fi ICS_FILE="$PYTHON_TEST_CASE_DIRECTORY/`basename \fuzz_testcase_"$testcase\"`.ics" # decode and ignore garbage, see https://stackoverflow.com/a/15490765/1320237 echo $TEST_FILE_CONTENT | base64 -di > /dev/null echo "Created $ICS_FILE" done collective-icalendar-138c845/src/icalendar/tests/fuzzed/test_fuzzed_calendars.py000066400000000000000000000013541524134033600301660ustar00rootroot00000000000000"""This test tests all fuzzed calendars. To add a new calendar test, echo the base64 calendar string into a file. - The file is in src/icalendar/tests/calendars - The file name contains "fuzz_testcase" and ends with ".ics" Templace code to create a new test case: .. code-block:: console echo "" | base64 -d > src/icalendar/tests/calendars/fuzz_testcase_.ics """ import icalendar.cal.calendar from icalendar.tests.fuzzed import fuzz_v1_calendar def test_fuzz_v1(fuzz_v1_calendar_path): """Test a calendar.""" calendar = fuzz_v1_calendar_path.read_bytes() print(repr(calendar)) fuzz_v1_calendar( icalendar.cal.calendar.Calendar.from_ical, calendar, multiple=True, should_walk=True, ) collective-icalendar-138c845/src/icalendar/tests/hypothesis/000077500000000000000000000000001524134033600241175ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/hypothesis/test_fuzzing.py000066400000000000000000000020161524134033600272230ustar00rootroot00000000000000import string import unittest import hypothesis.strategies as st from hypothesis import given, settings from icalendar.parser import Contentline, Contentlines, Parameters def printable_characters(**kw): return st.text(st.characters(blacklist_categories=("Cc", "Cs"), **kw)) key = st.text(string.ascii_letters + string.digits, min_size=1) value = printable_characters(blacklist_characters='\\;:"') class TestFuzzing(unittest.TestCase): @given( lines=st.lists(st.tuples(key, st.dictionaries(key, value), value), min_size=1) ) @settings(max_examples=10**3) def test_main(self, lines): cl = Contentlines() for key, params, value in lines: try: params = Parameters(**params) # noqa: PLW2901 except TypeError: # Happens when there is a random parameter 'self'... continue cl.append(Contentline.from_parts(key, params, value)) cl.append("") assert Contentlines.from_ical(cl.to_ical()) == cl collective-icalendar-138c845/src/icalendar/tests/journals/000077500000000000000000000000001524134033600235555ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/journals/example.ics000066400000000000000000000003071524134033600257100ustar00rootroot00000000000000BEGIN:VJOURNAL UID:19970901T130000Z-123405@example.com DTSTAMP:19970901T130000Z DTSTART;VALUE=DATE:19970317 SUMMARY:Staff meeting minutes DESCRIPTION:Aurora project plans were reviewed. END:VJOURNAL collective-icalendar-138c845/src/icalendar/tests/prop/000077500000000000000000000000001524134033600227005ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/prop/__init__.py000066400000000000000000000000521524134033600250060ustar00rootroot00000000000000"""Test the value types of properties.""" collective-icalendar-138c845/src/icalendar/tests/prop/test_common_functionality.py000066400000000000000000000134501524134033600305540ustar00rootroot00000000000000"""Check some module consistency.""" from datetime import date, datetime, time, timedelta import pytest import icalendar from icalendar import Parameters, prop, vDDDLists, vDDDTypes from icalendar.tests.data import PARAMETER_NAMES def test_all_propeties_are_exported(v_prop_name): """icalendar.prop should export all these properties.""" assert v_prop_name in prop.__all__ assert v_prop_name in icalendar.__all__ assert getattr(prop, v_prop_name, None) is not None assert getattr(icalendar, v_prop_name, None) is not None def test_all_properties_have_an_examples_method(v_prop): """All properties have a class method that returns a list of examples.""" examples = v_prop.examples() assert isinstance(examples, list), "We expect a list of examples." assert len(examples) > 0, "We have examples." assert all(isinstance(example, v_prop) for example in examples), ( "All examples should be of that type." ) assert v_prop.__name__ in v_prop.examples.__doc__.splitlines()[0], ( "type is in docstring" ) def test_all_properties_are_part_of_the_union_type(v_prop): """Check the union type captures all vProperty instances.""" assert v_prop in prop.VPROPERTY.__args__ @pytest.mark.parametrize("no_prop", PARAMETER_NAMES) def test_if_not_part_of_properties_they_are_not_included_in_the_type(no_prop): """If these are not properties rather parameters, do not include them.""" assert no_prop not in prop.VPROPERTY.__args__ default_value_map = { prop.vBoolean: "BOOLEAN", prop.vCalAddress: "CAL-ADDRESS", prop.vCategory: "TEXT", prop.vDate: "DATE", prop.vDatetime: "DATE-TIME", prop.vDuration: "DURATION", prop.vFloat: "FLOAT", prop.vInt: "INTEGER", prop.vPeriod: "PERIOD", prop.vRecur: "RECUR", prop.vText: "TEXT", prop.vTime: "TIME", prop.vUTCOffset: "UTC-OFFSET", prop.vUri: "URI", prop.vBinary: "BINARY", prop.vGeo: "FLOAT", } @pytest.mark.parametrize(("v_prop", "default_value"), list(default_value_map.items())) def test_get_default_value(v_prop_example: prop.VPROPERTY, default_value: str): """Check the default value of all properties.""" del v_prop_example.VALUE assert v_prop_example.VALUE == default_value assert "VALUE" not in v_prop_example.params def test_set_value(v_prop_example): """Test setting the VALUE parameter.""" v_prop_example.VALUE = "X-OTHER-VALUE" assert v_prop_example.VALUE == "X-OTHER-VALUE" assert v_prop_example.params.value == "X-OTHER-VALUE" def test_delete_the_set_value(v_prop_example, v_prop): """The value is defaulting after delete.""" v_prop_example.VALUE = "X-VALUE" del v_prop_example.VALUE assert v_prop_example.VALUE != "X-VALUE" if v_prop in default_value_map: assert v_prop_example.VALUE == default_value_map[v_prop], ( "deleted value defaults" ) @pytest.mark.parametrize("v_prop", [vDDDLists, vDDDTypes]) @pytest.mark.parametrize( ("dt", "value"), [ (datetime(2018, 1, 1), "DATE-TIME"), (date(2018, 1, 1), "DATE"), (time(1, 1), "TIME"), (timedelta(1, 1), "DURATION"), ], ) def test_dt_value(v_prop, dt, value): """Check that the VALUE parameter is correctly determined.""" assert v_prop(dt).VALUE == value def test_special_case_no_dts(): """If the list is empty, we still need a value.""" assert vDDDLists([]).VALUE == "DATE-TIME" def special_case_period(): """Check PERIOD for vDDDLists and vDDDTypes.""" assert vDDDLists([(datetime(2018, 1, 1), timedelta(1, 1))]).VALUE == "PERIOD" assert vDDDTypes((datetime(2018, 1, 1), timedelta(1, 1))).VALUE == "PERIOD" def test_value_parameter_does_not_turn_up_in_jcal(): """The VALUE parameter should not turn up if set.""" params = Parameters() params["VALUE"] = "DATE-TIME" assert params.to_jcal() == {} def test_value_is_always_uppercase(v_prop_example): """The VALUE parameter should always be uppercase.""" v_prop_example.VALUE = "unknown" assert v_prop_example.VALUE == "UNKNOWN" def test_setting_the_value_turns_it_uppercase(): """The VALUE parameter should always be uppercase.""" parameters = Parameters() parameters.value = "unknown" assert parameters.value == "UNKNOWN" def test_default_value_is_always_uppercase(): """The default value should always be uppercase.""" pytest.skip("TODO at a later stage when the __init__ function is implemented.") parameters = Parameters({"VALUE": "unknown1"}) assert parameters.value == "UNKNOWN1" assert parameters["VALUE"] == "UNKNOWN1" @pytest.mark.parametrize("attribute", ["VALUE", "to_jcal", "to_ical"]) def test_common_methods(v_prop, attribute): """Check that common functionality is provided by all of them.""" assert hasattr(v_prop, attribute), ( f'{v_prop.__name__} is missing a function or property named "{attribute}".' ) def test_all_possible_value_types_are_listed(types_factory): """Check that all values types added are also part of the TypesFactory.""" for value in types_factory.values(): assert value in prop.VPROPERTY.__args__, ( f"The value type {value} is not part of VPROPERTY." ) assert value in types_factory.all_types, ( f"The value type {value} is not part of TypesFactory." ) def test_all_value_types_are_in_the_factory(v_prop, types_factory): """Check that all vProperty types are part of the TypesFactory.""" assert v_prop in types_factory.all_types, ( f"The value type {v_prop} is not part of TypesFactory.all_types." ) def test_an_new_component_class_is_registered(component_factory): """Check that the result is always the same.""" assert component_factory.get_component_class( "VCUSTOM" ) is component_factory.get_component_class("VCUSTOM") collective-icalendar-138c845/src/icalendar/tests/prop/test_conference.py000066400000000000000000000167541524134033600264350ustar00rootroot00000000000000"""Test conference properties.""" import pytest from icalendar import Calendar, Event, Todo, vUri from icalendar.prop.conference import Conference def test_from_empty_uri(): """Test creation from a URI.""" conference = Conference.from_uri(vUri("https://chat.example.com/audio?id=123456")) assert conference.uri == "https://chat.example.com/audio?id=123456" assert conference.feature is None assert conference.label is None assert conference.language is None def test_from_example_uri(): """Create a conference with lots of values.""" conference = Conference.from_uri( vUri( "https://chat.example.com/audio?id=123456", params={ "FEATURE": "PHONE,MODERATOR", "LABEL": "Moderator dial-in", "LANGUAGE": "EN", }, ) ) assert conference.uri == "https://chat.example.com/audio?id=123456" assert conference.feature == "PHONE,MODERATOR" assert conference.label == "Moderator dial-in" assert conference.language == "EN" def test_to_uri(): """Test creating a vURI.""" uri = vUri( "https://chat.example.com/audio?id=123456", params={ "FEATURE": "PHONE", "LABEL": "Moderator", "LANGUAGE": "DE", "VALUE": "URI", }, ) conference = Conference.from_uri(uri) new_uri = conference.to_uri() assert new_uri == uri assert new_uri.params == uri.params def test_conference_list_params_serialization(): """Ensure list parameters are correctly serialized to comma-separated strings.""" conf = Conference( uri="https://example.com", feature=["AUDIO", "VIDEO"], label="Meeting room" ) vuri = conf.to_uri() assert isinstance(vuri.params["FEATURE"], str) assert vuri.params["FEATURE"] == "AUDIO,VIDEO" assert vuri.params["LABEL"] == "Meeting room" @pytest.fixture def conference_1(): """Fixture for a conference with a URI.""" return Conference.from_uri(vUri("https://chat.example.com/audio?id=123456")) @pytest.fixture def conference_2(): """Fixture for a conference with a URI.""" return Conference( vUri("https://chat.example.com/audio?id=123456"), feature="PHONE", label="Moderator dial-in", language="EN", ) @pytest.fixture(params=[Event, Todo]) def component_class(request): """Fixture to create a Conference component.""" return request.param @pytest.fixture def component(component_class): """Create a component.""" return component_class() def test_no_conferences(component): """No conferences by default.""" assert component.conferences == [] assert "CONFERENCE" not in component def test_add_conference(component, conference_1): """Add a new conference.""" component.conferences = [conference_1] assert component["CONFERENCE"] == conference_1.to_uri() def test_add_multiple_conferences(component, conference_1, conference_2): """Add a new conference.""" component.conferences = [conference_1, conference_2] assert component["CONFERENCE"] == [conference_1.to_uri(), conference_2.to_uri()] def test_new_component_with_conferences(component_class, conference_1, conference_2): """Create a new component with conferences.""" component = component_class.new(conferences=[conference_1, conference_2]) assert component.conferences == [conference_1, conference_2] def test_uri_in_ical_(conference_2, component): """The URI start be in the ical string.""" component.conferences = [conference_2] ical_str = component.to_ical().decode("utf-8") assert "CONFERENCE;" in ical_str assert "/audio?id=123456" in ical_str assert "PHONE" in ical_str def get_event(calendar: Calendar) -> Event: """Get the event directly""" return calendar.events[0] def get_serialized_event(calendar: Calendar) -> Event: """Get the event directly""" cal = Calendar.from_ical(calendar.to_ical()) return cal.events[0] @pytest.fixture(params=[get_event, get_serialized_event]) def event_with_conferences(request, calendars): """Return the event from the file.""" calendar = calendars.rfc_7986_conferences return request.param(calendar) def test_conferences_from_file(event_with_conferences): """The conferences should be in the calendar.""" assert len(event_with_conferences.conferences) == 5 assert event_with_conferences.conferences[0].uri == "tel:+1-412-555-0123,,,654321" assert event_with_conferences.conferences[1].uri == "tel:+1-412-555-0123,,,555123" assert event_with_conferences.conferences[2].uri == "tel:+1-888-555-0456,,,555123" assert ( event_with_conferences.conferences[3].uri == "xmpp:chat-123@conference.example.com" ) assert ( event_with_conferences.conferences[4].uri == "https://chat.example.com/audio?id=123456" ) assert event_with_conferences.conferences[4].feature == ["AUDIO", "VIDEO"] assert event_with_conferences.conferences[4].label == "Attendee dial-in" assert event_with_conferences.conferences[4].language is None def test_conference_from_string(): """Create a conference from a string.""" conference = Conference.from_uri("tel:+1-412-555-0123") assert conference.uri == "tel:+1-412-555-0123" assert conference.feature is None assert conference.label is None assert conference.language is None def test_from_uri_string_adds_value_type(): """Conference created from a string should add the value type.""" conference = Conference.from_uri("http://asd") uri = conference.to_uri() assert uri.VALUE == "URI" def test_string_feature_passthrough(): """A string feature should be kept as-is, not wrapped in a list.""" conf = Conference(uri="https://example.com", feature="AUDIO") vuri = conf.to_uri() assert vuri.params["FEATURE"] == "AUDIO" def test_empty_list_feature_filtered(): """An empty list feature should be omitted from params entirely.""" conf = Conference(uri="https://example.com", feature=[]) vuri = conf.to_uri() assert "FEATURE" not in vuri.params def test_none_feature_omitted(): """A None feature should not appear in params.""" conf = Conference(uri="https://example.com") vuri = conf.to_uri() assert "FEATURE" not in vuri.params @pytest.mark.parametrize( ("param_name", "kwargs"), [ ("FEATURE", {"feature": "AUDIO"}), ("LABEL", {"label": "Room A"}), ("LANGUAGE", {"language": "EN"}), ], ) def test_string_passthrough_all_params(param_name, kwargs): """String values for feature, label, and language should pass through unchanged.""" conf = Conference(uri="https://example.com", **kwargs) vuri = conf.to_uri() assert vuri.params[param_name] == list(kwargs.values())[0] @pytest.mark.parametrize( ("param_name", "kwargs"), [ ("FEATURE", {"feature": []}), ("LABEL", {"label": []}), ("LANGUAGE", {"language": []}), ], ) def test_empty_list_filtered_all_params(param_name, kwargs): """Empty lists for feature, label, and language should be omitted from params.""" conf = Conference(uri="https://example.com", **kwargs) vuri = conf.to_uri() assert param_name not in vuri.params @pytest.mark.parametrize("param_name", ["FEATURE", "LABEL", "LANGUAGE"]) def test_none_omitted_all_params(param_name): """None values for feature, label, and language should not appear in params.""" conf = Conference(uri="https://example.com") vuri = conf.to_uri() assert param_name not in vuri.params collective-icalendar-138c845/src/icalendar/tests/prop/test_constructors.py000066400000000000000000000104131524134033600270600ustar00rootroot00000000000000import datetime import pytest from icalendar.prop import ( vBoolean, vCalAddress, vCategory, vDatetime, vDuration, vFloat, vFrequency, vGeo, vInline, vInt, vMonth, vRecur, vText, vUri, vUTCOffset, vWeekday, ) def test_param_vCategory(): obj = vCategory(["Work", "Personal"], params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vCategory) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vCalAddress(): obj = vCalAddress("mailto:jane_doe@example.com", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vCalAddress) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vWeekday(): obj = vWeekday("2FR", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vWeekday) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vBoolean(): obj = vBoolean(True, params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vBoolean) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vDuration(): td = datetime.timedelta(days=15, seconds=18020) obj = vDuration(td, params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vDuration) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vFloat(): obj = vFloat("1.333", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vFloat) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vGeo(): obj = vGeo((37.386013, -122.082932), params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vGeo) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vInt(): obj = vInt("87", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vInt) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vInline(): obj = vInline("sometxt", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vInline) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vText(): obj = vText("sometxt", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vText) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vMonth(): obj = vMonth(1, params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vMonth) assert obj.params["SOME_PARAM"] == "VALUE" def test_vMonth_validation(): """Test validation of vMonth with leap month format.""" # Valid cases assert vMonth(1) == 1 assert vMonth("1") == 1 assert vMonth("5L") == 5 assert vMonth("5L").leap # Invalid cases with pytest.raises(ValueError, match="Invalid month"): vMonth("L") # Just L without digits with pytest.raises(ValueError, match="Invalid month"): vMonth("1LL") # Double L with pytest.raises(ValueError, match="Invalid month"): vMonth("abc") # Letters not in valid format with pytest.raises(ValueError, match="Invalid month"): vMonth("") # Empty string # Non-ASCII digits pass ``str.isdigit`` but are not RFC month values. with pytest.raises(ValueError, match="Invalid month"): vMonth("١٢") # Arabic-Indic "12", silently accepted before with pytest.raises(ValueError, match="Invalid month"): vMonth("²") # superscript two, leaked a raw int() error before with pytest.raises(ValueError, match="Invalid month"): vMonth("٥L") # Arabic-Indic "5" with leap suffix def test_param_vUTCOffset(): obj = vUTCOffset( datetime.timedelta(days=-1, seconds=68400), params={"SOME_PARAM": "VALUE"} ) assert isinstance(obj, vUTCOffset) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vFrequency(): obj = vFrequency("DAILY", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vFrequency) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vRecur(): obj = vRecur({"FREQ": ["DAILY"], "COUNT": [10]}, params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vRecur) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vDatetime(): dt = datetime.datetime(2025, 3, 16, 14, 30, 0, tzinfo=datetime.timezone.utc) obj = vDatetime(dt, params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vDatetime) assert obj.params["SOME_PARAM"] == "VALUE" def test_param_vUri(): obj = vUri("WWW.WESBITE.COM", params={"SOME_PARAM": "VALUE"}) assert isinstance(obj, vUri) assert obj.params["SOME_PARAM"] == "VALUE" collective-icalendar-138c845/src/icalendar/tests/prop/test_crlf_in_values.py000066400000000000000000000040101524134033600272770ustar00rootroot00000000000000r"""``URI``, ``CAL-ADDRESS`` and inline values must reject raw CR and LF. Unlike ``TEXT``, these value types are not escaped on serialization, so a raw ``\r`` or ``\n`` in the value is written straight into the content line. A lone ``\r`` slips past the newline-only assertion in ``Contentline`` and reparses as a separate property for consumers that treat a bare CR as a line break, which is content-line injection. The values now reject CR and LF at construction. """ import pytest from icalendar import Calendar, vCalAddress from icalendar.prop import vUri from icalendar.prop.inline import vInline from icalendar.prop.xml_reference import vXmlReference VERBATIM_VALUE_TYPES = [vUri, vCalAddress, vInline, vXmlReference] CRLF_INJECTION_VALUES = [ "mailto:a@b.com\rINJECTED:evil", "mailto:a@b.com\nINJECTED:evil", "http://e.com/a\r\nX-EVIL:1", "trailing\r", ] JCAL_PROPERTIES = [ ("url", "uri"), ("attendee", "cal-address"), ("organizer", "cal-address"), ] @pytest.mark.parametrize("cls", VERBATIM_VALUE_TYPES) @pytest.mark.parametrize("value", CRLF_INJECTION_VALUES) def test_value_rejects_cr_and_lf(cls, value): with pytest.raises(ValueError): cls.from_ical(value) @pytest.mark.parametrize("cls", VERBATIM_VALUE_TYPES) def test_valid_values_still_parse(cls): assert cls.from_ical("http://example.com/my-report.txt") @pytest.mark.parametrize(("prop", "jcal_type"), JCAL_PROPERTIES) @pytest.mark.parametrize("value", CRLF_INJECTION_VALUES) def test_jcal_value_with_cr_or_lf_is_rejected(prop, jcal_type, value): """A CR or LF from untrusted jCal must not reach the serialized output.""" jcal = [ "vcalendar", [["version", {}, "text", "2.0"], ["prodid", {}, "text", "x"]], [ [ "vevent", [ ["uid", {}, "text", "1"], [prop, {}, jcal_type, value], ], [], ] ], ] with pytest.raises(ValueError): Calendar.from_jcal(jcal) collective-icalendar-138c845/src/icalendar/tests/prop/test_date_and_time.py000066400000000000000000000053401524134033600270700ustar00rootroot00000000000000"""Testing functionality that is common to date and time property types.""" from datetime import datetime, time from typing import TYPE_CHECKING from zoneinfo import ZoneInfo import pytest from icalendar import Component, vDatetime, vDDDLists, vDDDTypes, vTime from icalendar.parser_tools import to_unicode from icalendar.prop import VPROPERTY from icalendar.timezone.tzid import is_utc if TYPE_CHECKING: from icalendar.cal.event import Event @pytest.fixture( params=[ vDatetime(datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))), vTime(time(1, 1, tzinfo=ZoneInfo("UTC"))), ] ) def utc_prop(request): return request.param @pytest.mark.parametrize("convert", [None, vDDDLists, vDDDTypes]) def test_setting_a_utc_datetime_value_does_not_include_a_TZID( utc_prop: VPROPERTY, convert ): """Check that the VALUE parameter is correctly determined.""" if convert: utc_prop = convert(utc_prop.dt) assert not utc_prop.params.tzid, "UTC datetimes must not carry a TZID parameter" component = Component() component.add("X-PROP", utc_prop) component.add("DTSTAMP", utc_prop) component.add("DTSTART", utc_prop) component.add("DUE", utc_prop) ical = component.to_ical().decode() print(ical) assert "TZID" not in ical def test_converting_to_utc_puts_a_z_in_the_end(utc_prop: VPROPERTY): """Test that the Z is appended. :rfc:`5545`, TIME: UTC time, or absolute time, is identified by a LATIN CAPITAL LETTER Z suffix character, the UTC designator, appended to the time value. For example, the following represents 07:00 AM UTC: .. code-block:: text 070000Z The "TZID" property parameter MUST NOT be applied to TIME properties whose time values are specified in UTC. :rfc:`5545`, DATE-TIME: The date with UTC time, or absolute time, is identified by a LATIN CAPITAL LETTER Z suffix character, the UTC designator, appended to the time value. For example, the following represents January 19, 1998, at 0700 UTC: .. code-block:: text 19980119T070000Z The "TZID" property parameter MUST NOT be applied to DATE-TIME properties whose time values are specified in UTC. """ ical = to_unicode(utc_prop.to_ical()) assert ical.endswith("Z") def test_reformed_UTC_serialization(events): """Check that UTC values are still propertly serialized.""" event: Event = events.issue_156_RDATE_with_PERIOD utc_value = event["DTSTART"] assert is_utc(utc_value.dt) # TODO: the UTC parameter is special - it can be present but should not be serialized # How should we treat the parameter? # I think: have it there, but do not serialize it collective-icalendar-138c845/src/icalendar/tests/prop/test_duration_overflow.py000066400000000000000000000056401524134033600300660ustar00rootroot00000000000000r"""A duration too large for ``datetime.timedelta`` must raise ``InvalidCalendar``. ``DURATION_REGEX`` accepts an unbounded run of digits, but ``timedelta`` rejects values that overflow its C implementation with ``OverflowError``. That error used to leak out of :meth:`vDuration.from_ical`, so callers catching the documented :exc:`ValueError` / :class:`~icalendar.error.InvalidCalendar` crashed instead. It is now reported as an invalid duration, like any other bad value. """ import pytest from icalendar import Calendar from icalendar.error import InvalidCalendar from icalendar.prop import vDuration @pytest.mark.parametrize( "value", [ "P999999999999999999W", "P1000000000000000000000D", "PT999999999999999999999H", ], ) def test_vDuration_from_ical_rejects_overflowing_duration(value): """An overflowing duration raises InvalidCalendar, not OverflowError.""" with pytest.raises(InvalidCalendar): vDuration.from_ical(value) def test_overflowing_duration_is_a_value_error(): """InvalidCalendar is a ValueError, so the common except clause catches it.""" with pytest.raises(ValueError): vDuration.from_ical("P999999999999999999W") def test_overflowing_duration_in_calendar_is_recorded_not_raised(): """A full calendar parse records the bad duration instead of crashing. Before the fix the ``OverflowError`` escaped the component parser (which only catches ``ValueError``) and aborted the whole parse. Now the value is handled like any other invalid property and recorded in ``errors``. """ ics = ( "BEGIN:VCALENDAR\r\n" "BEGIN:VEVENT\r\n" "UID:1\r\n" "DTSTART:20240101T000000Z\r\n" "DURATION:P999999999999999999W\r\n" "END:VEVENT\r\n" "END:VCALENDAR\r\n" ) calendar = Calendar.from_ical(ics) event = calendar.subcomponents[0] assert any(name == "DURATION" for name, _ in event.errors) def test_invalid_duration_calendar_keeps_original_value(calendars): """A calendar with an impractical duration keeps the raw value on to_ical. The value cannot be decoded to a :class:`datetime.timedelta`, so it is kept as a broken value. Serializing the calendar again must reproduce the original ``DURATION`` text unchanged rather than dropping it. Runs against both ``Calendar`` and ``LazyCalendar`` via the ``calendars`` fixture. """ calendar = calendars.invalid_duration event = calendar.walk("VEVENT")[0] assert any(name == "DURATION" for name, _ in event.errors) assert b"DURATION:P999999999999999999W" in calendar.to_ical() def test_valid_durations_still_parse(): """The guard must not affect durations within range.""" from datetime import timedelta assert vDuration.from_ical("P15DT5H0M20S") == timedelta(days=15, seconds=18020) assert vDuration.from_ical("P7W") == timedelta(days=49) assert vDuration.from_ical("-P14D") == timedelta(-14) collective-icalendar-138c845/src/icalendar/tests/prop/test_identity_and_equality.py000066400000000000000000000037571524134033600307150ustar00rootroot00000000000000"""Test the identity and equality between properties.""" from datetime import date, datetime, time from zoneinfo import ZoneInfo from icalendar import vDDDTypes try: import pytz except ImportError: pytz = None from copy import deepcopy import pytest from dateutil import tz v_ddd_types_list = [ vDDDTypes( datetime( year=2022, month=7, day=22, hour=12, minute=7, tzinfo=ZoneInfo("Europe/London"), ) ), vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7)), vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7, tzinfo=tz.UTC)), vDDDTypes(date(year=2022, month=7, day=22)), vDDDTypes(date(year=2022, month=7, day=23)), vDDDTypes(time(hour=22, minute=7, second=2)), ] if pytz: v_ddd_types_list.append( vDDDTypes( pytz.timezone("EST").localize( datetime(year=2022, month=7, day=22, hour=12, minute=7) ) ), ) def identity(x): return x @pytest.mark.parametrize( "transform", [ deepcopy, identity, hash, ], ) @pytest.mark.parametrize("v_type", v_ddd_types_list) @pytest.mark.parametrize("other", v_ddd_types_list) def test_vDDDTypes_equivalance(transform, v_type, other): if v_type is other: assert transform(v_type) == transform(other), ( f"identity implies equality: {transform.__name__}()" ) assert transform(v_type) == transform(other), ( f"identity implies equality: {transform.__name__}()" ) else: assert transform(v_type) != transform(other), ( f"expected inequality: {transform.__name__}()" ) assert transform(v_type) != transform(other), ( f"expected inequality: {transform.__name__}()" ) @pytest.mark.parametrize("v_type", v_ddd_types_list) def test_inequality_with_different_types(v_type): assert v_type != 42 assert v_type != "test" collective-icalendar-138c845/src/icalendar/tests/prop/test_property_values.py000066400000000000000000000012231524134033600275520ustar00rootroot00000000000000"""Test that composed values are properly converted.""" from datetime import datetime from icalendar.cal.event import Event def test_vDDDLists_timezone(tzp): """Test vDDDLists with timezone information.""" vevent = Event() dt1 = tzp.localize(datetime(2013, 1, 1), "Europe/Vienna") dt2 = tzp.localize(datetime(2013, 1, 2), "Europe/Vienna") dt3 = tzp.localize(datetime(2013, 1, 3), "Europe/Vienna") vevent.add("rdate", [dt1, dt2]) vevent.add("exdate", dt3) ical = vevent.to_ical() assert b"RDATE;TZID=Europe/Vienna:20130101T000000,20130102T000000" in ical assert b"EXDATE;TZID=Europe/Vienna:20130103T000000" in ical collective-icalendar-138c845/src/icalendar/tests/prop/test_trailing_newline.py000066400000000000000000000025301524134033600276430ustar00rootroot00000000000000r"""A trailing line break must not be accepted by the value validation regexes. ``$`` matches just before a final ``\n``, so a value with a trailing line break was accepted by ``DURATION_REGEX``, ``TIME_JCAL_REGEX`` and ``UTC_OFFSET_JCAL_REGEX``. The regexes are now anchored with ``\Z``. """ import pytest from icalendar.error import InvalidCalendar, JCalParsingError from icalendar.prop import vDuration, vTime, vUTCOffset @pytest.mark.parametrize("value", ["P1D\n", "P7W\n", "PT1H0M22S\n"]) def test_vDuration_from_ical_rejects_trailing_newline(value): with pytest.raises(InvalidCalendar): vDuration.from_ical(value) @pytest.mark.parametrize("value", ["12:00:00\n", "07:00:00Z\n"]) def test_vTime_parse_jcal_value_rejects_trailing_newline(value): with pytest.raises(JCalParsingError): vTime.parse_jcal_value(value) @pytest.mark.parametrize("value", ["+01:00\n", "-05:00\n", "+02:30:40\n"]) def test_vUTCOffset_from_jcal_rejects_trailing_newline(value): with pytest.raises(JCalParsingError): vUTCOffset.from_jcal(["tzoffsetto", {}, "utc-offset", value]) def test_valid_values_still_parse(): """The anchor change must not reject otherwise valid values.""" assert vDuration.from_ical("P1D") assert vTime.parse_jcal_value("12:00:00") assert vUTCOffset.from_jcal(["tzoffsetto", {}, "utc-offset", "+01:00"]) collective-icalendar-138c845/src/icalendar/tests/prop/test_unit.py000066400000000000000000000406531524134033600253000ustar00rootroot00000000000000import unittest from datetime import date, datetime, time, timedelta import pytest from icalendar.parser import Parameters class TestProp(unittest.TestCase): def test_prop_vFloat(self): from icalendar.prop import vFloat assert vFloat(1.0).to_ical() == b"1.0" assert vFloat.from_ical("42") == 42.0 assert vFloat(42).to_ical() == b"42.0" assert vFloat(1.2).ical_value == 1.2 self.assertRaises(ValueError, vFloat.from_ical, "1s3") def test_prop_vInt(self): from icalendar.prop import vInt assert vInt(42).to_ical() == b"42" assert vInt.from_ical("13") == 13 self.assertRaises(ValueError, vInt.from_ical, "1s3") def test_prop_vDDDLists(self): from icalendar.prop import vDDDLists dt_list = vDDDLists.from_ical("19960402T010000Z") assert isinstance(dt_list, list) assert len(dt_list) == 1 assert isinstance(dt_list[0], datetime) assert str(dt_list[0]) == "1996-04-02 01:00:00+00:00" p = "19960402T010000Z,19960403T010000Z,19960404T010000Z" dt_list = vDDDLists.from_ical(p) assert len(dt_list) == 3 assert str(dt_list[0]) == "1996-04-02 01:00:00+00:00" assert str(dt_list[2]) == "1996-04-04 01:00:00+00:00" dt_list = vDDDLists([]) assert dt_list.to_ical() == b"" dt_list = vDDDLists([datetime(2000, 1, 1)]) assert dt_list.to_ical() == b"20000101T000000" dt_list = vDDDLists([datetime(2000, 1, 1), datetime(2000, 11, 11)]) assert dt_list.to_ical() == b"20000101T000000,20001111T000000" instance = vDDDLists([]) assert instance != "value" def test_prop_vDate(self): from icalendar.prop import vDate assert vDate(date(2001, 1, 1)).to_ical() == b"20010101" assert vDate(date(1899, 1, 1)).to_ical() == b"18990101" assert vDate.from_ical("20010102") == date(2001, 1, 2) self.assertRaises(TypeError, vDate, "d") self.assertRaises(ValueError, vDate.from_ical, "200102") # trailing data and int() quirks must not be silently accepted self.assertRaises(ValueError, vDate.from_ical, "20010102T") self.assertRaises(ValueError, vDate.from_ical, "2001 102") self.assertRaises(ValueError, vDate.from_ical, "2001_102") self.assertRaises(ValueError, vDate.from_ical, "+0010102") def test_prop_vDuration(self): from icalendar.prop import vDuration assert vDuration(timedelta(11)).to_ical() == b"P11D" assert vDuration(timedelta(-14)).to_ical() == b"-P14D" assert vDuration(timedelta(1, 7384)).to_ical() == b"P1DT2H3M4S" assert vDuration(timedelta(1, 7380)).to_ical() == b"P1DT2H3M" assert vDuration(timedelta(1, 7200)).to_ical() == b"P1DT2H" assert vDuration(timedelta(0, 7200)).to_ical() == b"PT2H" assert vDuration(timedelta(0, 7384)).to_ical() == b"PT2H3M4S" assert vDuration(timedelta(0, 184)).to_ical() == b"PT3M4S" assert vDuration(timedelta(0, 22)).to_ical() == b"PT22S" assert vDuration(timedelta(0, 3622)).to_ical() == b"PT1H0M22S" assert vDuration(timedelta(days=1, hours=5)).to_ical() == b"P1DT5H" assert vDuration(timedelta(hours=-5)).to_ical() == b"-PT5H" assert vDuration(timedelta(days=-1, hours=-5)).to_ical() == b"-P1DT5H" # How does the parsing work? assert vDuration.from_ical("PT1H0M22S") == timedelta(0, 3622) self.assertRaises(ValueError, vDuration.from_ical, "kox") assert vDuration.from_ical("-P14D") == timedelta(-14) self.assertRaises(TypeError, vDuration, 11) # calling to_ical twice should result in same output duration = vDuration(timedelta(days=-1, hours=-5)) assert duration.to_ical() == b"-P1DT5H" assert duration.to_ical() == b"-P1DT5H" def test_prop_vWeekday(self): from icalendar.prop import vWeekday assert vWeekday("mo").to_ical() == b"MO" self.assertRaises(ValueError, vWeekday, "erwer") assert vWeekday.from_ical("mo") == "MO" assert vWeekday.from_ical("+3mo") == "+3MO" self.assertRaises(ValueError, vWeekday.from_ical, "Saturday") assert vWeekday("+mo").to_ical() == b"+MO" assert vWeekday("+3mo").to_ical() == b"+3MO" assert vWeekday("-tu").to_ical() == b"-TU" def test_prop_vFrequency(self): from icalendar.prop import vFrequency self.assertRaises(ValueError, vFrequency, "bad test") assert vFrequency("daily").to_ical() == b"DAILY" assert vFrequency("daily").from_ical("MONTHLY") == "MONTHLY" self.assertRaises(ValueError, vFrequency.from_ical, 234) def test_prop_vRecur(self): from icalendar.prop import vRecur # Let's see how close we can get to one from the rfc: # FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30 r = {"freq": "yearly", "interval": 2} r.update({"bymonth": 1, "byday": "su", "byhour": [8, 9], "byminute": 30}) assert ( vRecur(r).to_ical() == b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1" ) r = vRecur(FREQ="yearly", INTERVAL=2) r.update( { "BYMONTH": 1, "BYDAY": "su", "BYHOUR": [8, 9], "BYMINUTE": 30, } ) assert ( r.to_ical() == b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1" ) r = vRecur(freq="DAILY", count=10) r["bysecond"] = [0, 15, 30, 45] assert r.to_ical() == b"FREQ=DAILY;COUNT=10;BYSECOND=0,15,30,45" r = vRecur(freq="DAILY", until=datetime(2005, 1, 1, 12, 0, 0)) assert r.to_ical() == b"FREQ=DAILY;UNTIL=20050101T120000" # How do we fare with regards to parsing? r = vRecur.from_ical("FREQ=DAILY;INTERVAL=2;COUNT=10") assert r == {"COUNT": [10], "FREQ": ["DAILY"], "INTERVAL": [2]} assert vRecur(r).to_ical() == b"FREQ=DAILY;COUNT=10;INTERVAL=2" r = vRecur.from_ical( "FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;BYHOUR=8,9;BYMINUTE=30" ) assert r == { "BYHOUR": [8, 9], "BYDAY": ["-SU"], "BYMINUTE": [30], "BYMONTH": [1], "FREQ": ["YEARLY"], "INTERVAL": [2], } assert ( vRecur(r).to_ical() == b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;BYMONTH=1" ) r = vRecur.from_ical("FREQ=WEEKLY;INTERVAL=1;BYWEEKDAY=TH") assert r == {"FREQ": ["WEEKLY"], "INTERVAL": [1], "BYWEEKDAY": ["TH"]} assert vRecur(r).to_ical() == b"FREQ=WEEKLY;INTERVAL=1;BYWEEKDAY=TH" # Some examples from the spec r = vRecur.from_ical("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1") assert vRecur(r).to_ical() == b"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1" p = "FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30" r = vRecur.from_ical(p) assert ( vRecur(r).to_ical() == b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1" ) # and some errors self.assertRaises(ValueError, vRecur.from_ical, "BYDAY=12") # when key is not RFC-compliant, parse it as vText r = vRecur.from_ical("FREQ=MONTHLY;BYOTHER=TEXT;BYEASTER=-3") assert vRecur(r).to_ical() == b"FREQ=MONTHLY;BYEASTER=-3;BYOTHER=TEXT" def test_prop_vText(self): from icalendar.parser import Contentline from icalendar.prop import vText assert vText("Simple text").to_ical() == b"Simple text" # Escaped text t = vText("Text ; with escaped, chars") assert t.to_ical() == b"Text \\; with escaped\\, chars" # Escaped newlines assert ( vText("Text with escaped\\N chars").to_ical() == b"Text with escaped\\n chars" ) # If you pass a unicode object, it will be utf-8 encoded. As this is # the (only) standard that RFC 5545 support. t = vText("international chars \xe4\xf6\xfc") assert t.to_ical() == b"international chars \xc3\xa4\xc3\xb6\xc3\xbc" # and parsing? _, _, value = Contentline("SUMMARY:Text \\; with escaped\\, chars").parts() assert vText.from_ical(value) == "Text ; with escaped, chars" _, _, value = Contentline( "SUMMARY:A string with\\; some\\\\ characters in\\it" ).parts() assert vText.from_ical(value) == "A string with; some\\ characters in\\it" # We are forgiving to utf-8 encoding errors: # We intentionally use a string with unexpected encoding # assert vText.from_ical(b"Ol\xe9") == "Ol�" # Notice how accented E character, encoded with latin-1, got replaced # with the official U+FFFD REPLACEMENT CHARACTER. def test_prop_vTime(self): from icalendar.prop import vTime assert vTime(12, 30, 0).to_ical() == "123000" assert vTime.from_ical("123000") == time(12, 30) # We should also fail, right? self.assertRaises(ValueError, vTime.from_ical, "263000") self.assertRaises(ValueError, vTime, "263000") # trailing data and short values must not be silently accepted self.assertRaises(ValueError, vTime.from_ical, "123000GARBAGE") self.assertRaises(ValueError, vTime.from_ical, "12300") self.assertRaises(ValueError, vTime.from_ical, "12 000") # Form #3: local time with a TZID reference still parses assert vTime.from_ical("TZID=America/New_York:083000") == time(8, 30) def test_prop_vUri(self): from icalendar.prop import vUri assert vUri("http://www.example.com/").to_ical() == b"http://www.example.com/" assert vUri.from_ical("http://www.example.com/") == "http://www.example.com/" def test_prop_vGeo(self): from icalendar.prop import vGeo # Pass a list assert vGeo([1.2, 3.0]).to_ical() == "1.2;3.0" # Pass a tuple assert vGeo((1.2, 3.0)).to_ical() == "1.2;3.0" g = vGeo.from_ical("37.386013;-122.082932") assert g == (float("37.386013"), float("-122.082932")) assert vGeo(g).to_ical() == "37.386013;-122.082932" assert hash(vGeo(g)) == hash(g) self.assertRaises(ValueError, vGeo, "g") self.assertRaises(ValueError, vGeo.from_ical, "1s3;1s3") self.assertRaises(ValueError, vGeo.from_ical, "nan;0.0") self.assertRaises(ValueError, vGeo.from_ical, "0.0;inf") self.assertRaises(ValueError, vGeo.from_ical, "1e999;0.0") assert vGeo((37.386013, -122.082932)).ical_value == (37.386013, -122.082932) def test_prop_vUTCOffset(self): from icalendar.prop import vUTCOffset assert vUTCOffset(timedelta(hours=2)).to_ical() == "+0200" assert vUTCOffset(timedelta(hours=-5)).to_ical() == "-0500" assert vUTCOffset(timedelta()).to_ical() == "+0000" assert vUTCOffset(timedelta(minutes=-30)).to_ical() == "-0030" assert vUTCOffset(timedelta(hours=2, minutes=-30)).to_ical() == "+0130" assert vUTCOffset(timedelta(hours=1, minutes=30)).to_ical() == "+0130" # Support seconds assert ( vUTCOffset(timedelta(hours=1, minutes=30, seconds=7)).to_ical() == "+013007" ) # Parsing assert vUTCOffset.from_ical("0000") == timedelta(0) assert vUTCOffset.from_ical("-0030") == timedelta(-1, 84600) assert vUTCOffset.from_ical("+0200") == timedelta(0, 7200) assert vUTCOffset.from_ical("+023040") == timedelta(0, 9040) assert vUTCOffset(vUTCOffset.from_ical("+0230")).to_ical() == "+0230" # And a few failures self.assertRaises(ValueError, vUTCOffset.from_ical, "+323k") self.assertRaises(ValueError, vUTCOffset.from_ical, "+2400") self.assertRaises(TypeError, vUTCOffset, "0:00:00") def test_prop_vInline(self): from icalendar.prop import vInline assert vInline("Some text") == "Some text" assert vInline("Some text").to_ical() == b"Some text" assert vInline.from_ical("Some text") == "Some text" t2 = vInline("other text") t2.params["cn"] = "Test Osterone" assert isinstance(t2.params, Parameters) assert t2.params == {"CN": "Test Osterone"} def test_prop_vCategory(self): from icalendar.prop import vCategory catz = ["cat 1", "cat 2", "cat 3"] v_cat = vCategory(catz) assert v_cat.to_ical() == b"cat 1,cat 2,cat 3" assert vCategory.from_ical(v_cat.to_ical()) == catz c = vCategory(vCategory.from_ical("APPOINTMENT,EDUCATION")) cats = list(c) assert cats == ["APPOINTMENT", "EDUCATION"] def test_prop_TypesFactory(self): from icalendar.prop import TypesFactory # To get a type you can use it like this. factory = TypesFactory() datetime_parser = factory["date-time"] assert datetime_parser(datetime(2001, 1, 1)).to_ical() == b"20010101T000000" # A typical use is when the parser tries to find a content type and use # text as the default value = "20050101T123000" value_type = "date-time" assert factory.get(value_type, "text").from_ical(value) == datetime( 2005, 1, 1, 12, 30 ) # It can also be used to directly encode property and parameter values assert ( factory.to_ical("comment", "by Rasmussen, Max Müller") == b"by Rasmussen\\, Max M\xc3\xbcller" ) assert factory.to_ical("priority", 1) == b"1" assert ( factory.to_ical("cn", "Rasmussen, Max Müller") == b"Rasmussen\\, Max M\xc3\xbcller" ) assert ( factory.from_ical("cn", b"Rasmussen\\, Max M\xc3\xb8ller") == "Rasmussen\\, Max Møller" ) # Property name -> its RFC default value type (a jCal type identifier). # Contributed by @lcampanella98. RFC_DEFAULT_VALUE_TYPES = { # DATE-TIME "DTSTART": "date-time", "DTEND": "date-time", "DUE": "date-time", "COMPLETED": "date-time", "DTSTAMP": "date-time", "CREATED": "date-time", "LAST-MODIFIED": "date-time", "RECURRENCE-ID": "date-time", "RDATE": "date-time", "EXDATE": "date-time", "ACKNOWLEDGED": "date-time", # DURATION -- note TRIGGER's default is DURATION, not DATE-TIME "DURATION": "duration", "TRIGGER": "duration", "REFRESH-INTERVAL": "duration", # PERIOD "FREEBUSY": "period", # INTEGER "PERCENT-COMPLETE": "integer", "PRIORITY": "integer", "REPEAT": "integer", "SEQUENCE": "integer", # TEXT "SUMMARY": "text", "DESCRIPTION": "text", "COMMENT": "text", "LOCATION": "text", "UID": "text", "RELATED-TO": "text", "TZID": "text", "TZNAME": "text", "STATUS": "text", "ACTION": "text", "TRANSP": "text", "CLASS": "text", "METHOD": "text", "VERSION": "text", "PRODID": "text", "CALSCALE": "text", "CATEGORIES": "text", "CONTACT": "text", "RESOURCES": "text", "REQUEST-STATUS": "text", # URI "ATTACH": "uri", "URL": "uri", "TZURL": "uri", "SOURCE": "uri", "CONFERENCE": "uri", # UTC-OFFSET "TZOFFSETFROM": "utc-offset", "TZOFFSETTO": "utc-offset", # CAL-ADDRESS "ATTENDEE": "cal-address", "ORGANIZER": "cal-address", # RECUR "RRULE": "recur", "EXRULE": "recur", # FLOAT "GEO": "float", # UNKNOWN -- no default value type (IMAGE requires an explicit VALUE; # X- properties are unrecognized). "IMAGE": "unknown", "X-WR-CALNAME": "unknown", "X-APPLE-STRUCTURED-LOCATION": "unknown", } @pytest.mark.parametrize(("name", "expected"), sorted(RFC_DEFAULT_VALUE_TYPES.items())) def test_default_value_type_matches_rfc(name, expected): """default_value_type returns the RFC default value type for a property. The expected types in ``RFC_DEFAULT_VALUE_TYPES`` are written by hand from the RFCs, so this verifies ``TypesFactory.default_value_type()`` independently of ``Component.from_jcal`` -- the production code that also relies on it (RFC 7265 section 3.5.1) to decide whether a property needs a VALUE parameter. """ from icalendar.prop import TypesFactory factory = TypesFactory() assert factory.default_value_type(name) == expected # Property names are case-insensitive. assert factory.default_value_type(name.lower()) == expected collective-icalendar-138c845/src/icalendar/tests/prop/test_vBinary.py000066400000000000000000000142421524134033600257260ustar00rootroot00000000000000"""Test vBinary""" import base64 import pytest from icalendar import Calendar, vBinary from icalendar.parser import Parameters def test_text(): txt = b"This is gibberish" txt_ical = b"VGhpcyBpcyBnaWJiZXJpc2g=" assert vBinary(txt).to_ical() == txt_ical assert vBinary.from_ical(txt_ical) == txt def test_binary(): txt = b"Binary data \x13 \x56" txt_ical = b"QmluYXJ5IGRhdGEgEyBW" assert vBinary(txt).to_ical() == txt_ical assert vBinary.from_ical(txt_ical) == txt def test_param(): assert isinstance(vBinary("txt").params, Parameters) assert vBinary("txt").params == {"VALUE": "BINARY", "ENCODING": "BASE64"} def test_long_data(): """Long data should not have line breaks, as that would interfere""" txt = b"a" * 99 txt_ical = b"YWFh" * 33 assert vBinary(txt).to_ical() == txt_ical assert vBinary.from_ical(txt_ical) == txt def test_repr(): instance = vBinary("value") assert repr(instance) == "vBinary(b'dmFsdWU=')" def test_from_ical(): with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."): vBinary.from_ical("value") with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."): vBinary.from_ical("áèਮ") @pytest.mark.parametrize( "value", [ "!!!!dGV4dA==@@@@", "dG V4dA==", "dGV4dA==#", ], ) def test_from_ical_rejects_non_base64_characters(value): with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."): vBinary.from_ical(value) def test_ical_value(): """ical_value property returns the binary value.""" raw_data = b"magic string" assert vBinary(raw_data).ical_value == raw_data def test_ical_value_returns_raw_bytes_not_decoded(): """Test that ``ical_value`` returns the raw stored bytes. With the release of icalendar 7.1.0, and previous to PR #1356, ``ical_value`` Base64-decoded the raw stored bytes. For example, ``vBinary("SGVsbG8=").ical_value`` was decoded to ``b"Hello"`` and raised ``ValueError`` for non-Base64 input. """ assert vBinary(b"SGVsbG8=").ical_value == b"SGVsbG8=" # Non-base64 input no longer raises; it is just stored and returned as-is. assert vBinary(b"!!!!dGV4dA==@@@@").ical_value == b"!!!!dGV4dA==@@@@" def test_bytes_holds_raw_lossless_data(): """The .bytes attribute exposes the raw value, including non-UTF-8 data. See PR #1356. """ raw = bytes(range(256)) binary = vBinary(raw) assert binary.bytes == raw # round-trips losslessly through the wire format assert vBinary.from_ical(binary.to_ical()) == raw def test_obj_is_deprecated_string_view(): """.obj is kept for backward compatibility but deprecated in favour of .bytes. See PR #1356. """ with pytest.warns(DeprecationWarning, match="obj is deprecated"): assert vBinary(b"txt").obj == "txt" def test_obj_setter_updates_bytes(): """Setting .obj still works for backward compatibility and writes .bytes. See PR #1356. """ binary = vBinary(b"old") with pytest.warns(DeprecationWarning, match="obj is deprecated"): binary.obj = "new" assert binary.bytes == b"new" def test_hash(): obj = vBinary(b"hashed text") assert hash(obj) == hash(b"hashed text") def test_base64data_getter(): """base64data returns the same string as to_ical(), decoded to str.""" obj = vBinary(b"This is gibberish") assert obj.base64data == "VGhpcyBpcyBnaWJiZXJpc2g=" assert obj.base64data == obj.to_ical().decode("ascii") def test_base64data_getter_empty(): """base64data of an empty value is an empty string.""" assert vBinary(b"").base64data == "" def test_base64data_setter(): """Setting base64data updates the underlying value.""" obj = vBinary(b"initial value") obj.base64data = "QmluYXJ5IGRhdGEgEyBW" assert obj.to_ical() == b"QmluYXJ5IGRhdGEgEyBW" assert obj.from_ical(obj.to_ical()) == b"Binary data \x13 \x56" def test_base64data_roundtrip(): """Reading base64data after setting it returns the same string.""" obj = vBinary(b"initial value") obj.base64data = "QmluYXJ5IGRhdGEgEyBW" assert obj.base64data == "QmluYXJ5IGRhdGEgEyBW" @pytest.mark.parametrize( "value", [ "value", "áèਮ", "!!!!dGV4dA==@@@@", "dG V4dA==", "dGV4dA==#", ], ) def test_base64data_setter_rejects_invalid_base64(value): """The setter raises ValueError, matching from_ical's error, for bad input.""" obj = vBinary(b"unchanged") with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."): obj.base64data = value # a rejected assignment must not mutate the existing value assert obj.to_ical() == vBinary(b"unchanged").to_ical() def test_base64data_setter_rejects_non_str(): """Non-str input raises TypeError, matching base64.b64decode's behaviour.""" obj = vBinary(b"unchanged") with pytest.raises(TypeError): obj.base64data = 12345 def test_base64data_setter_stores_raw_bytes(): """The setter writes the decoded bytes to .bytes, so non-UTF-8 data round-trips. See PR #1356. """ raw = bytes(range(256)) encoded = base64.b64encode(raw).decode("ascii") obj = vBinary(b"") obj.base64data = encoded assert obj.bytes == raw assert obj.base64data == encoded def test_attach_example_preserves_binary_data(calendars): """The example calendar's PNG attachment round-trips without corruption. Regression test for the corruption fixed in #1356. See #1549. """ calendar = calendars.issue_1549_binary_attachment (event,) = calendar.events data = event.decoded("ATTACH") # the whole payload survives, not just the leading magic number assert data[:8] == b"\x89PNG\r\n\x1a\n" assert data[-8:] == b"IEND\xaeB`\x82" assert len(data) == 128 def test_attach_example_survives_reserialization(calendars): """The PNG attachment is byte-identical after to_ical()/from_ical(). See #1549. """ calendar = calendars.issue_1549_binary_attachment reparsed = Calendar.from_ical(calendar.to_ical()) (original,) = calendar.events (roundtripped,) = reparsed.events assert roundtripped.decoded("ATTACH") == original.decoded("ATTACH") collective-icalendar-138c845/src/icalendar/tests/prop/test_vBoolean.py000066400000000000000000000011251524134033600260550ustar00rootroot00000000000000import pytest from icalendar.prop import vBoolean def test_true(): assert vBoolean(True).to_ical() == b"TRUE" def test_false(): assert vBoolean(0).to_ical() == b"FALSE" def test_roundtrip(): assert vBoolean.from_ical(vBoolean(True).to_ical()) assert vBoolean.from_ical("true") def test_error(): """Error: key not exists""" with pytest.raises(ValueError): vBoolean.from_ical("ture") def test_ical_value(): """ical_value property returns the boolean value.""" assert vBoolean(True).ical_value is True assert vBoolean(False).ical_value is False collective-icalendar-138c845/src/icalendar/tests/prop/test_vCalAddress.py000066400000000000000000000026141524134033600265070ustar00rootroot00000000000000from icalendar.parser import Parameters from icalendar.prop import vCalAddress txt = b"MAILTO:maxm@mxm.dk" a = vCalAddress(txt) a.params["cn"] = "Max M" def test_to_ical(): assert a.to_ical() == txt def test_params(): assert isinstance(a.params, Parameters) assert a.params == {"CN": "Max M"} def test_from_ical(): assert vCalAddress.from_ical(txt) == "MAILTO:maxm@mxm.dk" def test_repr(): instance = vCalAddress("value") assert repr(instance) == "vCalAddress('value')" def test_email_malformed(): """Sometimes, people forget to add mailto that.""" address = vCalAddress("me@you.we") assert address.email == "me@you.we" def test_email_mailto(): """Email with a normal mailto link.""" address = vCalAddress("mailto:icalendar@email.list") assert address.email == "icalendar@email.list" def test_capital_email(): """mailto can be capital letters.""" address = vCalAddress("MAILTO:yemaya@posteo.net") assert address.email == "yemaya@posteo.net" def test_name(): """We want the name, too!""" address = vCalAddress("MAILTO:yemaya@posteo.net") assert address.name == "" address.params["CN"] = "name!" assert address.name == "name!" def test_set_the_name(): address = vCalAddress("MAILTO:yemaya@posteo.net") address.name = "Yemaya :)" assert address.name == "Yemaya :)" assert address.params["CN"] == "Yemaya :)" collective-icalendar-138c845/src/icalendar/tests/prop/test_vDDDTypes.py000066400000000000000000000017341524134033600261240ustar00rootroot00000000000000from datetime import date, datetime, time, timedelta import pytest from icalendar.prop import vDDDTypes def test_instance(): assert isinstance(vDDDTypes.from_ical("20010101T123000"), datetime) assert isinstance(vDDDTypes.from_ical("20010101"), date) def test_datetime_with_timezone(tzp): assert vDDDTypes.from_ical("20010101T123000Z") == tzp.localize_utc( datetime(2001, 1, 1, 12, 30) ) def test_timedelta(): assert vDDDTypes.from_ical("P31D") == timedelta(31) assert vDDDTypes.from_ical("-P31D") == timedelta(-31) def test_bad_input(): with pytest.raises(TypeError): vDDDTypes(42) def test_time_from_string(): assert vDDDTypes.from_ical("123000") == time(12, 30) assert isinstance(vDDDTypes.from_ical("123000"), time) def test_invalid_period_to_ical(): invalid_period = (datetime(2000, 1, 1), datetime(2000, 1, 2), datetime(2000, 1, 2)) with pytest.raises(ValueError): vDDDTypes(invalid_period).to_ical() collective-icalendar-138c845/src/icalendar/tests/prop/test_vDatetime.py000066400000000000000000000036711524134033600262420ustar00rootroot00000000000000from datetime import datetime import pytest from icalendar.prop import vDatetime def test_to_ical(): assert vDatetime(datetime(2001, 1, 1, 12, 30, 0)).to_ical() == b"20010101T123000" def test_from_ical(): assert vDatetime.from_ical("20000101T120000") == datetime(2000, 1, 1, 12, 0) assert vDatetime.from_ical("20010101T000000") == datetime(2001, 1, 1, 0, 0) def test_to_ical_utc(tzp): dutc = tzp.localize_utc(datetime(2001, 1, 1, 12, 30, 0)) assert vDatetime(dutc).to_ical() == b"20010101T123000Z" def test_to_ical_utc_1899(tzp): dutc = tzp.localize_utc(datetime(1899, 1, 1, 12, 30, 0)) assert vDatetime(dutc).to_ical() == b"18990101T123000Z" def test_bad_ical(): with pytest.raises(ValueError): vDatetime.from_ical("20010101T000000A") def test_from_ical_form_3(): # TZID=America/New_York:19980119T020000 dt = vDatetime.from_ical("TZID=America/New_York:19980119T020000") assert dt == datetime(1998, 1, 19, 2, 0, 0) def test_from_ical_no_t(): with pytest.raises(ValueError, match="Wrong datetime format: 20010101-000000"): vDatetime.from_ical("20010101-000000") def test_from_ical_wrong_t_position(): # T is at index 9 instead of 8 with pytest.raises(ValueError, match="Wrong datetime format: 200101011T00000"): vDatetime.from_ical("200101011T00000") def test_roundtrip(): utc = vDatetime.from_ical("20010101T000000Z") assert vDatetime(utc).to_ical() == b"20010101T000000Z" def test_transition(tzp): # 1 minute before transition to DST dat = vDatetime.from_ical("20120311T015959", "America/Denver") assert dat.strftime("%Y%m%d%H%M%S %z") == "20120311015959 -0700" # After transition to DST dat = vDatetime.from_ical("20120311T030000", "America/Denver") assert dat.strftime("%Y%m%d%H%M%S %z") == "20120311030000 -0600" dat = vDatetime.from_ical("20101010T000000", "Europe/Vienna") assert vDatetime(dat).to_ical() == b"20101010T000000" collective-icalendar-138c845/src/icalendar/tests/prop/test_vFloat.py000066400000000000000000000014331524134033600255450ustar00rootroot00000000000000import pytest from icalendar.prop.float import vFloat def test_zero(): assert vFloat(0.0).to_ical() == b"0.0" def test_roundtrip(): assert vFloat.from_ical(vFloat("42.0").to_ical()) == 42.0 assert vFloat.from_ical("42.0") == 42.0 def test_error(): """Error: not an float string""" with pytest.raises(ValueError): vFloat.from_ical("not a float") @pytest.mark.parametrize("value", ["nan", "inf", "-inf", "infinity", "1e999"]) def test_reject_non_finite(value): """Non-finite floats are not valid FLOAT values per RFC 5545.""" with pytest.raises(ValueError): vFloat.from_ical(value) def test_ical_value(): """ical_value property returns the float value.""" assert vFloat(1.0).ical_value == 1.0 assert vFloat(0.0).ical_value == 0.0 collective-icalendar-138c845/src/icalendar/tests/prop/test_vFrequency.py000066400000000000000000000021121524134033600264340ustar00rootroot00000000000000import pytest from icalendar.prop import vFrequency def test_valid_frequencies(): """vFrequency accepts every RFC 5545 frequency value.""" for freq in ( "SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY", ): assert vFrequency(freq).to_ical() == freq.encode("utf-8") def test_case_insensitive(): """vFrequency accepts lowercase input and uppercases it on serialization.""" assert vFrequency.from_ical("weekly").to_ical() == b"WEEKLY" def test_roundtrip(): assert vFrequency.from_ical(vFrequency("DAILY").to_ical()) == "DAILY" def test_error(): """Error: Expected frequency, got: BOGUS""" with pytest.raises(ValueError): vFrequency.from_ical("BOGUS") def test_ical_value(): """ical_value property returns the frequency string value.""" assert vFrequency("WEEKLY").ical_value == "WEEKLY" assert vFrequency("DAILY").ical_value == "DAILY" assert vFrequency("YEARLY").ical_value == "YEARLY" assert isinstance(vFrequency("WEEKLY").ical_value, str) collective-icalendar-138c845/src/icalendar/tests/prop/test_vInt.py000066400000000000000000000007731524134033600252400ustar00rootroot00000000000000import pytest from icalendar.prop.integer import vInt def test_zero(): assert vInt(0).to_ical() == b"0" def test_roundtrip(): assert vInt.from_ical(vInt("42").to_ical()) == 42 assert vInt.from_ical("42") == 42 def test_error(): """Error: not an integer string""" with pytest.raises(ValueError): vInt.from_ical("not an int") def test_ical_value(): """ical_value property returns the int value.""" assert vInt(1).ical_value == 1 assert vInt(0).ical_value == 0 collective-icalendar-138c845/src/icalendar/tests/prop/test_vPeriod.py000066400000000000000000000050101524134033600257150ustar00rootroot00000000000000import unittest from datetime import datetime, timedelta import pytest from icalendar.prop import vPeriod class TestProp(unittest.TestCase): def test_one_day(self): # One day in exact datetimes per = (datetime(2000, 1, 1), datetime(2000, 1, 2)) assert vPeriod(per).to_ical() == b"20000101T000000/20000102T000000" per = (datetime(2000, 1, 1), timedelta(days=31)) assert vPeriod(per).to_ical() == b"20000101T000000/P31D" def test_roundtrip(self): p = vPeriod.from_ical("20000101T000000/20000102T000000") assert p == (datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 2, 0, 0)) assert vPeriod(p).to_ical() == b"20000101T000000/20000102T000000" assert vPeriod.from_ical("20000101T000000/P31D") == ( datetime(2000, 1, 1, 0, 0), timedelta(31), ) def test_round_trip_with_absolute_time(self): p = vPeriod.from_ical("20000101T000000Z/20000102T000000Z") assert vPeriod(p).to_ical() == b"20000101T000000Z/20000102T000000Z" def test_bad_input(self): self.assertRaises(ValueError, vPeriod.from_ical, "20000101T000000/Psd31D") def test_timezoned(tzp): start = tzp.localize(datetime(2000, 1, 1), "Europe/Copenhagen") end = tzp.localize(datetime(2000, 1, 2), "Europe/Copenhagen") per = (start, end) assert vPeriod(per).to_ical() == b"20000101T000000/20000102T000000" assert vPeriod(per).params["TZID"] == "Europe/Copenhagen" def test_timezoned_with_timedelta(tzp): p = vPeriod( (tzp.localize(datetime(2000, 1, 1), "Europe/Copenhagen"), timedelta(days=31)) ) assert p.to_ical() == b"20000101T000000/P31D" @pytest.mark.parametrize( ("period", "error"), [ (("20000101T000000", datetime(2000, 1, 2)), TypeError), ((datetime(2000, 1, 1), "20000102T000000"), TypeError), ((datetime(2000, 1, 2), datetime(2000, 1, 1)), ValueError), ((datetime(2000, 1, 2), timedelta(-1)), ValueError), ], ) def test_invalid_parameters(period, error): """The parameters are of wrong type or of wrong order.""" with pytest.raises(error): vPeriod(period) def test_ical_value_with_end(): """ical_value property returns the start/end tuple.""" value = (datetime(2000, 1, 1), datetime(2000, 1, 2)) assert vPeriod(value).ical_value == value def test_ical_value_with_duration(): """ical_value property returns the start/duration tuple.""" value = (datetime(2000, 1, 1), timedelta(days=31)) assert vPeriod(value).ical_value == value collective-icalendar-138c845/src/icalendar/tests/prop/test_vWeekday.py000066400000000000000000000041621524134033600260730ustar00rootroot00000000000000import pytest from icalendar.prop import vWeekday def test_simple(): weekday = vWeekday("SU") assert weekday.to_ical() == b"SU" assert weekday.weekday == "SU" assert weekday.relative is None def test_relative(): weekday = vWeekday("-1MO") assert weekday.to_ical() == b"-1MO" assert weekday.weekday == "MO" assert weekday.relative == -1 def test_roundtrip(): assert vWeekday.from_ical(vWeekday("+2TH").to_ical()) == "+2TH" def test_error(): """Error: Expected weekday abbrevation, got: -100MO""" with pytest.raises(ValueError): vWeekday.from_ical("-100MO") @pytest.mark.parametrize("value", ["١٢MO", "٥SU", "۱۲FR", "١MO"]) def test_non_ascii_ordwk_digits_rejected(value): r"""Non-ASCII digits must not be accepted in the ``ordwk`` (relative) part. Use ``[0-9]`` to explicitly specify that only ASCII digits should be matched, instead of ``\d``. In Python, ``\d`` matches a digit zero through nine in any script except ideographic scripts, and is equivalent to ``\p{Nd}``. A malformed ``ordwk`` would therefore get parsed into a valid ``relative`` number. RFC 5545, section 3.3.10, allows only ASCII characters per its definition of ``DIGIT``. """ with pytest.raises(ValueError): vWeekday(value) with pytest.raises(ValueError): vWeekday.from_ical(value) @pytest.mark.parametrize("value", ["2MO\n", "MO\n", "+2TH\n", "-1SU\n"]) def test_trailing_newline_rejected(value): r"""A trailing line break must not be accepted and carried into the value. ``$`` matches just before a final ``\n``, so the newline survived in the ``str`` value and was re-emitted into RECUR output (content-line injection). """ with pytest.raises(ValueError): vWeekday(value) with pytest.raises(ValueError): vWeekday.from_ical(value) def test_ical_value(): """ical_value property returns the weekday string value.""" assert vWeekday("MO").ical_value == "MO" assert vWeekday("+2TH").ical_value == "+2TH" assert vWeekday("-1SU").ical_value == "-1SU" assert isinstance(vWeekday("MO").ical_value, str) collective-icalendar-138c845/src/icalendar/tests/prop/test_windows_to_olson_mapping.py000066400000000000000000000021611524134033600314320ustar00rootroot00000000000000"""Test the mappings from windows to olson tzids""" from datetime import datetime import pytest from icalendar import vDatetime from icalendar.timezone import tzids_from_tzinfo from icalendar.timezone.windows_to_olson import WINDOWS_TO_OLSON def test_windows_timezone(tzp): """Test that the timezone is mapped correctly to olson.""" dt = vDatetime.from_ical("20170507T181920", "Eastern Standard Time") expected = tzp.localize(datetime(2017, 5, 7, 18, 19, 20), "America/New_York") assert dt.tzinfo == expected.tzinfo assert dt == expected def test_mountain_standard_time_mexico(tzp): """Test the current CLDR mapping for Mountain Standard Time (Mexico).""" dt = vDatetime.from_ical("20260706T110000", "Mountain Standard Time (Mexico)") expected = tzp.localize(datetime(2026, 7, 6, 11), "America/Mazatlan") assert dt.tzinfo == expected.tzinfo assert dt == expected @pytest.mark.parametrize("olson_id", WINDOWS_TO_OLSON.values()) def test_olson_names(tzp, olson_id): """test if all mappings actually map to valid tzids""" assert olson_id in tzids_from_tzinfo(tzp.timezone(olson_id)) collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/000077500000000000000000000000001524134033600243265ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/__init__.py000066400000000000000000000003201524134033600264320ustar00rootroot00000000000000"""Tests for vCard structured properties per RFC 6350. vCard structured properties (ADR, N, ORG) use semicolons as field separators. These semicolons should NOT be escaped, unlike iCalendar TEXT values. """ collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/test_component_integration.py000066400000000000000000000045571524134033600323570ustar00rootroot00000000000000"""Tests for vCard properties within Component parsing.""" from __future__ import annotations from icalendar import Component from icalendar.prop import vAdr, vN, vOrg class TestComponentIntegration: """Test vCard properties work within Component parsing.""" def test_adr_in_component(self): """ADR property works in a Component.""" comp = Component() comp.add("ADR", ("", "", "123 Main St", "City", "State", "12345", "USA")) ical = comp.to_ical() assert b"ADR:;;123 Main St;City;State;12345;USA" in ical def test_n_in_component(self): """N property works in a Component.""" comp = Component() comp.add("N", ("Doe", "John", "M.", "Dr.", "Jr.")) ical = comp.to_ical() assert b"N:Doe;John;M.;Dr.;Jr." in ical def test_org_in_component(self): """ORG property works in a Component.""" comp = Component() comp.add("ORG", ("ABC Inc.", "Marketing")) ical = comp.to_ical() assert b"ORG:ABC Inc.;Marketing" in ical def test_vcard_in_vcalendar(self): """vCard properties can appear in VCALENDAR (non-standard but possible).""" ical_data = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//EN ADR:;;123 Main St;Springfield;IL;62701;USA END:VCALENDAR """ cal = Component.from_ical(ical_data) assert "ADR" in cal adr_value = cal["ADR"] # Should be parsed as vAdr assert isinstance(adr_value, vAdr) assert adr_value.fields.street == "123 Main St" def test_parse_n_from_component(self): """N property is parsed correctly from component.""" ical_data = b"""BEGIN:VCARD VERSION:4.0 N:Doe;John;M.;Dr.;Jr. END:VCARD """ comp = Component.from_ical(ical_data) assert "N" in comp n_value = comp["N"] assert isinstance(n_value, vN) assert n_value.fields.family == "Doe" assert n_value.fields.given == "John" def test_parse_org_from_component(self): """ORG property is parsed correctly from component.""" ical_data = b"""BEGIN:VCARD VERSION:4.0 ORG:ABC Inc.;Marketing;Sales END:VCARD """ comp = Component.from_ical(ical_data) assert "ORG" in comp org_value = comp["ORG"] assert isinstance(org_value, vOrg) assert org_value.name == "ABC Inc." assert org_value.units == ("Marketing", "Sales") collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/test_split_on_unescaped_semicolon.py000066400000000000000000000035211524134033600336660ustar00rootroot00000000000000"""Tests for split_on_unescaped_semicolon function.""" from __future__ import annotations import pytest from icalendar.parser import split_on_unescaped_semicolon @pytest.mark.parametrize( ("input_str", "expected"), [ # Simple cases ("a;b;c", ["a", "b", "c"]), ("single", ["single"]), # Escaped semicolons are not split points (r"a\;b\;c", ["a;b;c"]), # Mixed escaped and unescaped (r"field1\;with;field2", ["field1;with", "field2"]), # vCard ADR example (r"PO Box 123\;Suite 200;City", ["PO Box 123;Suite 200", "City"]), # Empty string returns single empty field ("", [""]), # Empty fields are preserved (";;a;b;;c;", ["", "", "a", "b", "", "c", ""]), # Escaped backslash before semicolon (the backslash is escaped, not the semicolon) (r"field1\\;field2", ["field1\\", "field2"]), # Escaped backslash followed by escaped semicolon (r"field1\\\;still field1;field2", ["field1\\;still field1", "field2"]), # Multiple escaped backslashes (r"a\\\\;b", ["a\\\\", "b"]), # Escaped comma (should not affect semicolon splitting) (r"a\,b;c\,d", ["a,b", "c,d"]), ], ids=[ "simple_split", "no_semicolons", "escaped_semicolons_not_split", "mixed_escaped_and_unescaped", "vcard_adr_example", "empty_string", "empty_fields_preserved", "escaped_backslash_before_semicolon", "escaped_backslash_then_escaped_semicolon", "multiple_escaped_backslashes", "escaped_comma_no_effect", ], ) def test_split_on_unescaped_semicolon(input_str: str, expected: list[str]) -> None: """Test splitting strings on unescaped semicolons.""" result = split_on_unescaped_semicolon(input_str) assert result == expected collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/test_vAdr.py000066400000000000000000000133721524134033600266410ustar00rootroot00000000000000"""Tests for vCard ADR (Address) property per RFC 6350.""" from __future__ import annotations import pytest from icalendar.prop import AdrFields, vAdr class TestVAdrCreation: """Test vAdr creation and field access.""" def test_adr_creation_with_tuple(self): """Create ADR from tuple of 7 fields.""" adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA")) assert adr.fields == AdrFields( po_box="", extended="", street="123 Main St", locality="Springfield", region="IL", postal_code="62701", country="USA", ) def test_adr_creation_with_list(self): """Create ADR from list of 7 fields.""" adr = vAdr(["", "", "123 Main St", "Springfield", "IL", "62701", "USA"]) assert adr.fields.street == "123 Main St" def test_adr_creation_with_string(self): """Create ADR from vCard format string.""" adr = vAdr(";;123 Main St;Springfield;IL;62701;USA") assert adr.fields.street == "123 Main St" assert adr.fields.locality == "Springfield" def test_adr_creation_with_adrfields(self): """Create ADR from AdrFields named tuple.""" fields = AdrFields("", "", "123 Main St", "Springfield", "IL", "62701", "USA") adr = vAdr(fields) assert adr.fields is fields def test_adr_requires_7_fields(self): """ADR must have exactly 7 fields.""" with pytest.raises(ValueError, match="exactly 7 fields"): vAdr(("too", "few")) def test_adr_named_field_access(self): """Access ADR fields by name.""" adr = vAdr( ("PO Box 1", "Suite 200", "123 Main St", "City", "State", "12345", "USA") ) assert adr.fields.po_box == "PO Box 1" assert adr.fields.extended == "Suite 200" assert adr.fields.street == "123 Main St" assert adr.fields.locality == "City" assert adr.fields.region == "State" assert adr.fields.postal_code == "12345" assert adr.fields.country == "USA" class TestVAdrSerialization: """Test vAdr to_ical and from_ical.""" def test_adr_to_ical(self): """ADR to_ical generates semicolon-separated format.""" adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA")) result = adr.to_ical() assert result == b";;123 Main St;Springfield;IL;62701;USA" def test_adr_from_ical(self): """Parse ADR from vCard format.""" result = vAdr.from_ical(";;123 Main St;Springfield;IL;62701;USA") assert isinstance(result, AdrFields) assert result == AdrFields( "", "", "123 Main St", "Springfield", "IL", "62701", "USA" ) def test_adr_round_trip(self): """ADR survives round-trip through to_ical and from_ical.""" original = AdrFields("", "", "123 Main St", "Springfield", "IL", "62701", "USA") adr = vAdr(original) ical = adr.to_ical() parsed = vAdr.from_ical(ical) assert parsed == original class TestVAdrEscaping: """Test vAdr escaping behavior.""" def test_adr_semicolons_not_escaped_as_separators(self): """Semicolons in ADR are field separators, not escaped.""" adr = vAdr( ("PO Box 1", "Suite 200", "123 Main St", "City", "State", "12345", "USA") ) ical = adr.to_ical() # Semicolons between fields should NOT be escaped assert b"\\;" not in ical # No escaped semicolons assert ical.count(b";") == 6 # 6 semicolons for 7 fields def test_adr_commas_in_field_are_escaped(self): """Commas within ADR fields ARE escaped.""" adr = vAdr(("", "", "123 Main St, Apt 4", "Springfield", "IL", "62701", "USA")) ical = adr.to_ical() assert b"\\," in ical def test_adr_semicolons_in_field_are_escaped(self): """Semicolons within ADR field values ARE escaped.""" adr = vAdr(("", "", "123 Main St; Apt 4", "Springfield", "IL", "62701", "USA")) ical = adr.to_ical() assert b"\\;" in ical # 6 separators + 1 escaped within field = 7 total semicolons assert ical.count(b";") == 7 # Round-trip preserves the semicolon in the field value parsed = vAdr.from_ical(ical) assert parsed.street == "123 Main St; Apt 4" def test_adr_empty_fields_preserved(self): """Empty fields are preserved in ADR.""" adr = vAdr(("", "", "123 Main St", "City", "", "12345", "USA")) ical = adr.to_ical() parsed = vAdr.from_ical(ical) assert parsed.po_box == "" assert parsed.extended == "" assert parsed.region == "" class TestVAdrIcalValue: """Test vAdr ical_value property.""" def test_ical_value_returns_fields(self): """ical_value property returns the AdrFields.""" adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA")) assert adr.ical_value == adr.fields assert isinstance(adr.ical_value, AdrFields) class TestVAdrIssue137: """Test the exact example from Issue #137.""" def test_issue_137_example(self): """Parse and round-trip the ADR example from Issue #137.""" adr_value = ";2822 Email HQ;Suite 2821;RFCVille;PA;1521$;USA" parsed = vAdr.from_ical(adr_value) assert len(parsed) == 7 assert parsed.po_box == "" assert parsed.extended == "2822 Email HQ" assert parsed.street == "Suite 2821" assert parsed.locality == "RFCVille" assert parsed.region == "PA" assert parsed.postal_code == "1521$" assert parsed.country == "USA" # Round-trip should preserve structure adr = vAdr(parsed) regenerated = adr.to_ical().decode("utf-8") assert regenerated == adr_value collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/test_vN.py000066400000000000000000000074121524134033600263260ustar00rootroot00000000000000"""Tests for vCard N (Name) property per RFC 6350.""" from __future__ import annotations import pytest from icalendar.prop import vN from icalendar.prop.n import NFields class TestVNCreation: """Test vN creation and field access.""" def test_n_creation_with_tuple(self): """Create N from tuple of 5 fields.""" n = vN(("Doe", "John", "M.", "Dr.", "Jr.")) assert n.fields == NFields( family="Doe", given="John", additional="M.", prefix="Dr.", suffix="Jr.", ) def test_n_creation_with_list(self): """Create N from list of 5 fields.""" n = vN(["Doe", "John", "M.", "Dr.", "Jr."]) assert n.fields.family == "Doe" def test_n_creation_with_string(self): """Create N from vCard format string.""" n = vN("Doe;John;M.;Dr.;Jr.") assert n.fields.family == "Doe" assert n.fields.given == "John" def test_n_creation_with_nfields(self): """Create N from NFields named tuple.""" fields = NFields("Doe", "John", "M.", "Dr.", "Jr.") n = vN(fields) assert n.fields is fields def test_n_requires_5_fields(self): """N must have exactly 5 fields.""" with pytest.raises(ValueError, match="exactly 5 fields"): vN(("too", "few")) def test_n_named_field_access(self): """Access N fields by name.""" n = vN(("Doe", "John", "Michael", "Dr.", "Jr.")) assert n.fields.family == "Doe" assert n.fields.given == "John" assert n.fields.additional == "Michael" assert n.fields.prefix == "Dr." assert n.fields.suffix == "Jr." class TestVNSerialization: """Test vN to_ical and from_ical.""" def test_n_to_ical(self): """N to_ical generates semicolon-separated format.""" n = vN(("Doe", "John", "M.", "Dr.", "Jr.")) result = n.to_ical() assert result == b"Doe;John;M.;Dr.;Jr." def test_n_from_ical(self): """Parse N from vCard format.""" result = vN.from_ical("Doe;John;M.;Dr.;Jr.") assert isinstance(result, NFields) assert result == NFields("Doe", "John", "M.", "Dr.", "Jr.") def test_n_round_trip(self): """N survives round-trip through to_ical and from_ical.""" original = NFields("Doe", "John", "Michael", "Dr.", "Jr.") n = vN(original) ical = n.to_ical() parsed = vN.from_ical(ical) assert parsed == original class TestVNEscaping: """Test vN escaping behavior.""" def test_n_semicolons_not_escaped_as_separators(self): """Semicolons in N are field separators, not escaped.""" n = vN(("Doe", "John", "M.", "Dr.", "Jr.")) ical = n.to_ical() assert b"\\;" not in ical assert ical.count(b";") == 4 # 4 semicolons for 5 fields def test_n_empty_fields_preserved(self): """Empty fields are preserved in N.""" n = vN(("Doe", "John", "", "", "")) ical = n.to_ical() parsed = vN.from_ical(ical) assert parsed.additional == "" assert parsed.prefix == "" assert parsed.suffix == "" def test_n_commas_in_suffix_escaped(self): """Commas within N fields ARE escaped per RFC 6350.""" n = vN(("Doe", "John", "M.", "Dr.", "Jr.,M.D.,A.C.P.")) ical = n.to_ical() assert b"\\," in ical # Round-trip preserves commas parsed = vN.from_ical(ical) assert parsed.suffix == "Jr.,M.D.,A.C.P." class TestVNIcalValue: """Test vN ical_value property.""" def test_ical_value_returns_fields(self): """ical_value property returns the NFields.""" n = vN(("Doe", "John", "M.", "Dr.", "Jr.")) assert n.ical_value == n.fields assert isinstance(n.ical_value, NFields) collective-icalendar-138c845/src/icalendar/tests/rfc_6350_vcard/test_vOrg.py000066400000000000000000000106721524134033600266620ustar00rootroot00000000000000"""Tests for vCard ORG (Organization) property per RFC 6350.""" from __future__ import annotations import pytest from icalendar.prop import vOrg class TestVOrgCreation: """Test vOrg creation and field access.""" def test_org_creation_with_tuple(self): """Create ORG from tuple of fields.""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org.fields == ("ABC Inc.", "Marketing", "Sales") def test_org_creation_with_list(self): """Create ORG from list of fields.""" org = vOrg(["ABC Inc.", "Marketing"]) assert org.fields == ("ABC Inc.", "Marketing") def test_org_creation_with_string(self): """Create ORG from vCard format string.""" org = vOrg("ABC Inc.;Marketing;Sales") assert org.fields == ("ABC Inc.", "Marketing", "Sales") def test_org_requires_at_least_one_field(self): """ORG must have at least 1 field (organization name).""" with pytest.raises(ValueError, match="at least 1 field"): vOrg(()) def test_org_variable_length(self): """ORG supports variable number of organizational units.""" # Just organization name org1 = vOrg(("ABC Inc.",)) assert len(org1.fields) == 1 # Organization with 1 unit org2 = vOrg(("ABC Inc.", "Marketing")) assert len(org2.fields) == 2 # Organization with many units org3 = vOrg(("ABC Inc.", "North America", "USA", "California", "Marketing")) assert len(org3.fields) == 5 class TestVOrgNameAndUnits: """Test vOrg name and units properties.""" def test_name_property(self): """name property returns the organization name (first field).""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org.name == "ABC Inc." def test_name_property_single_field(self): """name property works with single field.""" org = vOrg(("ABC Inc.",)) assert org.name == "ABC Inc." def test_units_property(self): """units property returns organizational units (remaining fields).""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org.units == ("Marketing", "Sales") def test_units_property_single_field(self): """units property returns empty tuple when only name is present.""" org = vOrg(("ABC Inc.",)) assert org.units == () def test_units_property_many_units(self): """units property returns all organizational units.""" org = vOrg(("ABC Inc.", "North America", "USA", "California", "Marketing")) assert org.units == ("North America", "USA", "California", "Marketing") class TestVOrgSerialization: """Test vOrg to_ical and from_ical.""" def test_org_to_ical(self): """ORG to_ical generates semicolon-separated format.""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) result = org.to_ical() assert result == b"ABC Inc.;Marketing;Sales" def test_org_from_ical(self): """Parse ORG from vCard format.""" result = vOrg.from_ical("ABC Inc.;Marketing;Sales") assert result == ("ABC Inc.", "Marketing", "Sales") def test_org_round_trip(self): """ORG survives round-trip through to_ical and from_ical.""" original = ("ABC Inc.", "North American Division", "Marketing") org = vOrg(original) ical = org.to_ical() parsed = vOrg.from_ical(ical) assert parsed == original class TestVOrgEscaping: """Test vOrg escaping behavior.""" def test_org_semicolons_not_escaped_as_separators(self): """Semicolons in ORG are field separators, not escaped.""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) ical = org.to_ical() assert b"\\;" not in ical assert ical.count(b";") == 2 # 2 semicolons for 3 fields def test_org_commas_in_name_escaped(self): """Commas within ORG fields ARE escaped per RFC 6350.""" org = vOrg(("ABC, Inc.", "Marketing")) ical = org.to_ical() assert b"\\," in ical # Round-trip preserves commas parsed = vOrg.from_ical(ical) assert parsed[0] == "ABC, Inc." class TestVOrgIcalValue: """Test vOrg ical_value property.""" def test_ical_value_returns_fields(self): """ical_value property returns the fields tuple.""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org.ical_value == org.fields assert org.ical_value == ("ABC Inc.", "Marketing", "Sales") collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/000077500000000000000000000000001524134033600241465ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/__init__.py000066400000000000000000000000511524134033600262530ustar00rootroot00000000000000"""Test cases for jCal compatibility.""" collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_additional_considerations.py000066400000000000000000000042461524134033600330010ustar00rootroot00000000000000"""These are tests for considerations that are specific to our implementation.""" import pytest from icalendar import Calendar, Component, ComponentFactory, Event, vText def test_adding_a_description_several_times_works(): """We should be able to add valid parameters several times.""" event = Event() en = vText("English description", params={"language": "en"}) de = vText("Deutsche Beschreibung", params={"language": "de"}) event.add("DESCRIPTION", en) event.add("DESCRIPTION", de) jcal = event.to_jcal() assert jcal[1][0] == [ "description", {"language": "en"}, "text", "English description", ] assert jcal[1][1] == [ "description", {"language": "de"}, "text", "Deutsche Beschreibung", ] def test_get_new_component_class(): """A new component class should be created.""" factory = ComponentFactory() cls1 = factory.get_component_class("VUNKNOWN") cls2 = factory.get_component_class("VUNKNOWN") assert cls1 is cls2 assert cls1.name == "VUNKNOWN" assert cls1 is not Component def test_text_from_jcal_does_not_add_backslashes(): """It is possible that backslashes are added. see https://github.com/collective/icalendar/pull/979#issuecomment-3568261726 """ v_text = vText.from_jcal(["name", {}, "unknown", "8 backslash \\\\\\\\"]) assert v_text.ical_value == "8 backslash \\\\\\\\" CALENDAR_NAME = "8 backslash \\\\\\\\" @pytest.fixture def calendar_with_name(): """The calendar for escape tests.""" cal = Calendar() cal.calendar_name = CALENDAR_NAME assert cal.calendar_name == CALENDAR_NAME, "Setter ad Getter work." return cal def test_backslash_escape_ics(calendar_with_name): """Test for properly escaped backslashes.""" ics = calendar_with_name.to_ical() cal2: Calendar = Calendar.from_ical(ics) assert CALENDAR_NAME == cal2.calendar_name, "ics works" def test_backslash_escape_jCal(calendar_with_name): """Test for properly escaped backslashes.""" jcal = calendar_with_name.to_jcal() cal3: Calendar = Calendar.from_jcal(jcal) assert cal3.calendar_name == CALENDAR_NAME, "jcal works" collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_categories.py000066400000000000000000000030601524134033600277030ustar00rootroot00000000000000"""Test code for categories.""" import pytest from icalendar.prop import vCategory, vText # we also work on text @pytest.fixture(params=[vCategory, vText]) def v_cat(request): """Fixture for vCategory and vText.""" return request.param def test_categories_parse_with_no_items(v_cat): """We make sure all items are collected.""" cats = v_cat.from_jcal(["categories", {}, "text", ""]) assert cats.cats == [""] def test_categories_parse_with_more_items(v_cat): """We make sure all items are collected.""" cats = v_cat.from_jcal(["categories", {}, "text", "WORK", "PERSONAL", "URGENT"]) assert cats.cats == ["WORK", "PERSONAL", "URGENT"] def test_category_jcal_with_1_item(): """Check converting vCategory to and from jCal.""" cats = vCategory(["item 1"]) jcal = cats.to_jcal("CATEGORIES") assert jcal == ["CATEGORIES", {}, "text", "item 1"] def test_category_jcal_with_0_items(): """Check converting vCategory to and from jCal.""" cats = vCategory([]) jcal = cats.to_jcal("CATEGORIES") assert jcal == ["CATEGORIES", {}, "text", ""] def test_category_jcal_with_1_empty_item(): """Check converting vCategory to and from jCal.""" cats = vCategory([""]) jcal = cats.to_jcal("CATEGORIES") assert jcal == ["CATEGORIES", {}, "text", ""] def test_category_jcal_with_3_items(): """Check converting vCategory to and from jCal.""" cats = vCategory(["item 1", "item 2", "item 3"]) jcal = cats.to_jcal("CATEGORIES") assert jcal == ["CATEGORIES", {}, "text", "item 1", "item 2", "item 3"] collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_examples.py000066400000000000000000000021031524134033600273710ustar00rootroot00000000000000"""Tests for jCal running on all ics files.""" import json from datetime import datetime from pprint import pprint from icalendar.cal.alarm import Alarm from icalendar.timezone.tzid import is_utc def test_to_jcal_can_be_json_serialized(source_file): """Check that all calendars can be converted to JCAL and serialized to JSON.""" jcal = source_file.to_jcal() s = json.dumps(jcal) assert s def test_trigger(tzp): """Check jCal loading of TRIGGER.""" alarm = Alarm() alarm.TRIGGER = tzp.localize_utc(datetime(2022, 1, 1)) assert is_utc(alarm.TRIGGER) jcal = alarm.to_jcal() pprint(jcal) alarm2 = Alarm.from_jcal(jcal) assert alarm2.TRIGGER.replace(tzinfo=None) == datetime(2022, 1, 1) assert is_utc(alarm2.TRIGGER) def test_trigger_from_example(events): """Check jCal loading of TRIGGER.""" alarm: Alarm = events.rfc_9074_example_proximity.subcomponents[0] print(alarm["TRIGGER"]) jcal = alarm.to_jcal() pprint(jcal) alarm2 = Alarm.from_jcal(jcal) assert alarm2 == alarm assert is_utc(alarm2.TRIGGER) collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_invalid_jcal.py000066400000000000000000000420321524134033600301770ustar00rootroot00000000000000"""We would like to have certain errors for invalid JCal.""" from json import JSONDecodeError import pytest from icalendar import ( Calendar, Component, InvalidCalendar, JCalParsingError, Parameters, vDate, vDatetime, vDDDTypes, vDuration, vPeriod, vTime, ) from icalendar.prop import vCategory, vMonth, vRecur, vSkip def test_invalid_json(): """JSON can be invalid""" with pytest.raises(JSONDecodeError): Component.from_jcal("") def test_jcal_parsing_error_is_invalid_calendar(): """JCalParsingError should inherit from InvalidCalendar.""" assert issubclass(JCalParsingError, InvalidCalendar) def test_validate_list_type_rejects_non_list(): """List validation reports the container type before checking its items.""" with pytest.raises(JCalParsingError, match=r"The value must be a list\."): JCalParsingError.validate_list_type("not-a-list", str, "test") @pytest.fixture(params=[[], {}, None, 1, 1.3]) def str_expected(request): """Return everything else""" return request.param @pytest.fixture(params=["a", {}, None, 1, 1.3]) def list_expected(request): """Return everything else""" return request.param @pytest.fixture(params=["a", [], None, 1, 1.3]) def object_expected(request): """Return everything else""" return request.param @pytest.fixture(params=["a", [], {}, None, 1.3]) def int_expected(request): """Return everything else""" return request.param @pytest.fixture(params=["a", [], {}, None, 1]) def float_expected(request): """Return everything else""" return request.param @pytest.fixture(params=[[], {}, None, [None], [{}], [[]]]) def parameter_value_expected(request): """Return everything else""" return request.param @pytest.mark.parametrize("length", [0, 1, 2, 4, 5]) def test_component_is_too_short_or_too_long(length): """Test that a component has not enough values. See https://github.com/collective/icalendar/pull/979/#discussion_r2529703982 """ with pytest.raises( JCalParsingError, match=r"in Component: A component must be a list with 3 items\.", ): Component.from_jcal(["vevent", [], [], [], []][:length]) def test_invalid_component_type(list_expected): """We expect a list for components.""" if isinstance(list_expected, str): return # skip JSON with pytest.raises( JCalParsingError, match=r"Component: A component must be a list with 3 items\." ): Component.from_jcal(list_expected) with pytest.raises( JCalParsingError, match=r"\[2\]\[0\] in Calendar: A component must be a list with 3 items\.", ): Component.from_jcal(["VCALENDAR", [], [list_expected]]) def test_invalid_component_name(str_expected): """Test the component name.""" with pytest.raises( JCalParsingError, match=r"\[0\] in Component: The name must be a string\." ): Component.from_jcal([str_expected, [], []]) def test_invalid_component_properties(list_expected): """Test the component properties.""" with pytest.raises( JCalParsingError, match=r"\[1\] in Alarm: The properties must be a list\." ): Component.from_jcal(["valarm", list_expected, []]) def test_invalid_component_subcomponents(list_expected): """Test the component subcomponents.""" with pytest.raises( JCalParsingError, match=r"\[2\] in Todo: The subcomponents must be a list\." ): Component.from_jcal(["vtodo", [], list_expected]) @pytest.mark.parametrize("i", [0, 3, 10]) def test_invalid_component_property(list_expected, i): """Test the component properties.""" with pytest.raises( JCalParsingError, match=rf"\[1\]\[{i}\] in Event: The property must be a list with at least 4 items\.", ): Component.from_jcal( ["vevent", [["x-x", {}, "unknown", ""]] * i + [list_expected], []] ) @pytest.mark.parametrize("length", [0, 1, 2, 3]) def test_property_too_short(length, v_prop_example, v_prop): """The example is too short.""" jcal = v_prop_example.to_jcal("name")[:length] with pytest.raises( JCalParsingError, match=rf"in {v_prop.__name__}: The property must be a list with at least 4 items\.", ): v_prop.from_jcal(jcal) with pytest.raises( JCalParsingError, match=r"in Parameters: The property must be a list with at least 4 items\.", ): Parameters.from_jcal_property(jcal) def test_property_name(v_prop_example, v_prop, str_expected): """The name is a string.""" jcal = v_prop_example.to_jcal("name") jcal[0] = str_expected with pytest.raises( JCalParsingError, match=rf"\[0\] in {v_prop.__name__}: The name must be a string\.", ): v_prop.from_jcal(jcal) def test_property_params(v_prop_example, v_prop, object_expected): """The name is a string.""" jcal = v_prop_example.to_jcal("name") jcal[1] = object_expected with pytest.raises( JCalParsingError, match=rf"\[1\] in {v_prop.__name__}: The parameters must be a mapping\.", ): v_prop.from_jcal(jcal) with pytest.raises( JCalParsingError, match=r"\[1\] in Parameters: The parameters must be a mapping\.", ): Parameters.from_jcal_property(jcal) def test_property_type(v_prop_example, v_prop, str_expected): """The name is a string.""" jcal = v_prop_example.to_jcal("name") jcal[2] = str_expected with pytest.raises( JCalParsingError, match=rf"\[2\] in {v_prop.__name__}: The VALUE parameter must be a string\.", ): v_prop.from_jcal(jcal) @pytest.mark.parametrize("index", [0, 3]) def test_property_too_short_in_component(v_prop_example, v_prop, index): """The example is too short.""" jcal = v_prop_example.to_jcal("x-prop") component = [ "vcalendar", [["x-prop-2", {}, "unknown", ""]] * index + [jcal[:2]], [], ] print(jcal) with pytest.raises( JCalParsingError, match=rf"\[1\]\[{index}\] in Calendar: The property must be a list with at least 4 items\.", ): Component.from_jcal(component) def test_parameters_keys(str_expected): """The parameter keys should be all strings.""" if str_expected in ([], {}): return # TypeError: unhashable type with pytest.raises( JCalParsingError, match=r"in Parameters: All parameter names must be strings\.", ): Parameters.from_jcal_property(["", {str_expected: "value"}, "", ""]) @pytest.mark.parametrize("key", ["a", "key"]) def test_values_allowed_in_parameters(parameter_value_expected, key): """The parameter keys should be all strings.""" with pytest.raises( JCalParsingError, match=rf'\[1\]\["{key}"\] in Parameters: Parameter values must be a string, integer or float or a list of those\.', ): Parameters.from_jcal_property(["", {key: parameter_value_expected}, "", ""]) def test_failing_example_delegated_to(calendars): """Check the failing example.""" calendar: Calendar = calendars.rfc_7256_multi_value_parameters jcal = calendar.to_jcal() delegated_to = jcal[2][0][1][0][1]["delegated-to"] assert delegated_to == [ "mailto:jdoe@example.com", "mailto:jqpublic@example.com", ] assert all(type(item) is str for item in delegated_to) @pytest.mark.parametrize( ("v_prop", "ty", "message", "value"), [ (vDatetime, "date-time", r"Cannot parse date-time\.", ""), (vDate, "date", r"Cannot parse date\.", ""), (vTime, "time", r"Cannot parse time\.", ""), (vTime, "time", r"Cannot parse time\.", "asd"), (vDuration, "duration", r"Cannot parse duration\.", "PXD"), ( vDDDTypes, "date-time", r"Cannot parse date, time, date-time, duration, or period\.", "", ), ], ) def test_date_time_parsing_errors(v_prop, message, ty, value): """An empty datetime should raise an error.""" with pytest.raises( JCalParsingError, match=rf"\[3\] in {v_prop.__name__}: {message}", ): v_prop.from_jcal(["dt", {}, ty, value]) @pytest.mark.parametrize( "v_prop", [ vDuration, vDate, vTime, vDatetime, vDDDTypes, ], ) def test_wrong_type(v_prop, str_expected): """Passing an int or float where a string is expected should raise an error.""" if isinstance(str_expected, list) and v_prop is vDDDTypes: return # skip vPeriod with pytest.raises( JCalParsingError, match=rf"\[3\] in {v_prop.__name__}: The value must be a string\.", ): v_prop.from_jcal(["dt", {}, "date-time", str_expected]) def test_vPeriod_wrong_type(str_expected): """Passing an int or float where a string is expected should raise an error.""" with pytest.raises( JCalParsingError, match=r"\[3\]\[0\] in .*: The value must be a string\.", ): vPeriod.from_jcal( ["rdate", {}, "period", [str_expected, "2024-01-01T00:00:00Z"]] ) with pytest.raises( JCalParsingError, match=r"\[3\]\[1\] in .*: The value must be a string\.", ): vPeriod.from_jcal( ["rdate", {}, "period", ["2024-01-01T00:00:00Z", str_expected]] ) def test_vPeriod_too_short(): """A period with too few items should raise an error.""" with pytest.raises( JCalParsingError, match=r"\[3\] in vPeriod: A period must be a list with exactly 2 items\.", ): vPeriod.from_jcal(["rdate", {}, "period", ["2024-01-01T00:00:00Z"]]) def test_vPeriod_too_long(): """A period with too many items should raise an error.""" with pytest.raises( JCalParsingError, match=r"\[3\] in vPeriod: A period must be a list with exactly 2 items\.", ): vPeriod.from_jcal( [ "rdate", {}, "period", ["2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", "extra"], ] ) def test_vPeriod_expects_date_time_as_start(): """vPeriod expects date-time as start but we hand in a duration.""" with pytest.raises( JCalParsingError, match=r"\[3\]\[0\] in .*: Cannot parse date-time\.", ): vPeriod.from_jcal( [ "rdate", {}, "period", ["P1D", "2024-01-02T00:00:00Z"], ] ) def test_vPeriod_expects_date_time_or_duration_as_second_item(): """vPeriod expects date-time or duration as second item but we hand in a date.""" with pytest.raises( JCalParsingError, match=r"\[3\]\[1\] in .*: Cannot parse date-time or duration\.", ): vPeriod.from_jcal( [ "rdate", {}, "period", ["2024-01-01T00:00:00", "2024-01-02"], ] ) @pytest.mark.parametrize("index", [0, 4]) def test_invalid_category_type(str_expected, index): """The name is a string.""" with pytest.raises( JCalParsingError, match=rf"\[{index + 3}\] in vCategory: The value must be a string\.", ): vCategory.from_jcal(["categories", {}, "text"] + [""] * index + [str_expected]) def test_validation_of_list(): """Check the list validation.""" JCalParsingError.validate_list_type(["a", "b", "c"], str, "test", ["path"]) with pytest.raises( JCalParsingError, match=r"\[3\]\[1\] in test: Each item in the list must be a string\.", ): JCalParsingError.validate_list_type(["a", 1, "c"], str, "test", path=3) def test_recurrence_rule_must_be_mapping(object_expected): """The recurrence rule must be a mapping.""" with pytest.raises( JCalParsingError, match=r"\[3\] in vRecur: The recurrence rule must be a mapping with string keys\.", ): vRecur.from_jcal(["rrule", {}, "recur", object_expected]) def test_recurrence_rule_must_be_mapping_with_str(str_expected): """The recurrence rule must be a mapping.""" if isinstance(str_expected, (dict, list)): return # skip unhashable type with pytest.raises( JCalParsingError, match=r"\[3\] in vRecur: The recurrence rule must be a mapping with string keys\.", ): vRecur.from_jcal(["rrule", {}, "recur", {str_expected: 1}]) @pytest.mark.parametrize( "key", [ "count", "interval", "bysecond", "byminute", "byhour", "byweekno", "bymonthday", "byyearday", ], ) @pytest.mark.parametrize("as_list", [False, True]) def test_int_parameters(int_expected, key, as_list): """The integer parameters must be integers.""" correct_value = 4 if as_list: int_expected = [int_expected] correct_value = [4] # parse correct value recur = vRecur.from_jcal(["rrule", {}, "recur", {key: correct_value}]) assert recur[key] == correct_value or recur[key] == [correct_value] if int_expected == []: return # parse bad value with pytest.raises( JCalParsingError, match=f'\\[3\\]\\["{key}"\\](?:\\[0\\])? in vInt: The value must be an integer.', ): vRecur.from_jcal(["rrule", {}, "recur", {key: int_expected}]) @pytest.mark.parametrize("valid", [1, "1", "1L"]) def test_parse_jcal_value_month(valid): """Test parsing of vMonth jCal values.""" month = vMonth.parse_jcal_value(valid) assert month == 1 assert month.leap == (valid == "1L") @pytest.mark.parametrize("invalid", ["abc", "", 1.5, [], {}, "1LL"]) def test_parse_jcal_value_month_invalid(invalid): """Test parsing of vMonth jCal values.""" with pytest.raises( JCalParsingError, match=r"in vMonth: The value must be a string or an integer\.", ): vMonth.parse_jcal_value(invalid) def test_skip(): """The skip parameter works.""" assert vSkip.parse_jcal_value("OMIT") is vSkip.OMIT with pytest.raises( JCalParsingError, match=r"in vSkip: The value must be a valid skip value\.", ): vSkip.parse_jcal_value("INVALID") INVALID_NAME_TOKENS = ["x-foo:INJECT", "x-foo\rRRULE", "a;b", "a=b", "a,b", "a b", ""] @pytest.mark.parametrize("name", INVALID_NAME_TOKENS) def test_invalid_property_name_token(name): """A jCal property name that is not a valid token is rejected. Otherwise the name is re-emitted verbatim into the content line, so a ``:``, ``;``, or a lone carriage return injects structure on serialization. """ with pytest.raises( JCalParsingError, match=r"The property name must be a valid iCalendar token", ): Component.from_jcal(["vcalendar", [[name, {}, "text", "x"]], []]) @pytest.mark.parametrize("name", INVALID_NAME_TOKENS) def test_invalid_parameter_name_token(name): """A jCal parameter name that is not a valid token is rejected.""" with pytest.raises( JCalParsingError, match=r"The parameter name must be a valid iCalendar token", ): Parameters.from_jcal_property(["x-prop", {name: "v"}, "text", "x"]) @pytest.mark.parametrize("key", ["FREQ:INJECT", "FREQ\rINJECT", "a;b", "a=b", ""]) def test_invalid_recur_part_name_token(key): """A jCal RRULE part name that is not a valid token is rejected.""" with pytest.raises( JCalParsingError, match=r"The recurrence rule part name must be a valid iCalendar token", ): vRecur.from_jcal(["rrule", {}, "recur", {key: "DAILY"}]) # valid tokens that are not lowercase (RFC 7265 sections 3.4, 3.5 and 3.6.10) UPPER_CASE_NAMES = ["SUMMARY", "Summary", "X-Foo", "x-Foo"] @pytest.mark.parametrize("name", UPPER_CASE_NAMES) def test_property_name_must_be_lowercase(name): """A jCal property name must be lowercase (RFC 7265 section 3.4).""" with pytest.raises( JCalParsingError, match=r"The property name must be lowercase\.", ): Component.from_jcal(["vcalendar", [[name, {}, "text", "x"]], []]) @pytest.mark.parametrize("name", UPPER_CASE_NAMES) def test_parameter_name_must_be_lowercase(name): """A jCal parameter name must be lowercase (RFC 7265 section 3.5).""" with pytest.raises( JCalParsingError, match=r"The parameter name must be lowercase\.", ): Parameters.from_jcal_property(["x-prop", {name: "v"}, "text", "x"]) @pytest.mark.parametrize("key", ["FREQ", "Freq", "COUNT", "ByDay"]) def test_recur_part_name_must_be_lowercase(key): """A jCal RRULE part name must be lowercase (RFC 7265 section 3.6.10).""" with pytest.raises( JCalParsingError, match=r"The recurrence rule part name must be lowercase\.", ): vRecur.from_jcal(["rrule", {}, "recur", {key: "DAILY"}]) def test_frequency(): """The FREQ parameter must be valid.""" # parse correct value recur = vRecur.from_jcal(["rrule", {}, "recur", {"freq": "DAILY"}]) assert recur["FREQ"] == "DAILY" # parse bad value with pytest.raises( JCalParsingError, match=r'\[3\]\["freq"\] in vFrequency: The value must be a valid frequency\.', ): vRecur.from_jcal(["rrule", {}, "recur", {"freq": "INVALID"}]) collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_3_3_components.py000066400000000000000000000023571524134033600321430ustar00rootroot00000000000000"""Test the conversion of components.""" import json import pytest from icalendar import Calendar, Event, Journal, Todo @pytest.mark.parametrize( "component_class", [ Calendar, Event, Todo, Journal, ], ) def test_component_conversion(component_class): """test conversion of components to jCal.""" component = component_class() jcal = component.to_jcal() assert len(jcal) == 3 assert jcal[0] == component_class.name.lower() assert jcal[1] == [] assert jcal[2] == [] @pytest.mark.parametrize( "component_class", [ Event, Todo, Journal, ], ) def test_nesting(component_class): """Nested components turn up in an sub array.""" calendar = Calendar() calendar.add_component(component_class()) jcal = calendar.to_jcal() assert len(jcal) == 3 assert jcal[0] == "vcalendar" assert jcal[1] == [] assert len(jcal[2]) == 1 assert jcal[2][0] == [ component_class.name.lower(), [], [], ] def test_apply_json_serialization(): """Check that we can convert to JSON.""" calendar = Calendar() calendar.add_component(Event()) assert calendar.to_json() == json.dumps(calendar.to_jcal()) collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_3_4_properties.py000066400000000000000000000032601524134033600321450ustar00rootroot00000000000000"""The RFC contains examples of conversions that we can use here.""" from icalendar import Event, vGeo def test_convert_text_property_example(): """Convert the example from Section 3.4 of :rfc:`7265`.""" event = Event() event.add("SUMMARY", "Meeting with Fred") event.add("categories", ["Meetings", "Work"]) jcal = event.to_jcal() assert jcal == [ "vevent", [ [ "summary", {}, "text", "Meeting with Fred", ], [ "categories", {}, "text", "Meetings", "Work", ], ], [], ] def test_vGeo(): """Check convertion of vGeo""" geo = vGeo((37.386013, -122.082932)) assert geo.to_jcal("geo") == ["geo", {}, "float", [37.386013, -122.082932]] def test_request_status(events): """Convert the request status example.""" event: Event = events.rfc_7265_request_status jcal = event.to_jcal() assert jcal[1][0] == ["request-status", {}, "text", ["2.0", "Success"]], ( "request-status 1" ) assert jcal[1][1] == [ "request-status", {}, "text", ["3.7", "Invalid calendar user", "ATTENDEE:mailto:jsmith@example.org"], ], "request-status 2" assert jcal == [ "vevent", [ ["request-status", {}, "text", ["2.0", "Success"]], [ "request-status", {}, "text", ["3.7", "Invalid calendar user", "ATTENDEE:mailto:jsmith@example.org"], ], ], [], ], "whole component" collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_3_5_parameters.py000066400000000000000000000127771524134033600321320ustar00rootroot00000000000000""" Property parameters are represented as a JSON object where each key- value pair represents the iCalendar parameter name and its value. The name of the parameter MUST be in lowercase; the original case of the parameter value MUST be preserved. """ from typing import TYPE_CHECKING import pytest from icalendar import PARTSTAT, ROLE, VPROPERTY, Event, vCalAddress from icalendar.parser import Parameters if TYPE_CHECKING: from icalendar.cal.calendar import Calendar def test_vCalAddressExample(): """Test the example from vCalAddress.""" addr = vCalAddress.new( "jsmith@example.org", partstat=PARTSTAT.ACCEPTED, rsvp=True, role=ROLE.REQ_PARTICIPANT, ) event = Event() event.attendees = [addr] event.summary = "Meeting" jcal = event.to_jcal() assert jcal == [ "vevent", [ [ "attendee", {"partstat": "ACCEPTED", "rsvp": "TRUE", "role": "REQ-PARTICIPANT"}, "cal-address", "mailto:jsmith@example.org", ], ["summary", {}, "text", "Meeting"], ], [], ] def test_parameter_conversion_is_handed_over_to_parameter(v_prop: VPROPERTY, mock): """The parameters are part of the jCal conversion.""" example = v_prop.examples()[0] assert isinstance(example.params, Parameters) example.params = mock return_value = example.params.to_jcal.return_value jcal = example.to_jcal("X-NAME") mock.to_jcal.assert_called_once() assert len(jcal) >= 4 assert jcal[1] == return_value assert jcal[0] == "X-NAME" def test_all_default_values_are_lowercase(v_prop_example): """All the default values should be lowercase. :rfc:`7265`: If the property has no "VALUE" parameter but has a default value type, the default value type is used. """ jcal = v_prop_example.to_jcal("X-NAME") ical_type: str = jcal[2] assert ical_type.islower() def test_all_set_values_are_preserved(v_prop_example): """Check the set value type. :rfc:`5545`: All names of [...] property parameter values are case-insensitive. :rfc:`7265`: If the property has a "VALUE" parameter, that parameter's value is used as the value type. We can convert them to lowercase but it is not stated explicitely. """ v_prop_example.VALUE = "X-OTHER-VALUE" jcal = v_prop_example.to_jcal("X-NAME") ical_type: str = jcal[2] assert ical_type == "x-other-value" def test_unknown_value(calendars): """Test unknown value. :rfc:`7265`: If the property has no "VALUE" parameter and has no default value type, "unknown" is used. """ calendar: Calendar = calendars.x_location jcal = calendar.to_jcal() props = jcal[1] checked = 0 for prop in props: print(prop) if prop[0].startswith("x-"): assert prop[2] == "unknown" checked += 1 assert checked >= 3, "We checked at least some properties." @pytest.mark.parametrize( ("event_index", "parameter_index", "expected_value"), [ ( 0, 0, [ "attendee", { "delegated-to": [ "mailto:jdoe@example.com", "mailto:jqpublic@example.com", ] }, "cal-address", "mailto:jsmith@example.com", ], ), ( 0, 1, [ "attendee", { "delegated-from": [ "mailto:jsmith@example.com", "mailto:jdoe@example.com", ] }, "cal-address", "mailto:jdoe@example.com", ], ), ( 0, 2, [ "attendee", { "member": [ "mailto:projectA@example.com", "mailto:projectB@example.com", ] }, "cal-address", "mailto:janedoe@example.com", ], ), ( 1, 0, [ "attendee", {"delegated-to": "mailto:jdoe@example.com"}, "cal-address", "mailto:jsmith@example.com", ], ), ( 1, 1, [ "attendee", {"delegated-from": "mailto:jsmith@example.com"}, "cal-address", "mailto:jdoe@example.com", ], ), ( 1, 2, [ "attendee", {"member": "mailto:projectA@example.com"}, "cal-address", "mailto:janedoe@example.com", ], ), ], ) def test_parameters_with_values_as_list( calendars, event_index, parameter_index, expected_value ): """Check the conversion of list value parameters. In [RFC5545], some parameters allow using a COMMA-separated list of values. To ease processing in jCal, the value of such parameters MUST be represented in an array containing the separated values. """ calendar: Calendar = calendars.rfc_7256_multi_value_parameters event = calendar.events[event_index] parameter = event.to_jcal()[1][parameter_index] assert parameter == expected_value collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_3_6_values.py000066400000000000000000000176731524134033600312670ustar00rootroot00000000000000"""Check serialization and deserialization of values. See https://datatracker.ietf.org/doc/html/rfc7265#section-3.6 """ from datetime import date, datetime, time, timedelta from zoneinfo import ZoneInfo import pytest from icalendar import ( Calendar, Event, vBinary, vBoolean, vCalAddress, vDate, vDatetime, vDDDLists, vDDDTypes, vDuration, vFloat, vInt, vPeriod, vRecur, vText, vTime, vUnknown, vUri, vUTCOffset, ) JCAL_PAIRS = [ (["attach", {}, "binary", "SGVsbG8gV29ybGQh"], vBinary(b"Hello World!")), (["x-non-smoking", {}, "boolean", True], vBoolean(True)), (["x-non-smoking", {}, "boolean", False], vBoolean(False)), ( ["attendee", {}, "cal-address", "mailto:kewisch@example.com"], vCalAddress("mailto:kewisch@example.com"), ), (["dtstart", {}, "date", "2011-05-17"], vDate(date(2011, 5, 17))), ( ["dtstart", {}, "date-time", "2012-10-17T12:00:00"], vDatetime(datetime(2012, 10, 17, 12, 0, 0)), ), ( ["dtstamp", {}, "date-time", "2012-11-17T12:00:00Z"], vDatetime(datetime(2012, 11, 17, 12, 0, 0, tzinfo=ZoneInfo("UTC"))), ), ( ["dtend", {"tzid": "Europe/Berlin"}, "date-time", "2011-12-17T13:00:00"], vDatetime(datetime(2011, 12, 17, 13, 0, 0, tzinfo=ZoneInfo("Europe/Berlin"))), ), (["duration", {}, "duration", "P1D"], vDuration(timedelta(days=1))), (["x-grade", {}, "float", 1.3], vFloat(1.3)), (["percent-complete", {}, "integer", 42], vInt(42)), (["x-foo", {}, "integer", -42], vInt(-42)), ( ["freebusy", {"fbtype": "FREE"}, "period", ["1997-03-08T16:00:00Z", "P1D"]], vPeriod( (datetime(1997, 3, 8, 16, 0, 0, tzinfo=ZoneInfo("UTC")), timedelta(days=1)), params={"fbtype": "FREE"}, ), ), ( [ "freebusy", {"fbtype": "FREE", "tzid": "Europe/Moscow"}, "period", ["1997-03-08T16:00:00", "P1D"], ], vPeriod( ( datetime(1997, 3, 8, 16, 0, 0, tzinfo=ZoneInfo("Europe/Moscow")), timedelta(days=1), ), params={"fbtype": "FREE"}, ), ), ( [ "freebusy", {"fbtype": "FREE"}, "period", ["1997-03-08T16:00:00", "1997-03-08T16:00:01"], ], vPeriod( (datetime(1997, 3, 8, 16, 0, 0), datetime(1997, 3, 8, 16, 0, 1)), params={"fbtype": "FREE"}, ), ), ( [ "rrule", {}, "recur", {"freq": "YEARLY", "count": 5, "byday": ["-1SU", "2MO"], "bymonth": [10]}, ], vRecur({"freq": "YEARLY", "count": 5, "bymonth": 10, "byday": ["-1SU", "2MO"]}), ), ( [ "rrule", {}, "recur", { "freq": "MONTHLY", "interval": 2, "bymonthday": [1, 15, -1], "until": "2013-10-01", }, ], vRecur( { "freq": "MONTHLY", "interval": 2, "bymonthday": [1, 15, -1], "until": [date(2013, 10, 1)], } ), ), (["comment", {}, "text", "hello, world"], vText("hello, world")), (["x-time-local", {}, "time", "12:30:00"], vTime(time(hour=12, minute=30))), ( ["x-time-utc", {}, "time", "12:31:00Z"], vTime(time(hour=12, minute=31, tzinfo=ZoneInfo("UTC"))), ), ( ["x-time-offset", {"tzid": "Europe/Berlin"}, "time", "12:30:00"], vTime(time(hour=12, minute=30, tzinfo=ZoneInfo("Europe/Berlin"))), ), ( ["tzurl", {}, "uri", "http://example.org/tz/Europe-Berlin.ics"], vUri("http://example.org/tz/Europe-Berlin.ics"), ), (["tzoffsetfrom", {}, "utc-offset", "-05:00"], vUTCOffset(timedelta(hours=-5))), ( ["tzoffsetto", {}, "utc-offset", "+12:45"], vUTCOffset(timedelta(hours=12, minutes=45)), ), (["x-foo", {}, "unknown", "bar"], vUnknown("bar")), ( ["trigger", {}, "date-time", "1976-04-01T00:55:45Z"], vDDDTypes(datetime(1976, 4, 1, 0, 55, 45, tzinfo=ZoneInfo("UTC"))), ), ] mark_pairs = pytest.mark.parametrize(("jcal_value", "prop"), JCAL_PAIRS) @mark_pairs def test_convert_property_to_jcal(jcal_value, prop): """Check converting an icalendar property type into a jCal value""" assert prop.to_jcal(jcal_value[0]) == jcal_value @mark_pairs def test_convert_jcal_to_property(jcal_value, prop): """Check converting an icalendar property type into a jCal value""" assert prop.from_jcal(jcal_value) == prop vDDD_pairs = [ # noqa: N816 (jcal, prop) for jcal, prop in JCAL_PAIRS if isinstance(prop, (vDatetime, vDate, vTime, vDuration)) ] @pytest.mark.parametrize(("jcal_value", "prop"), vDDD_pairs) @pytest.mark.parametrize("ddd_type", [vDDDTypes, vDDDLists]) def test_parse_from_ddd(ddd_type, jcal_value, prop): """The DDD types should be able to parse from a jCal value that is of a supported type.""" parsed = ddd_type.from_jcal(jcal_value) print(parsed) print(prop, "expected") assert parsed == prop def test_adding_unknown_value_parameter(): """When converting to iCalendar, [...] the VALUE parameter MUST be omitted for properties that have the jCal type identifier "unknown".""" jcal = ["x-foo", {}, "unknown", "bar"] prop = vUnknown.from_jcal(jcal) calendar = Calendar() calendar["X-FOO"] = prop ical = calendar.to_ical().decode("utf-8") assert "UNKNOWN" not in ical assert "VALUE" not in ical def test_unknown_jcal_type_does_not_add_value_parameter(): """The "unknown" jCal type must not produce a VALUE parameter. Per :rfc:`7265#section-5.2` a value type of "unknown" yields no VALUE parameter, even for a registered property such as DTSTART (whose default is DATE-TIME) reached through ``Component.from_jcal``. UNKNOWN is reserved from iCalendar. """ jcal = ["vevent", [["uid", {}, "text", "1"], ["dtstart", {}, "unknown", "x"]], []] event = Event.from_jcal(jcal) assert "VALUE" not in event["DTSTART"].params assert b"VALUE=UNKNOWN" not in event.to_ical() @pytest.mark.parametrize( ("ics_line", "prop_name", "expected_value"), [ # Explicit, non-default value types must survive the jCal round-trip. ("RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z", "RDATE", "PERIOD"), ("TRIGGER;VALUE=DATE-TIME:19760401T005545Z", "TRIGGER", "DATE-TIME"), ("DTEND;VALUE=DATE:19961230", "DTEND", "DATE"), # Default value types must not gain a spurious VALUE parameter, even for # properties whose jCal type differs from the internal type name (such # as CATEGORIES, whose jCal type is "text"). ("DTSTART:19961230T020000Z", "DTSTART", None), ("RDATE:19970101T180000Z", "RDATE", None), ("TRIGGER:PT15M", "TRIGGER", None), ("CATEGORIES:a,b,c", "CATEGORIES", None), ("SUMMARY:hello", "SUMMARY", None), # Unregistered X- properties: an explicit VALUE must survive, while a # bare X- property (jCal type "unknown") must not gain a VALUE parameter # -- in particular never the reserved VALUE=UNKNOWN (RFC 7265 section 5.2). ("X-FOO;VALUE=DATE:19961230", "X-FOO", "DATE"), ("X-FOO:plain text", "X-FOO", None), ], ) def test_jcal_round_trip_preserves_value_parameter(ics_line, prop_name, expected_value): """jCal encodes the value type in the type field rather than as a VALUE parameter. A round-trip must restore an explicit, non-default VALUE parameter and must not introduce one for default value types (GH #1426).""" event = Event.from_ical(f"BEGIN:VEVENT\r\n{ics_line}\r\nEND:VEVENT\r\n") restored = Event.from_jcal(event.to_jcal()) prop = restored[prop_name] if isinstance(prop, list): prop = prop[0] assert prop.params.get("VALUE") == expected_value collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_4_ical_parsing.py000066400000000000000000000010631524134033600321610ustar00rootroot00000000000000"""Converting from jCal into iCalendar - :rfc:`7265` - parameters - properties - components """ import pytest from icalendar.parser import Parameters @pytest.mark.parametrize("jcal", [{}, {"value": "DATE-TIME"}, {"x-slack": "30.3"}]) def test_parameters_from_jcal(jcal): """General constrints for parameter parsing. > When converting [...] property parameter names, > the names SHOULD be converted to uppercase. """ params = Parameters.from_jcal(jcal) assert len(params) == len(jcal) assert all(key.isupper() for key in params) collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py000066400000000000000000000034671524134033600316010ustar00rootroot00000000000000"""Check that we convert the examples propertly.""" from pprint import pprint import pytest from icalendar import Calendar, Event, vText from icalendar.parser_tools import to_unicode def test_convert_coffee(calendars): """convert the unknown value property""" calendar = calendars.rfc_7265_example_2 ical = calendar.to_ical().decode() print(to_unicode(ical)) assert r"X-COFFEE-DATA:Stenophylla;Guinea\,Africa" in ical @pytest.mark.parametrize( ("prop_name", "value", "expected_value"), [ ("SUMMARY", "TEXT", "TEXT"), ("SUMMARY", "URI", "URI"), ("DESCRIPTION", "URI", "URI"), ("X-ALT-DESC", "UNKNOWN", "UNKNOWN"), ("X-ALT-DESC", "TEXT", "TEXT"), ("DESCRIPTION", "TEXT", "TEXT"), ], ) def test_know_attributes_do_not_receive_a_VALUE(prop_name, value, expected_value): """If we do not know a certain attribute, we always use the unknown property type.""" event = Event() prop = vText("XXX") prop.VALUE = value event[prop_name] = prop jcal = event.to_jcal() pprint(jcal) event2 = Event.from_jcal(jcal) pprint(event2.to_jcal()) assert event2[prop_name].VALUE == expected_value def test_conversion_from_ical(calendars): """Convert the example to jCal.""" calendar: Calendar = calendars.rfc_7265_example_1 jcal = calendar.to_jcal() assert jcal[1][0] == ["x-complaint-deadline", {}, "unknown", "20110512T120000Z"] def test_example_3(calendars): """Test the 3rd example.""" calendar = calendars.rfc_7265_example_3 ical = calendar.to_ical().decode() assert "PERCENT-COMPLETE:95" in ical def test_example_4(events): """Test the 4th example.""" event = events.rfc_7265_example_4 ical = event.to_jcal() assert ical[1][0] == ["dtstart", {"x-slack": "30.3"}, "date", "2011-05-12"] collective-icalendar-138c845/src/icalendar/tests/rfc_7265_jcal/test_uri_parameters_round_trip.py000066400000000000000000000054371524134033600330570ustar00rootroot00000000000000"""Property parameters on URI-typed properties must survive a jCal round-trip. ``vUri.from_jcal`` used to pass the parsed parameters as the second *positional* argument. Unlike every other value type, ``vUri.__new__`` takes an ``encoding`` argument in that slot (``vUri(value, encoding, /, params=None)``), so the :class:`~icalendar.parser.Parameters` object was bound to ``encoding`` and silently discarded, leaving ``params`` at its default of ``None``. As a result parameters such as ``FMTTYPE`` (ATTACH), ``FEATURE``/``LABEL`` (CONFERENCE) or ``RELTYPE`` (RELATED-TO) were dropped whenever a URI property was read back from jCal, breaking the round-trip required by :rfc:`7265`. """ from icalendar import Calendar, Component, Event, vUri from icalendar.parser import Parameters def _jcal_round_trip(component: Component) -> Component: return Component.from_jcal(component.to_jcal()) def test_vuri_from_jcal_keeps_parameters(): """The root cause: ``vUri.from_jcal`` must attach the parsed parameters.""" jcal_property = ["attach", {"fmttype": "text/plain"}, "uri", "http://example.com"] uri = vUri.from_jcal(jcal_property) assert uri.params == Parameters({"FMTTYPE": "text/plain"}) # The value (and the ``encoding`` slot the parameters used to be bound to) # is still parsed correctly. assert str(uri) == "http://example.com" def test_attach_fmttype_survives_jcal_round_trip(): event = Event() event.add("UID", "1") event.add( "ATTACH", vUri("http://example.com/report.txt", params={"FMTTYPE": "text/plain"}), ) restored = _jcal_round_trip(event) assert restored["ATTACH"].params["FMTTYPE"] == "text/plain" assert b"ATTACH;FMTTYPE=text/plain:" in restored.to_ical() def test_conference_multiple_parameters_survive_jcal_round_trip(): """Several parameters on the same URI property are all preserved.""" event = Event() event.add("UID", "1") event.add( "CONFERENCE", vUri("tel:+1-555-0100", params={"FEATURE": "AUDIO", "LABEL": "Call in"}), ) restored = _jcal_round_trip(event) params = restored["CONFERENCE"].params assert params["FEATURE"] == "AUDIO" assert params["LABEL"] == "Call in" def test_jcal_round_trip_is_idempotent_for_uri_parameters(): """``to_jcal`` of the reparsed component equals the original jCal.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "-//test//EN") event = Event() event.add("UID", "1") event.add( "ATTACH", vUri("http://example.com/x.txt", params={"FMTTYPE": "text/plain"}) ) event.add( "CONFERENCE", vUri("https://chat.example.com/", params={"FEATURE": "VIDEO"}) ) calendar.add_component(event) jcal = calendar.to_jcal() assert Component.from_jcal(jcal).to_jcal() == jcal collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/000077500000000000000000000000001524134033600301225ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/__init__.py000066400000000000000000000000341524134033600322300ustar00rootroot00000000000000"""Common functionality.""" collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/test_concept.py000066400000000000000000000022231524134033600331650ustar00rootroot00000000000000"""Test the CONCEPT property.""" import pytest from icalendar import Component, vUri @pytest.fixture def component(): """Return a basic component for testing.""" return Component() def test_concept_is_converted_to_a_vUri(component): """We want a vUri list!""" component.concepts = ["file://a/concept"] assert len(component.concepts) == 1 assert component.concepts[0].uri == "file://a/concept" assert isinstance(component.concepts[0], vUri) def test_new_component_with_concept(): """We can use new with just a string.""" component = Component.new(concepts="https://gihub.com/collective/icalendar/issues") assert len(component.concepts) == 1 assert component.concepts[0].uri == "https://gihub.com/collective/icalendar/issues" assert isinstance(component.concepts[0], vUri) def test_delete_concepts(component: Component): del component.concepts component.concepts = ["ftp://asd"] del component.concepts assert component.concepts == [] def test_delete_concepts_with_none(component: Component): component.concepts = ["ftp://asd"] component.concepts = None assert component.concepts == [] collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/test_gap.py000066400000000000000000000053771524134033600323160ustar00rootroot00000000000000"""The GAP parameter can be defined on RELATED-TO properties.""" from datetime import timedelta from typing import TYPE_CHECKING import pytest from icalendar import Journal from icalendar.error import InvalidCalendar from icalendar.prop import vText, vUid, vUri if TYPE_CHECKING: from icalendar import Calendar @pytest.fixture def component(): """Return a basic component for testing.""" return Journal() def test_get_gap_example(calendars): """Get the gap from an example calendar.""" calendar: Calendar = calendars.rfc_9253_gap event1 = calendar.events[0] event2 = calendar.events[1] assert event1.related_to == [] relation: vUri = event2.related_to[0] assert relation.params["GAP"] assert relation.GAP == timedelta(weeks=1) assert relation.uid == "1" @pytest.fixture(params=[vText, vUid, vUri]) def related_to_prop(request): """The RELATED-TO property classes to test.""" return request.param("uid-text-or-url") @pytest.mark.parametrize( ("gap_input", "value"), [ (timedelta(days=3), timedelta(days=3)), ("P15D", timedelta(days=15)), ("-PT6H", timedelta(hours=-6)), ], ) def test_get_and_set_gap(gap_input, value, related_to_prop): """Check that we can get the GAP parameter.""" related_to_prop.GAP = gap_input assert related_to_prop.GAP == value assert related_to_prop.params["GAP"] def test_serialize(component, related_to_prop): """Check that we can serialize the GAP parameter.""" related_to_prop.GAP = timedelta(days=10) component.add("RELATED-TO", related_to_prop) ical = component.to_ical().decode() assert "GAP=P10D" in ical def test_delete_gap(related_to_prop): """Check that we can delete the GAP parameter.""" related_to_prop.GAP = timedelta(days=5) del related_to_prop.GAP assert related_to_prop.GAP is None def test_delete_gap_by_setting_to_None(related_to_prop): """Check that we can delete the GAP parameter.""" related_to_prop.GAP = timedelta(days=5) related_to_prop.GAP = None assert related_to_prop.GAP is None def test_set_invalid_value(related_to_prop): """Check setting invalid values.""" with pytest.raises(ValueError): related_to_prop.GAP = "invalid" with pytest.raises(TypeError): related_to_prop.GAP = object() def test_get_malformed_value(related_to_prop): """What if the value is a string?""" related_to_prop.params["GAP"] = "invalid" with pytest.raises(InvalidCalendar): related_to_prop.GAP # noqa: B018, RUF100 def test_gap_invalid_value(related_to_prop): """Other value types could be there but are problematic.""" related_to_prop.params["GAP"] = object() with pytest.raises(TypeError): related_to_prop.GAP # noqa: B018, RUF100 collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/test_link.py000066400000000000000000000025771524134033600325030ustar00rootroot00000000000000"""Tests for icalendar relationships as per RFC 9253.""" import pytest from icalendar import ( Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, TimezoneDaylight, TimezoneStandard, Todo, ) from icalendar.prop import vUri @pytest.fixture( params=( Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, TimezoneDaylight, TimezoneStandard, Todo, ) ) def link_component_class(request): """The classes to test. rfc:`9253`:: Conformance: Link: This property can be specified zero or more times in any iCalendar component. """ return request.param @pytest.fixture def link_component(link_component_class): """An instance of the component class to test.""" return link_component_class() def test_no_links_by_default(link_component): """By default, components have no LINK properties.""" assert "LINK" not in link_component assert link_component.links == [] def test_strings_are_converted_to_vUri(link_component): """We set the URI.""" link_component.links = ["https://example.com"] assert link_component.links == [vUri("https://example.com")] assert isinstance(link_component.links[0], vUri) assert link_component.links[0].VALUE == "URI" test_new_property_types.py000066400000000000000000000142461524134033600354440ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships"""Test the new property types UID and XML-REFERENCE. - creation - parameters - serialization - deserialization - attributes """ import pytest from icalendar import Calendar, Event, vUid, vUri, vXmlReference @pytest.fixture( params=[ "123e4567-e89b-12d3-a456-426614174000", "550e8400-e29b-41d4-a716-446655440000", ] ) def uid(request): return request.param @pytest.fixture( params=[ "https://example.com/events", "https://example.com/tasks/01234567-abcd1234.ics", ] ) def uri(request): return request.param @pytest.fixture( params=[ ( "https://example.com/xmlDocs/bidFramework.xml#xpointer(descendant::CostStruc/range-to(following::CostStrucEND[1]))", "descendant::CostStruc/range-to(following::CostStrucEND[1])", ), ( "https://www.w3.org/TR/2003/REC-xptr-framework-20030325/#xpointer(id('r%C3%A9sum%C3%A9'))", "id('résumé')", ), ] ) def xml_reference(request): return request.param def test_uid_creation(uid): """Test creation of UID property.""" uid_prop = vUid(uid) assert repr(uid_prop) == f"vUid({uid!r})" assert uid_prop.ical_value == uid assert uid_prop.uid == uid assert_default_parameters(uid_prop) def test_uid_new(test_uid): """The new uid should be tested.""" uid_prop = vUid.new() assert uid_prop.uid == test_uid assert_default_parameters(uid_prop) assert uid_prop.VALUE == "UID" def test_vUri_creation(uri): """Test creation of a URI property.""" uri_prop = vUri(uri) assert str(uri_prop) == uri assert uri_prop.ical_value == uri assert uri_prop.uri == uri assert_default_parameters(uri_prop) assert uri_prop.VALUE == "URI" def test_vXmlReference_creation(xml_reference): """Test creation of a URI property.""" uri, x_pointer = xml_reference xml_prop = vXmlReference(uri) assert str(xml_prop) == uri assert xml_prop.ical_value == uri assert xml_prop.uri == uri assert_default_parameters(xml_prop) assert xml_prop.x_pointer == x_pointer assert xml_prop.VALUE == "XML-REFERENCE" def assert_default_parameters(prop): """Test an empty property.""" assert prop.LABEL is None assert prop.LANGUAGE is None assert prop.FMTTYPE is None assert prop.LINKREL is None assert repr(prop).startswith(prop.__class__.__name__ + "(") assert repr(prop).endswith(")") assert prop.params.value is None assert prop.params.get("VALUE") is None @pytest.mark.parametrize("no_x_pointer", ["", "asd)", "xpointer(id('r%C3%A9sum%C3%A9'"]) def test_not_x_pointer(no_x_pointer): """Test that the x_pointer throws a value error.""" xml = vXmlReference("http://asd#" + no_x_pointer) assert xml.x_pointer is None mark_valid_values = pytest.mark.parametrize( ("name", "value"), [ ("LABEL", None), ("LABEL", "Venue"), ("LANGUAGE", None), ("LANGUAGE", "de"), ("LANGUAGE", "en-gb"), ("LINKREL", None), ("LINKREL", "SOURCE"), ("LINKREL", "https://example.com/linkrel/derivedFrom"), ("LINKREL", "icon"), ("FMTTYPE", None), ("FMTTYPE", "text/html"), ], ) def vUri_example(): """New example instance.""" return vUri("http://example.com") def vXmlReference_example(): """New example instance.""" return vXmlReference("http://example.com") mark_new_prop = pytest.mark.parametrize( "new_prop", [vUri_example, vUid.new, vXmlReference_example] ) @mark_new_prop @mark_valid_values def test_set_attributes(new_prop, name, value): """Set the values and test""" prop = new_prop() assert hasattr(prop, name) setattr(prop, name, value) print(prop.params) assert getattr(prop, name) == value if value is None: assert name not in prop.params else: assert prop.params[name] == value @mark_new_prop @mark_valid_values def test_delete_attribute_setting_to_none(new_prop, name, value): """The attribute can be deleted by setting to None.""" prop = new_prop() setattr(prop, name, value) setattr(prop, name, None) assert name not in prop.params assert getattr(prop, name) is None @mark_new_prop @mark_valid_values def test_delete_attribute(new_prop, name, value): """The attribute can be deleted.""" prop = new_prop() setattr(prop, name, value) delattr(prop, name) assert name not in prop.params assert getattr(prop, name) is None def test_parse_example_calendar(calendars): """We should make sure that examples from the RFC work.""" calendar: Calendar = calendars.rfc_9253_examples event = calendar.events[0] links = event.links assert len(links) == 3 assert links[0].uri == "https://example.com/events" assert links[0].LINKREL == "SOURCE" assert links[0].FMTTYPE is None assert links[0].LABEL == "Venue" assert isinstance(links[0], vUri) assert links[1].uri == "https://example.com/tasks/01234567-abcd1234.ics" assert links[1].LINKREL == "https://example.com/linkrel/derivedFrom" assert links[1].FMTTYPE is None assert links[1].LABEL is None assert isinstance(links[1], vUri) assert ( links[2].xml_reference == "https://example.com/xmlDocs/bidFramework.xml#xpointer(descendant::CostStruc/range-to(following::CostStrucEND[1]))" ) assert links[2].LINKREL == "https://example.com/linkrel/costStructure" assert links[2].FMTTYPE is None assert links[2].LABEL is None assert isinstance(links[2], vXmlReference) def test_parse_uid_example(calendars): """Parse the UID type.""" calendar: Calendar = calendars.rfc_9253_examples event = calendar.events[1] links = event.links assert len(links) == 1 assert links[0].uid == "links-rfc-9253-section-8.2" assert links[0].LINKREL == "REFERENCE" assert links[0].FMTTYPE is None assert links[0].LABEL is None assert isinstance(links[0], vUid) def test_linkrel_is_always_quoted(): """Test that the LINKREL is always quoted.""" link = vUri("https://example.com", params={"LINKREL": "SOURCE"}) event = Event() event.links = [link] ical = event.to_ical().decode() assert 'LINKREL="SOURCE"' in ical collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/test_refid.py000066400000000000000000000013731524134033600326300ustar00rootroot00000000000000"""Test the REFID property.""" import pytest from icalendar import Component @pytest.fixture def component(): """Return a basic component for testing.""" return Component() def test_new_component_with_concept(): """We can use new with just a string.""" component = Component.new(refids="refid-1") assert len(component.refids) == 1 assert component.refids[0] == "refid-1" assert isinstance(component.refids[0], str) def test_delete_refids(component: Component): del component.refids component.refids = ["asd"] del component.refids assert component.refids == [] def test_delete_refids_with_none(component: Component): component.refids = ["123"] component.refids = None assert component.refids == [] collective-icalendar-138c845/src/icalendar/tests/rfc_9253_icalendar_relationships/test_related_to.py000066400000000000000000000053721524134033600336640ustar00rootroot00000000000000"""The RALATED-TO property is extended by :rfc:`9253`. These are the tests for related_to. """ import pytest from icalendar import ( RELTYPE, Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, TimezoneDaylight, TimezoneStandard, Todo, vText, vUid, vUri, ) def test_examples_from_rfc_9253(calendars): """Section 9.1 provides some examples.""" calendar: Calendar = calendars.rfc_9253_related_to event = calendar.events[0] related_to = event.related_to assert len(related_to) == 3 assert ( related_to[0].ical_value == "jsmith.part7.19960817T083000.xyzMail@example.com" ) assert related_to[0].RELTYPE == "PARENT", "default" assert isinstance(related_to[0], vText) assert related_to[1].uid == "19960401-080045-4000F192713-0052@example.com" assert related_to[1].ical_value == "19960401-080045-4000F192713-0052@example.com" assert related_to[1].RELTYPE == "PARENT", "default" assert isinstance(related_to[1], vUid) assert ( related_to[2].uri == "https://example.com/caldav/user/jb/cal/19960401-080045-4000F192713.ics" ) assert ( related_to[2].ical_value == "https://example.com/caldav/user/jb/cal/19960401-080045-4000F192713.ics" ) assert related_to[2].RELTYPE == RELTYPE.STARTTOFINISH assert isinstance(related_to[2], vUri) @pytest.fixture( params=( Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, TimezoneDaylight, TimezoneStandard, Todo, ) ) def related_to_component_class(request): """The classes to test. rfc:`9253`:: Conformance: RELATED-TO: This property can be specified zero or more times in any iCalendar component. """ return request.param @pytest.fixture def related_to_component(related_to_component_class): """An instance of the component class to test.""" return related_to_component_class() def test_no_related_to_by_default(related_to_component): """By default, components have no RELATED-TO properties.""" assert "RELATED-TO" not in related_to_component assert related_to_component.related_to == [] def test_strings_are_converted_to_the_default_value_type(related_to_component): """We set the URI. According to :rfc:`5545`, this should be vText. """ related_to_component.related_to = ["https://example.com"] assert related_to_component.related_to == [vText("https://example.com")] assert isinstance(related_to_component.related_to[0], vText) assert related_to_component.related_to[0].params.value is None, ( "We do not need to specify the default value." ) collective-icalendar-138c845/src/icalendar/tests/test_bom_calendar.py000066400000000000000000000002411524134033600257340ustar00rootroot00000000000000def test_bom_calendar(calendars): assert calendars.bom_calendar.walk("VCALENDAR"), ( "Unable to parse a calendar starting with an Unicode BOM" ) collective-icalendar-138c845/src/icalendar/tests/test_categories_value_with_colon.py000066400000000000000000000041241524134033600311000ustar00rootroot00000000000000"""A CATEGORIES value may contain an unescaped colon. ``TEXT`` values do not escape ``:``, so a category such as a URI is valid. The value separator must be the first colon outside a quoted parameter section, not the last colon on the line. """ from icalendar import Calendar from icalendar.parser import Contentline def _categories(value: str) -> list[str]: ics = ( "BEGIN:VCALENDAR\r\n" "BEGIN:VEVENT\r\n" "UID:1\r\n" f"CATEGORIES:{value}\r\n" "END:VEVENT\r\n" "END:VCALENDAR\r\n" ) event = Calendar.from_ical(ics).walk("VEVENT")[0] return [str(c) for c in event["CATEGORIES"].cats] def test_category_value_containing_a_colon_is_not_truncated(): assert _categories("CONFIDENTIAL,http://example.com/tag") == [ "CONFIDENTIAL", "http://example.com/tag", ] def test_plain_categories_still_split_on_commas(): assert _categories("HOME,WORK") == ["HOME", "WORK"] def test_escaped_comma_in_category_is_preserved(): assert _categories(r"Work,Personal\,Urgent") == ["Work", "Personal,Urgent"] def test_colon_in_a_quoted_parameter_is_not_the_value_separator(): ics = ( "BEGIN:VCALENDAR\r\n" "BEGIN:VEVENT\r\n" "UID:1\r\n" 'CATEGORIES;ALTREP="http://p":A,B\r\n' "END:VEVENT\r\n" "END:VCALENDAR\r\n" ) event = Calendar.from_ical(ics).walk("VEVENT")[0] assert [str(c) for c in event["CATEGORIES"].cats] == ["A", "B"] assert event["CATEGORIES"].params["ALTREP"] == "http://p" def test_value_separator_index_skips_quoted_colon(): line = Contentline('CATEGORIES;ALTREP="http://p":A,B') assert line.value_separator_index() == line.index(":A,B") def test_value_separator_index_does_not_treat_backslash_as_escape(): # Backslash has no special meaning in the parameter grammar (RFC 5545 # §3.1), so the first colon after ``X-A=\`` is the value separator and # the category is the single ``:``. line = Contentline(r"CATEGORIES;X-A=\::") assert line.value_separator_index() == 16 assert str(line)[line.value_separator_index() + 1 :] == ":" collective-icalendar-138c845/src/icalendar/tests/test_cli_tool.py000066400000000000000000000051571524134033600251450ustar00rootroot00000000000000import unittest from datetime import datetime from zoneinfo import ZoneInfo from icalendar import Calendar, cli INPUT = """ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT SUMMARY:Test Summary ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com ATTENDEE:attendee2@test.test COMMENT:Comment DTSTART;TZID=Europe/Warsaw:20220820T103400 DTEND;TZID=Europe/Warsaw:20220820T113400 LOCATION:New Amsterdam, 1000 Sunrise Test Street DESCRIPTION: Test Description END:VEVENT BEGIN:VEVENT ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com SUMMARY:Test summary DTSTART;TZID=Europe/Warsaw:20220820T200000 DTEND;TZID=Europe/Warsaw:20220820T203000 LOCATION:New Amsterdam, 1010 Test Street DESCRIPTION:Test Description\\nThis one is multiline END:VEVENT BEGIN:VEVENT UID:1 SUMMARY:TEST DTSTART:20220511 DURATION:P5D END:VEVENT END:VCALENDAR """ def local_datetime(dt): return ( datetime.strptime(dt, "%Y%m%dT%H%M%S") .replace(tzinfo=ZoneInfo("Europe/Warsaw")) .astimezone() .strftime("%c") ) # datetimes are displayed in the local timezone, so we cannot just hardcode them firststart = local_datetime("20220820T103400") firstend = local_datetime("20220820T113400") secondstart = local_datetime("20220820T200000") secondend = local_datetime("20220820T203000") PROPER_OUTPUT = f""" Organizer: organizer Attendees: attendee1 attendee2 Summary : Test Summary Starts : {firststart} End : {firstend} Duration : 1:00:00 Location : New Amsterdam, 1000 Sunrise Test Street Comment : Comment Description: Test Description Organizer: organizer Attendees: attendee1 Summary : Test summary Starts : {secondstart} End : {secondend} Duration : 0:30:00 Location : New Amsterdam, 1010 Test Street Comment : Description: Test Description This one is multiline Organizer: Attendees: Summary : TEST Starts : Wed May 11 00:00:00 2022 End : Mon May 16 00:00:00 2022 Duration : 5 days, 0:00:00 Location : Comment : Description: """ # noqa: W291, W293 class CLIToolTest(unittest.TestCase): def test_output_is_proper(self): self.maxDiff = None calendar = Calendar.from_ical(INPUT) output = "" for event in calendar.walk("vevent"): output += cli.view(event) + "\n\n" assert output == PROPER_OUTPUT if __name__ == "__main__": unittest.main() collective-icalendar-138c845/src/icalendar/tests/test_compatibility.py000066400000000000000000000012101524134033600261740ustar00rootroot00000000000000"""Tests for compatibility helpers.""" import pytest from icalendar.compatibility import deprecate_for_version_8 def test_deprecate_for_version_8_warns_and_delegates() -> None: """The wrapper should warn while preserving the wrapped behavior.""" def _join_parts(left: str, right: str = "") -> str: return f"{left}-{right}" deprecated = deprecate_for_version_8(_join_parts) with pytest.warns( DeprecationWarning, match="join_parts is deprecated and will be removed in icalendar 8", ): assert deprecated("left", right="right") == "left-right" assert deprecated.__name__ == "join_parts" collective-icalendar-138c845/src/icalendar/tests/test_component_register.py000066400000000000000000000052511524134033600272420ustar00rootroot00000000000000"""Tests for Component.register() method.""" import pytest from icalendar import Component class TestComponentRegister: """Test Component.register() method.""" def test_register_custom_component(self): """Register a custom component and use it.""" class XMyComponent(Component): name = "X-MY-COMPONENT" def custom_method(self): return "custom_value" Component.register(XMyComponent) # Get the registered component retrieved = Component.get_component_class("X-MY-COMPONENT") assert retrieved is XMyComponent # Create instance comp = retrieved() assert comp.name == "X-MY-COMPONENT" assert comp.custom_method() == "custom_value" def test_register_component_without_name_raises(self): """Registering component without name attribute raises ValueError.""" class BadComponent(Component): pass # No name attribute with pytest.raises(ValueError, match="must have a 'name' attribute"): Component.register(BadComponent) def test_register_component_with_none_name_raises(self): """Registering component with None name raises ValueError.""" class BadComponent(Component): name = None with pytest.raises(ValueError, match="must have a 'name' attribute"): Component.register(BadComponent) def test_register_duplicate_component_raises(self): """Registering same component name twice raises ValueError.""" class XComponent1(Component): name = "X-DUPLICATE" class XComponent2(Component): name = "X-DUPLICATE" Component.register(XComponent1) with pytest.raises(ValueError, match="already registered"): Component.register(XComponent2) def test_register_same_class_twice_allowed(self): """Registering the exact same class twice is allowed (idempotent).""" class XComponent(Component): name = "X-IDEMPOTENT" Component.register(XComponent) Component.register(XComponent) # Should not raise def test_parse_uses_registered_component(self): """Parsing uses the registered custom component class.""" class XVendor(Component): name = "X-VENDOR" def get_vendor_id(self): return self.get("X-VENDOR-ID") Component.register(XVendor) ical_data = b"""BEGIN:X-VENDOR UID:123 X-VENDOR-ID:vendor-1 END:X-VENDOR """ comp = Component.from_ical(ical_data) assert isinstance(comp, XVendor) assert comp.name == "X-VENDOR" assert str(comp.get_vendor_id()) == "vendor-1" collective-icalendar-138c845/src/icalendar/tests/test_components_break_on_bad_ics.py000066400000000000000000000034221524134033600310230ustar00rootroot00000000000000import pytest def test_ignore_exceptions_on_broken_events_issue_104(events): """Issue #104 - line parsing error in a VEVENT (which has ignore_exceptions). Should mark the event broken but not raise an exception. https://github.com/collective/icalendar/issues/104 """ assert events.issue_104_mark_events_broken.errors == [ (None, "Content line could not be parsed into parts: 'X': Invalid content line") ] def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars): """Issue #104 - line parsing error in a VCALENDAR (which doesn't have ignore_exceptions). Should raise an exception. """ with pytest.raises(ValueError): calendars.issue_104_broken_calendar def test_rdate_dosent_become_none_on_invalid_input_issue_464(events): """Issue #464 - [BUG] RDATE can become None if value is invalid https://github.com/collective/icalendar/issues/464 """ event = events.issue_464_invalid_rdate # After VALUE parameter fix, the assertion checks for the full value string # in the error message, making the test more flexible assert len(event.errors) == 1 assert event.errors[0][0] == "RDATE" assert "Expected period format" in event.errors[0][1] assert "199709T180000Z/PT5H30M" in event.errors[0][1] assert b"RDATE:None" not in event.to_ical() @pytest.mark.parametrize( "calendar_name", [ "big_bad_calendar", "small_bad_calendar", "multiple_calendar_components", "pr_480_summary_with_colon", ], ) def test_error_message_doesnt_get_too_big(calendars, calendar_name): with pytest.raises(ValueError) as exception: calendars[calendar_name] # Ignore part before first : for the test. assert len(str(exception).split(": ", 1)[1]) <= 100 collective-icalendar-138c845/src/icalendar/tests/test_consolidation_attr_functions.py000066400000000000000000000323311524134033600313220ustar00rootroot00000000000000"""Tests for consolidated attribute functions in icalendar.attr module.""" from datetime import date, datetime, timedelta import pytest from icalendar import Alarm, Event, Todo from icalendar.attr import ( get_duration_property, get_end_property, get_start_end_duration_with_validation, get_start_property, set_duration_with_locking, set_end_with_locking, set_start_with_locking, ) from icalendar.error import IncompleteComponent, InvalidCalendar class TestConsolidatedSetters: """Test consolidated setter functions from icalendar.attr.""" def test_set_start_with_locking_event(self) -> None: """Test set_start_with_locking for Event components.""" event = Event() event.add("UID", "test-event") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 14, 0)) # Keep end locked set_start_with_locking(event, datetime(2026, 1, 1, 10, 0), "end", "DTEND") assert event.start == datetime(2026, 1, 1, 10, 0) assert event.end == datetime(2026, 1, 1, 14, 0) assert event.duration == timedelta(hours=4) def test_set_start_with_locking_todo(self) -> None: """Test set_start_with_locking for Todo components.""" todo = Todo() todo.add("UID", "test-todo") todo.add("DTSTART", datetime(2026, 1, 1, 12, 0)) todo.add("DUE", datetime(2026, 1, 1, 14, 0)) # Keep end locked set_start_with_locking(todo, datetime(2026, 1, 1, 10, 0), "end", "DUE") assert todo.start == datetime(2026, 1, 1, 10, 0) assert todo.end == datetime(2026, 1, 1, 14, 0) assert todo.duration == timedelta(hours=4) def test_set_end_with_locking_event(self) -> None: """Test set_end_with_locking for Event components.""" event = Event() event.add("UID", "test-event") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 14, 0)) # Keep start locked set_end_with_locking(event, datetime(2026, 1, 1, 16, 0), "start", "DTEND") assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 16, 0) assert event.duration == timedelta(hours=4) def test_set_end_with_locking_todo(self) -> None: """Test set_end_with_locking for Todo components.""" todo = Todo() todo.add("UID", "test-todo") todo.add("DTSTART", datetime(2026, 1, 1, 12, 0)) todo.add("DUE", datetime(2026, 1, 1, 14, 0)) # Keep start locked set_end_with_locking(todo, datetime(2026, 1, 1, 16, 0), "start", "DUE") assert todo.start == datetime(2026, 1, 1, 12, 0) assert todo.end == datetime(2026, 1, 1, 16, 0) assert todo.duration == timedelta(hours=4) def test_set_duration_with_locking_event(self) -> None: """Test set_duration_with_locking for Event components.""" event = Event() event.add("UID", "test-event") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 14, 0)) # Set duration with start locked set_duration_with_locking(event, timedelta(hours=3), "start", "DTEND") assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 15, 0) assert event.duration == timedelta(hours=3) def test_set_duration_with_locking_todo(self) -> None: """Test set_duration_with_locking for Todo components.""" todo = Todo() todo.add("UID", "test-todo") todo.add("DTSTART", datetime(2026, 1, 1, 12, 0)) todo.add("DUE", datetime(2026, 1, 1, 14, 0)) # Set duration with start locked set_duration_with_locking(todo, timedelta(hours=3), "start", "DUE") assert todo.start == datetime(2026, 1, 1, 12, 0) assert todo.end == datetime(2026, 1, 1, 15, 0) assert todo.duration == timedelta(hours=3) def test_auto_detect_property_locking(self) -> None: """Test auto-detection of property locking.""" event = Event() event.add("UID", "test-event") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DURATION", timedelta(hours=2)) # Auto-detect should choose duration set_start_with_locking( event, datetime(2026, 1, 1, 10, 0), None, "DTEND", ) assert event.start == datetime(2026, 1, 1, 10, 0) assert event.duration == timedelta(hours=2) assert event.end == datetime(2026, 1, 1, 12, 0) def test_error_handling_consolidated_functions(self) -> None: """Test error handling in consolidated functions.""" event = Event() event.add("UID", "test-event") # Invalid locked parameter with pytest.raises(ValueError, match="locked must be"): set_start_with_locking( event, datetime(2026, 1, 1, 12, 0), "invalid", "DTEND", ) with pytest.raises(ValueError, match="locked must be"): set_end_with_locking( event, datetime(2026, 1, 1, 12, 0), "invalid", "DTEND", ) with pytest.raises(ValueError, match="locked must be"): set_duration_with_locking( event, timedelta(hours=1), "invalid", "DTEND", ) def test_consolidated_functions_maintain_identical_behavior(self) -> None: """Test that consolidated functions maintain identical behavior.""" # Create identical events event1 = Event() event1.add("UID", "test-event-1") event1.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event1.add("DTEND", datetime(2026, 1, 1, 14, 0)) event2 = Event() event2.add("UID", "test-event-2") event2.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event2.add("DTEND", datetime(2026, 1, 1, 14, 0)) # Use consolidated function vs direct property set_start_with_locking( event1, datetime(2026, 1, 1, 10, 0), "end", "DTEND", ) event2.set_start(datetime(2026, 1, 1, 10, 0), locked="end") # Results should be identical assert event1.start == event2.start assert event1.end == event2.end assert event1.duration == event2.duration class TestConsolidatedPropertyGetters: """Test consolidated property getter functions.""" def test_get_duration_property_with_duration_set(self) -> None: """Test get_duration_property when DURATION is explicitly set.""" event = Event() event.add("UID", "test-duration") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DURATION", timedelta(hours=1)) duration = get_duration_property(event) assert duration == timedelta(hours=1) def test_get_duration_property_calculated(self) -> None: """Test get_duration_property when calculated from start/end.""" event = Event() event.add("UID", "test-calculated") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 15, 0)) duration = get_duration_property(event) assert duration == timedelta(hours=3) def test_get_start_property_success(self) -> None: """Test get_start_property success case.""" event = Event() event.add("UID", "test-start") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) start = get_start_property(event) assert start == datetime(2026, 1, 1, 12, 0) def test_get_start_property_missing(self) -> None: """Test get_start_property when DTSTART is missing.""" event = Event() event.add("UID", "test-start") with pytest.raises(IncompleteComponent, match="No DTSTART given"): get_start_property(event) def test_get_end_property_with_dtend(self) -> None: """Test get_end_property when DTEND is set.""" event = Event() event.add("UID", "test-end") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 15, 0)) end = get_end_property(event, "DTEND") assert end == datetime(2026, 1, 1, 15, 0) def test_get_end_property_with_duration(self) -> None: """Test get_end_property when DURATION is set.""" event = Event() event.add("UID", "test-duration-end") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DURATION", timedelta(hours=3)) end = get_end_property(event, "DTEND") assert end == datetime(2026, 1, 1, 15, 0) def test_get_end_property_default_behavior(self) -> None: """Test get_end_property default behavior.""" event = Event() event.add("UID", "test-default") event.add("DTSTART", date(2026, 1, 1)) end = get_end_property(event, "DTEND") assert end == date(2026, 1, 2) event2 = Event() event2.add("UID", "test-default-2") event2.add("DTSTART", datetime(2026, 1, 1, 12, 0)) end2 = get_end_property(event2, "DTEND") assert end2 == datetime(2026, 1, 1, 12, 0) def test_get_start_end_duration_validation_success(self) -> None: """Test get_start_end_duration_with_validation success case.""" event = Event() event.add("UID", "test-validation") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 15, 0)) start, end, duration = get_start_end_duration_with_validation( event, "DTSTART", "DTEND", "VEVENT", ) assert start == datetime(2026, 1, 1, 12, 0) assert end == datetime(2026, 1, 1, 15, 0) assert duration is None def test_get_start_end_duration_validation_errors(self) -> None: """Test get_start_end_duration_with_validation error cases.""" # Test both DTEND and DURATION event = Event() event.add("UID", "test-both") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) event.add("DTEND", datetime(2026, 1, 1, 15, 0)) event.add("DURATION", timedelta(hours=2)) with pytest.raises(InvalidCalendar, match="Only one of DTEND and DURATION"): get_start_end_duration_with_validation( event, "DTSTART", "DTEND", "VEVENT", ) # Test invalid duration for date DTSTART event2 = Event() event2.add("UID", "test-date-duration") event2.add("DTSTART", date(2026, 1, 1)) event2.add("DURATION", timedelta(hours=2)) with pytest.raises(InvalidCalendar, match="DURATION must be of days"): get_start_end_duration_with_validation( event2, "DTSTART", "DTEND", "VEVENT", ) # Test type mismatch event3 = Event() event3.add("UID", "test-type-mismatch") event3.add("DTSTART", date(2026, 1, 1)) event3.add("DTEND", datetime(2026, 1, 1, 15, 0)) with pytest.raises(InvalidCalendar, match="must be of the same type"): get_start_end_duration_with_validation( event3, "DTSTART", "DTEND", "VEVENT", ) class TestUidVendorFallback: """Test UID access for vendor-specific UID properties.""" def test_uid_uses_evolution_alarm_uid_fallback(self) -> None: """Use X-EVOLUTION-ALARM-UID when UID is absent.""" alarm = Alarm() alarm.add("X-EVOLUTION-ALARM-UID", "evo-123") assert alarm.uid == "evo-123" def test_uid_prefers_canonical_uid(self) -> None: """Prefer UID over vendor-specific UID properties.""" alarm = Alarm() alarm.add("UID", "canonical-123") alarm.add("X-EVOLUTION-ALARM-UID", "evo-123") assert alarm.uid == "canonical-123" def test_uid_defaults_to_empty_string(self) -> None: """Return the empty-string default when no UID is present.""" alarm = Alarm() assert alarm.uid == "" def test_setting_uid_to_none_removes_evolution_alarm_uid(self) -> None: """Setting uid to None removes canonical and vendor UID properties.""" alarm = Alarm() alarm.add("UID", "canonical-123") alarm.add("X-EVOLUTION-ALARM-UID", "evo-123") alarm.uid = None assert "UID" not in alarm assert "X-EVOLUTION-ALARM-UID" not in alarm def test_deleting_uid_removes_evolution_alarm_uid(self) -> None: """Deleting uid removes canonical and vendor UID properties.""" alarm = Alarm() alarm.add("UID", "canonical-123") alarm.add("X-EVOLUTION-ALARM-UID", "evo-123") del alarm.uid assert "UID" not in alarm assert "X-EVOLUTION-ALARM-UID" not in alarm def test_alarm_uid_keeps_x_alarmuid_fallback(self) -> None: """Keep the existing single-string other_name fallback behavior.""" alarm = Alarm() alarm.add("X-ALARMUID", "alarm-123") assert alarm.uid == "alarm-123" del alarm.uid assert "X-ALARMUID" not in alarm collective-icalendar-138c845/src/icalendar/tests/test_create_release.sh000077500000000000000000000013111524134033600262550ustar00rootroot00000000000000#!/bin/sh # # Create a release file and test it. # set -e cd "`dirname \"$0\"`" cd "../../.." rm -rf dist pip3 install build python3 -m build archive=`echo dist/icalendar-*.tar.gz` if ! [ -f "$archive" ]; then echo "ERROR: Cannot find distribution archive '$archive'." exit 1 fi if tar -tf "$archive" | grep -q 'fuzzing/'; then echo "ERROR: Fuzzing files are included in the release." echo " See https://github.com/collective/icalendar/pull/569" exit 1 fi if ! tar -tf "$archive" | grep -q '/docs/'; then echo "ERROR: The documentation is not included in the release, but should be." echo " See https://github.com/collective/icalendar/issues/712" exit 1 fi echo "Checks passed." collective-icalendar-138c845/src/icalendar/tests/test_custom_components.py000066400000000000000000000246251524134033600271210ustar00rootroot00000000000000"""Tests for custom component parsing and handling. Custom components include: - X-Components (vendor-specific, starting with X-) - IANA-Components (registered but not in RFC 5545) - Any unrecognized component names According to RFC 5545: - Applications MUST ignore custom components they don't recognize - Applications SHOULD NOT silently drop components (data loss prevention) icalendar preserves all custom components through dynamic component creation. """ from icalendar import Calendar, Component, Event from icalendar.cal.component_factory import ComponentFactory class TestComponentFactory: """Test ComponentFactory's dynamic component creation.""" def test_create_custom_component_class(self): """Factory creates component classes for unknown names.""" factory = ComponentFactory() my_component_class = factory.get_component_class("My-Component") assert my_component_class.name == "MY-COMPONENT" assert my_component_class.__name__ == "MyComponent" def test_custom_component_inherits_from_component(self): """Dynamically created components inherit from Component.""" factory = ComponentFactory() custom_class = factory.get_component_class("X-CUSTOM") assert issubclass(custom_class, Component) def test_factory_caches_component_classes(self): """Factory returns same class for repeated requests.""" factory = ComponentFactory() class1 = factory.get_component_class("X-VENDOR") class2 = factory.get_component_class("X-VENDOR") assert class1 is class2 def test_sanitizes_component_names(self): """Factory sanitizes non-alphanumeric characters in names.""" factory = ComponentFactory() component_class = factory.get_component_class("X-MY-COMPONENT") # Hyphens removed from class name, but preserved in .name assert component_class.__name__ == "XMYCOMPONENT" assert component_class.name == "X-MY-COMPONENT" class TestCustomComponentWithComponentFromIcal: """Test parsing custom components using Component.from_ical().""" def test_parse_standalone_custom_component(self, calendars): """Parse a standalone custom component (issue #178).""" # MYCOMP is not a standard RFC 5545 component calendar = calendars.issue_178_component_with_invalid_name_represented # Should parse without error assert calendar.to_ical() == calendar.raw_ics def test_custom_component_preserves_name(self): """Custom component name is preserved.""" ics_data = b"BEGIN:X-MYCOMPONENT\r\nEND:X-MYCOMPONENT\r\n" comp = Component.from_ical(ics_data) assert comp.name == "X-MYCOMPONENT" def test_custom_component_with_properties(self): """Custom components can have properties.""" ics_data = b"""BEGIN:X-VENDOR SUMMARY:Vendor Component X-VENDOR-PROP:proprietary value UID:vendor-123 END:X-VENDOR """ comp = Component.from_ical(ics_data) assert comp.name == "X-VENDOR" assert comp["SUMMARY"] == "Vendor Component" assert comp["X-VENDOR-PROP"] == "proprietary value" assert comp["UID"] == "vendor-123" def test_custom_component_round_trip(self): """Custom components survive round-trip parsing.""" original = b"BEGIN:X-TEST\r\nSUMMARY:Test\r\nEND:X-TEST\r\n" comp = Component.from_ical(original) regenerated = comp.to_ical() assert b"BEGIN:X-TEST" in regenerated assert b"SUMMARY:Test" in regenerated assert b"END:X-TEST" in regenerated def test_custom_component_can_contain_standard_components(self, calendars): """Custom component can contain standard components (issue #178).""" # MYCOMPTOO contains a VEVENT calendar = calendars.issue_178_custom_component_contains_other assert calendar.to_ical() == calendar.raw_ics def test_parse_custom_component_containing_event(self): """Parse custom component containing VEVENT.""" ics_data = b"""BEGIN:X-CONTAINER SUMMARY:Container BEGIN:VEVENT UID:event-1 DTSTART:20240101T120000Z SUMMARY:Event inside custom END:VEVENT END:X-CONTAINER """ container = Component.from_ical(ics_data) assert container.name == "X-CONTAINER" assert len(container.subcomponents) == 1 event = container.subcomponents[0] assert event.name == "VEVENT" assert event["UID"] == "event-1" assert event["SUMMARY"] == "Event inside custom" class TestCustomComponentWithCalendarFromIcal: """Test parsing custom components using Calendar.from_ical().""" def test_calendar_with_custom_component(self, calendars): """Calendar can contain custom components (issue #178).""" # VCALENDAR with UNKNOWN component calendar = calendars.issue_178_custom_component_inside_other assert calendar.to_ical() == calendar.raw_ics def test_parse_calendar_containing_custom_component(self): """Parse VCALENDAR containing custom component.""" ics_data = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//EN BEGIN:X-CUSTOM UID:custom-1 SUMMARY:Custom component in calendar END:X-CUSTOM END:VCALENDAR """ cal = Calendar.from_ical(ics_data) assert len(cal.subcomponents) == 1 custom = cal.subcomponents[0] assert custom.name == "X-CUSTOM" assert custom["UID"] == "custom-1" def test_calendar_with_mixed_standard_and_custom_components(self): """Calendar can mix standard and custom components.""" ics_data = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//EN BEGIN:VEVENT UID:event-1 DTSTART:20240101T120000Z SUMMARY:Standard event END:VEVENT BEGIN:X-VENDOR UID:vendor-1 X-PROP:value END:X-VENDOR BEGIN:VTODO UID:todo-1 SUMMARY:Standard todo END:VTODO END:VCALENDAR """ cal = Calendar.from_ical(ics_data) assert len(cal.subcomponents) == 3 # Verify component types names = [comp.name for comp in cal.subcomponents] assert "VEVENT" in names assert "X-VENDOR" in names assert "VTODO" in names def test_custom_component_round_trip_in_calendar(self): """Custom components in calendar survive round-trip.""" original = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//EN BEGIN:X-MYAPP X-SETTING:value END:X-MYAPP END:VCALENDAR """ cal = Calendar.from_ical(original) regenerated = cal.to_ical() assert b"BEGIN:X-MYAPP" in regenerated assert b"X-SETTING:value" in regenerated class TestCustomComponentAPI: """Test that custom components work with standard Component API.""" def test_add_property_to_custom_component(self): """Can add properties to custom component.""" comp = Component(name="X-TEST") comp.add("summary", "Test Summary") comp.add("x-custom", "Custom Value") assert comp["SUMMARY"] == "Test Summary" assert comp["X-CUSTOM"] == "Custom Value" def test_add_subcomponent_to_custom_component(self): """Can add subcomponents to custom component.""" container = Component(name="X-CONTAINER") event = Event() event.add("uid", "123") event.add("summary", "Test Event") container.add_component(event) assert len(container.subcomponents) == 1 assert container.subcomponents[0].name == "VEVENT" def test_custom_component_iteration(self): """Can iterate over properties in custom component.""" comp = Component(name="X-TEST") comp.add("prop1", "value1") comp.add("prop2", "value2") # property_items() includes BEGIN, END, and all component properties props = list(comp.property_items()) prop_names = [name for name, value in props] assert "PROP1" in prop_names assert "PROP2" in prop_names assert "BEGIN" in prop_names assert "END" in prop_names def test_custom_component_get_method(self): """Can use get() method on custom component.""" comp = Component(name="X-TEST") comp.add("existing", "value") assert comp.get("EXISTING") == "value" assert comp.get("NONEXISTENT") is None assert comp.get("NONEXISTENT", "default") == "default" class TestRFC5545Compliance: """Test RFC 5545 compliance for custom components.""" def test_preserves_x_components(self): """X-components (vendor-specific) are preserved.""" ics_data = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:X-VENDOR-COMP X-VENDOR-PROP:proprietary END:X-VENDOR-COMP END:VCALENDAR """ cal = Calendar.from_ical(ics_data) regenerated = cal.to_ical() # X-component preserved assert b"X-VENDOR-COMP" in regenerated assert b"X-VENDOR-PROP" in regenerated def test_preserves_iana_components(self): """IANA-registered components are preserved.""" # Hypothetical IANA component (not X- prefixed) ics_data = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:VFUTURE UID:future-1 SUMMARY:Future IANA component END:VFUTURE END:VCALENDAR """ cal = Calendar.from_ical(ics_data) regenerated = cal.to_ical() # IANA component preserved assert b"VFUTURE" in regenerated assert b"Future IANA component" in regenerated def test_does_not_silently_drop_custom_components(self): """Custom components are never silently dropped (RFC 5545).""" ics_data = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:X-UNKNOWN1 UID:1 END:X-UNKNOWN1 BEGIN:X-UNKNOWN2 UID:2 END:X-UNKNOWN2 BEGIN:UNKNOWN3 UID:3 END:UNKNOWN3 END:VCALENDAR """ cal = Calendar.from_ical(ics_data) # All 3 custom components preserved assert len(cal.subcomponents) == 3 component_names = {comp.name for comp in cal.subcomponents} assert component_names == {"X-UNKNOWN1", "X-UNKNOWN2", "UNKNOWN3"} def test_nested_custom_components_preserved(self): """Nested custom components are fully preserved.""" ics_data = b"""BEGIN:X-OUTER SUMMARY:Outer BEGIN:X-INNER SUMMARY:Inner END:X-INNER END:X-OUTER """ outer = Component.from_ical(ics_data) assert outer.name == "X-OUTER" assert len(outer.subcomponents) == 1 inner = outer.subcomponents[0] assert inner.name == "X-INNER" assert inner["SUMMARY"] == "Inner" # Round-trip preserves nesting regenerated = outer.to_ical() reparsed = Component.from_ical(regenerated) assert len(reparsed.subcomponents) == 1 assert reparsed.subcomponents[0].name == "X-INNER" collective-icalendar-138c845/src/icalendar/tests/test_docstrings.py000066400000000000000000000141241524134033600255120ustar00rootroot00000000000000"""Inspect all docstring section headings. ``ALLOWED_HEADINGS`` is a set of allowed headings. It may be amended as needed. ``KNOWN_BAD_HEADINGS`` is a set of known bad headings. As docstrings are fixed, their headings may be removed from this set. See https://github.com/collective/icalendar/issues/1481 """ import inspect import re import pytest import icalendar ALLOWED_HEADINGS = { "Attributes", "Parameters", "Returns", "Raises", "Example", "Examples", "See also", "Attention", "Caution", "Danger", "Hint", "Important", "Note", "Tip", "Todo", } KNOWN_BAD_HEADINGS = { "Availability", "Available", "BUSYTYPE", "CLASS", "CUTYPE", "Calendar", "Conference", "Event", "FBTYPE", "FreeBusy", "Image", "Journal", "PARTSTAT", "Parameters", "RANGE", "RELATED", "RELTYPE", "ROLE", "STATUS", "TRANSP", "Todo", "VALUE", "vCalAddress", "vDate", "vDatetime", "vDuration", "vFloat", "vGeo", "vInt", "vMonth", "vRecur", "vTime", "vUTCOffset", "vUri", } def get_public_objects(): objs = [] # Test on the main public module for now to establish baseline for name, obj in inspect.getmembers(icalendar): if name.startswith("_"): continue if inspect.isclass(obj) or inspect.isfunction(obj): objs.append(obj) return objs def _obj_id(obj): """Return the name of the object for parametrize IDs.""" return obj.__name__ @pytest.mark.parametrize("obj", get_public_objects(), ids=_obj_id) def test_docstring_headings_are_valid(obj): """ Identify docstring section headings that are not one of the allowed types. """ if obj.__name__ in KNOWN_BAD_HEADINGS: pytest.xfail( f"'{obj.__module__}.{obj.__qualname__}'\n" " Invalid docstring section heading. See:\n" " https://icalendar.readthedocs.io/en/stable/contribute/documentation/style-guide.html#docstring-structure" ) doc = inspect.getdoc(obj) if not doc: return # Match standard docstring headings (Word(s) followed by a colon) headings = re.findall( r"^ *([A-Z][A-Za-z]+(?: [A-Za-z]+)*):(?:\n|$)", doc, re.MULTILINE ) for heading in headings: assert heading in ALLOWED_HEADINGS, ( f"Invalid docstring heading '{heading}' found in '{obj.__name__}'. " f"Allowed headings are: {', '.join(sorted(ALLOWED_HEADINGS))}" ) def get_all_public_methods_and_functions(): objs = [] # Test on the main public module for now to establish baseline for obj in get_public_objects(): if inspect.isfunction(obj): objs.append(obj) elif inspect.isclass(obj): for name, method in inspect.getmembers(obj, predicate=inspect.isfunction): if name.startswith("_") and name != "__init__": continue objs.append(method) return objs def _method_id(obj): return obj.__qualname__ @pytest.mark.parametrize("obj", get_all_public_methods_and_functions(), ids=_method_id) def test_docstring_has_parameters_section(obj): """ Identify docstrings that lack a Parameters section when the Python object's signature accepts parameters other than self. """ try: sig = inspect.signature(obj) except ValueError: return params = [p for n, p in sig.parameters.items() if n not in ("self", "cls")] if not params: return doc = inspect.getdoc(obj) if not doc: return if not re.search(r"^Parameters:$", doc, re.MULTILINE): pytest.xfail( f"'{obj.__module__}.{obj.__qualname__}' lacks a Parameters section." ) def check_returns_and_raises(obj): try: source = inspect.getsource(obj) source = inspect.cleandoc(source) import ast tree = ast.parse(source) except (OSError, TypeError, SyntaxError): return False, False has_returns = False has_raises = False for node in ast.walk(tree): if isinstance(node, ast.Return) and node.value is not None: has_returns = True if isinstance(node, ast.Raise): has_raises = True return has_returns, has_raises @pytest.mark.parametrize("obj", get_all_public_methods_and_functions(), ids=_method_id) def test_docstring_has_returns_section(obj): """ Identify docstrings for Python objects that return something, but lack a Returns section heading. """ has_ret, _ = check_returns_and_raises(obj) if not has_ret: return doc = inspect.getdoc(obj) if not doc: return if not re.search(r"^Returns:$", doc, re.MULTILINE): pytest.xfail( f"'{obj.__module__}.{obj.__qualname__}' returns a value but lacks a Returns section." ) @pytest.mark.parametrize("obj", get_all_public_methods_and_functions(), ids=_method_id) def test_docstring_has_raises_section(obj): """ Identify docstrings for Python objects that raise something, but lack a Raises section heading. """ _, has_raise = check_returns_and_raises(obj) if not has_raise: return doc = inspect.getdoc(obj) if not doc: return if not re.search(r"^Raises:$", doc, re.MULTILINE): pytest.xfail( f"'{obj.__module__}.{obj.__qualname__}' raises an exception but lacks a Raises section." ) @pytest.mark.parametrize("obj", get_all_public_methods_and_functions(), ids=_method_id) def test_docstring_has_examples_section(obj): """ Identify docstrings with both a summary and description, but lack an Example. """ doc = inspect.getdoc(obj) if not doc: return lines = doc.strip().split("\n") if len(lines) < 3 or lines[1].strip() != "": # Doesn't have a clear summary and description separated by a blank line return if not re.search(r"^(Example|Examples):$", doc, re.MULTILINE): pytest.xfail( f"'{obj.__module__}.{obj.__qualname__}' has a summary and description but lacks an Example section." ) collective-icalendar-138c845/src/icalendar/tests/test_encoding.py000066400000000000000000000070251524134033600251230ustar00rootroot00000000000000import datetime import pytest @pytest.mark.parametrize( ("field", "expected_value"), [ ("PRODID", "-//Plönë.org//NONSGML plone.app.event//EN"), ("X-WR-CALDESC", "test non ascii: äöü ÄÖÜ €"), ], ) def test_calendar_from_ical_respects_unicode(field, expected_value, calendars): cal = calendars.calendar_with_unicode assert cal[field].to_ical().decode("utf-8") == expected_value @pytest.mark.parametrize( ("test_input", "field", "expected_value"), [ ("event_with_unicode_fields", "SUMMARY", "Non-ASCII Test: ÄÖÜ äöü €"), ( "event_with_unicode_fields", "DESCRIPTION", "icalendar should be able to handle non-ascii: €äüöÄÜÖ.", ), ("event_with_unicode_fields", "LOCATION", "Tribstrül"), # Non-unicode characters in summary # https://github.com/collective/icalendar/issues/64 ("issue_64_event_with_non_ascii_summary", "SUMMARY", "åäö"), # Unicode characters in summary ("issue_64_event_with_ascii_summary", "SUMMARY", "abcdef"), ], ) def test_event_from_ical_respects_unicode(test_input, field, expected_value, events): event = events[test_input] assert event[field].to_ical().decode("utf-8") == expected_value @pytest.mark.parametrize( ("test_input", "expected_output"), [ # chokes on umlauts in ORGANIZER # https://github.com/collective/icalendar/issues/101 ("issue_101_icalendar_chokes_on_umlauts_in_organizer", "acme, ädmin"), ("event_with_unicode_organizer", "Джон До�"), ], ) def test_events_parameter_unicoded(events, test_input, expected_output): assert events[test_input]["ORGANIZER"].params["CN"] == expected_output def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ start = calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.walk( "VEVENT" )[0].decoded("DTSTART") expected = in_timezone(datetime.datetime(2017, 5, 11, 13, 30), "America/Sao_Paulo") assert not calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.errors assert start == expected def test_parses_timezone_with_non_ascii_tzid_issue_237(timezones): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ assert timezones.issue_237_brazilia_standard["tzid"] == "(UTC-03:00) Brasília" @pytest.mark.parametrize("timezone_name", ["standard", "daylight"]) def test_parses_timezone_with_non_ascii_tzname_issue_273(timezones, timezone_name): """Issue #237 - Fail to parse timezone with non-ascii TZID see https://github.com/collective/icalendar/issues/237 """ assert ( timezones.issue_237_brazilia_standard.walk(timezone_name)[0]["TZNAME"] == f"Brasília {timezone_name}" ) def test_broken_property(calendars): """ Test if error messages are encoded properly. """ for event in calendars.broken_ical.walk("vevent"): assert len(event.errors) == 1, "Not the right amount of errors." error = event.errors[0][1] assert error.startswith("Content line could not be parsed into parts") def test_apple_xlocation(calendars): """ Test if we support base64 encoded binary data in parameter values. """ for event in calendars.x_location.walk("vevent"): assert len(event.errors) == 0, "Got too many errors" collective-icalendar-138c845/src/icalendar/tests/test_enums.py000066400000000000000000000035721524134033600244670ustar00rootroot00000000000000"""Test the enums for values.""" import pickle import pytest import icalendar from icalendar import enums ENUMS = [name for name in dir(enums) if name.isupper()] @pytest.fixture(params=ENUMS) def enum_name(request): """The name of an enum""" return request.param @pytest.fixture def enum(enum_name): """An enum.""" return getattr(enums, enum_name) def test_all_enums_are_exported(enum_name): """All enums should be exported.""" assert enum_name in enums.__all__ def test_all_enums_are_public(enum_name): """All enums should be exported.""" assert enum_name in icalendar.__all__, f"icalendar.__all__ is missing {enum_name}" def test_enum_has_description(enum): """We should have a docstring.""" assert "Description:" in enum.__doc__ def test_enum_can_be_pickled(): """Enum members retain their identity after a pickle round trip.""" related = enums.RELATED.START assert pickle.loads(pickle.dumps(related)) is related # noqa: S301 def test_value_enum_includes_binary(): """BINARY is an RFC 5545 (section 3.2.20) value type and must be present.""" assert enums.VALUE.BINARY == "BINARY" def test_value_enum_matches_rfc_5545_value_types(): """VALUE lists every value type defined in RFC 5545, section 3.2.20.""" rfc_5545_value_types = { "BINARY", "BOOLEAN", "CAL-ADDRESS", "DATE", "DATE-TIME", "DURATION", "FLOAT", "INTEGER", "PERIOD", "RECUR", "TEXT", "TIME", "URI", "UTC-OFFSET", } assert {member.value for member in enums.VALUE} == rfc_5545_value_types def test_vbinary_default_value_is_a_value_enum_member(): """vBinary serializes as VALUE=BINARY, so BINARY must exist in VALUE.""" from icalendar.prop import vBinary assert vBinary.default_value in {member.value for member in enums.VALUE} collective-icalendar-138c845/src/icalendar/tests/test_equality.py000066400000000000000000000243051524134033600251720ustar00rootroot00000000000000"""Test the equality and inequality of components.""" import contextlib import copy try: from pytz import UnknownTimeZoneError except ImportError: class UnknownTimeZoneError(Exception): pass from datetime import date, datetime, time, timedelta from time import time as time_time import pytest from icalendar import ( Calendar, Component, Event, vBinary, vBoolean, vCategory, vDate, vDatetime, vDDDLists, vDDDTypes, vDuration, vGeo, vPeriod, vRecur, vText, vTime, ) def assert_equal(actual_value, expected_value): """Make sure both values are equal""" assert actual_value == expected_value assert expected_value == actual_value def assert_not_equal(actual_value, expected_value): """Make sure both values are not equal""" assert actual_value != expected_value assert expected_value != actual_value def test_parsed_calendars_are_equal_if_parsed_again(source_file, tzp): """Ensure that a calendar equals the same calendar. source -> calendar -> ics -> same calendar """ copy_of_calendar = source_file.__class__.from_ical(source_file.to_ical()) assert_equal(copy_of_calendar, source_file) def test_parsed_calendars_are_equal_if_parsed_again_jcal(source_file, tzp): """Ensure that a calendar equals the same calendar. source -> calendar -> jcal -> same calendar """ if source_file.source_file == "issue_464_invalid_rdate.ics": pytest.skip( "Invalid RDATE falls back to vBroken, which can't round-trip to jCal." ) copy_of_calendar = Component.from_jcal(source_file.to_jcal()) assert_equal(copy_of_calendar, source_file) def test_parsed_calendars_are_equal_if_from_same_source(ics_file, tzp): """Ensure that a calendar equals the same calendar. ics -> calendar ics -> same calendar """ cal1 = ics_file.__class__.from_ical(ics_file.raw_ics) cal2 = ics_file.__class__.from_ical(ics_file.raw_ics) assert_equal(cal1, cal2) def test_parsed_calendars_are_equal_if_from_same_source_jcal(jcal_file, tzp): """Ensure that a calendar equals the same calendar. jcal -> calendar jcal -> same calendar """ cal1 = Component.from_jcal(jcal_file.raw_jcal) cal2 = Component.from_jcal(jcal_file.raw_jcal) assert_equal(cal1, cal2) def test_copies_are_equal(source_file, tzp): """Ensure that copies are equal.""" copy1 = source_file.copy() copy1.subcomponents = source_file.subcomponents copy2 = source_file.copy() copy2.subcomponents = source_file.subcomponents[:] assert_equal(copy1, copy2) assert_equal(copy1, source_file) assert_equal(copy2, source_file) def test_calendar_equality_order(): """Ensure inequality of calendars when subcomponents are strict subset, regardless of order.""" calendar1 = Calendar() calendar2 = Calendar() event1 = Event.new(start=datetime(2021, 1, 1, 12, 30, 0)) event2 = Event() calendar1.subcomponents = [event1, event2] calendar2.subcomponents = [event1, event1] assert_not_equal(calendar1, calendar2) def test_component_equality_order(): """Ensure inequality of components when subcomponents are strict subset, regardless of order.""" component1 = Component() component2 = Component() event1 = Event.new(start=datetime(2021, 1, 1, 12, 30, 0)) event2 = Event() component1.subcomponents = [event1, event2] component2.subcomponents = [event1, event1] assert_not_equal(component1, component2) def _nested_component(depth: int) -> Event: """Build a component nested depth subcomponents deep.""" root = node = Event() for _ in range(depth): child = Event() node.add_component(child) node = child return root def test_deeply_nested_equality_at_depth_50(): """Regression test for GHSA-cv84-9p8j-fj68. Comparing deeply nested components used to recurse and re-compare both directions at every level, which is exponential in the nesting depth: a depth-30 structure took minutes. The fix walks an explicit stack and compares each pair of nested components exactly once, which is linear. We try at a few depths and fail when the first one takes too long. We could try just one large depth, but if there really is a regression, the tests could take ten or a hundred times as long. At depth 20 the comparison already takes 2.85 seconds on my fast laptop when using the original implementation. We give each depth more time. The main thing is that this should not be exponential. """ for seconds in range(1, 6): depth = 10 * seconds # equality and its commutative counterpart, see #1224 t1 = time_time() assert_equal(_nested_component(depth), _nested_component(depth)) t2 = time_time() total = t2 - t1 assert total < seconds, ( f"equal nested {depth=} takes too long: {total} > {seconds}" ) # A difference at the deepest end gets detected left = _nested_component(depth) right = _nested_component(depth) deepest = right while deepest.subcomponents: deepest = deepest.subcomponents[0] deepest.add("summary", "different") t3 = time_time() assert_not_equal(left, right) t4 = time_time() total = t4 - t3 assert total < seconds, ( f"unequal nested {depth=} takes too long: {total} > {seconds}" ) def test_copy_does_not_copy_subcomponents(calendars, tzp): """If we copy the subcomponents, assumptions around copies will be broken.""" assert calendars.timezoned.subcomponents assert not calendars.timezoned.copy().subcomponents def test_deep_copies_are_equal(source_file, tzp): """Ensure that deep copies are equal. Ignore errors when a custom time zone is used. This is still covered by the parsing test. """ if ( source_file.source_file == "issue_722_timezone_transition_ambiguity.ics" and tzp.uses_zoneinfo() ): pytest.skip("This test fails for now.") with contextlib.suppress(UnknownTimeZoneError): assert_equal(copy.deepcopy(source_file), copy.deepcopy(source_file)) with contextlib.suppress(UnknownTimeZoneError): assert_equal(copy.deepcopy(source_file), source_file) def test_vGeo(): """Check the equality of vGeo.""" assert_equal(vGeo(("100", "12.33")), vGeo(("100.00", "12.330"))) assert_not_equal(vGeo(("100", "12.331")), vGeo(("100.00", "12.330"))) assert_not_equal(vGeo(("10", "12.33")), vGeo(("100.00", "12.330"))) def test_vBinary(): assert_equal(vBinary("asd"), vBinary("asd")) assert_not_equal(vBinary("asdf"), vBinary("asd")) def test_vBoolean(): assert_equal(vBoolean.from_ical("TRUE"), vBoolean.from_ical("TRUE")) assert_equal(vBoolean.from_ical("FALSE"), vBoolean.from_ical("FALSE")) assert_not_equal(vBoolean.from_ical("TRUE"), vBoolean.from_ical("FALSE")) def test_vCategory(): assert_equal(vCategory("HELLO"), vCategory("HELLO")) assert_equal(vCategory(["a", "b"]), vCategory(["a", "b"])) assert_not_equal(vCategory(["a", "b"]), vCategory(["a", "b", "c"])) def test_vText(): assert_equal(vText("HELLO"), vText("HELLO")) assert_not_equal(vText("HELLO1"), vText("HELLO")) @pytest.mark.parametrize( ("v_type", "v1", "v2"), [ (vDatetime, datetime(2023, 11, 1, 10, 11), datetime(2023, 11, 1, 10, 10)), (vDate, date(2023, 11, 1), date(2023, 10, 31)), (vDuration, timedelta(3, 11, 1), timedelta(23, 10, 31)), ( vPeriod, (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(23, 10, 31)), ), ( vPeriod, (datetime(2023, 11, 1, 10, 1), timedelta(3, 11, 1)), (datetime(2023, 11, 1, 10, 11), timedelta(3, 11, 1)), ), ( vPeriod, (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 3)), (datetime(2023, 11, 1, 10, 1), datetime(2023, 11, 1, 10, 2)), ), (vTime, time(10, 10, 10), time(10, 10, 11)), ], ) @pytest.mark.parametrize("eq", ["==", "!="]) @pytest.mark.parametrize("cls1", [0, 1]) @pytest.mark.parametrize("cls2", [0, 1]) @pytest.mark.parametrize("hash", [lambda x: x, hash]) def test_vDDDTypes_and_others(v_type, v1, v2, cls1, cls2, eq, hash): # noqa: A002 """Check equality and inequality.""" t1 = (v_type, vDDDTypes)[cls1] t2 = (v_type, vDDDTypes)[cls2] if eq == "==": assert hash(v1) == hash(v1) assert hash(t1(v1)) == hash(t2(v1)) assert hash(t1(v1)) == hash(t2(v1)) else: assert hash(v1) != hash(v2) assert hash(t1(v1)) != hash(t2(v2)) def test_repr_vDDDTypes(): assert "vDDDTypes" in repr(vDDDTypes(timedelta(3, 11, 1))) vDDDLists_examples = [ # noqa: N816 vDDDLists([]), vDDDLists([datetime(2023, 11, 1, 10, 1)]), vDDDLists([datetime(2023, 11, 1, 10, 1), date(2023, 11, 1)]), ] @pytest.mark.parametrize("l1", vDDDLists_examples) @pytest.mark.parametrize("l2", vDDDLists_examples) def test_vDDDLists(l1, l2): """Check the equality functions of vDDDLists.""" equal = l1 is l2 l2 = copy.deepcopy(l2) assert equal == (l1 == l2) assert equal != (l1 != l2) @pytest.mark.parametrize("index", [1, 2]) def test_rfc_7265_equivalence_of_example_from_appendix(calendars, index): """jcal and ical should be the same""" ical = calendars[f"rfc_7265_appendix_example_{index}_ical"] jcal = calendars[f"rfc_7265_appendix_example_{index}_jcal"] assert_equal(ical.to_jcal(), jcal.to_jcal()) assert_equal(ical, jcal) @pytest.mark.parametrize( ("v1", "v2", "equal"), [ ({}, {}, True), ({"BYDAY": "1SU"}, {}, False), ({"BYDAY": "1SU"}, {"BYDAY": "1SU"}, True), ({"BYDAY": "1SU"}, {"BYDAY": ["1SU"]}, True), ({"byday": "1SU"}, {"BYDAY": "1SU"}, True), ({"byday": "1SU", "count": 1}, {"BYDAY": "1SU"}, False), ({"count": 1}, {"BYDAY": "1SU"}, False), ], ) def test_v_recur_equal(v1, v2, equal): """Check the equality functions of vRecur.""" recur1 = vRecur(v1) recur2 = vRecur(v2) if equal: assert_equal(recur1, recur2) else: assert_not_equal(recur1, recur2) collective-icalendar-138c845/src/icalendar/tests/test_error_tolerant_parsing.py000066400000000000000000000163101524134033600301160ustar00rootroot00000000000000"""Tests for error-tolerant parsing of property values.""" import pytest from icalendar.error import BrokenCalendarProperty from icalendar.prop import vBroken, vDDDLists, vDDDTypes, vRecur, vText def test_properties_parsed_immediately(calendars): """Verify properties are parsed immediately during from_ical().""" cal = calendars.issue_1081_event_with_rrule event = cal.walk("VEVENT")[0] rrule = event["RRULE"] assert isinstance(rrule, vRecur) assert rrule["FREQ"] == ["DAILY"] assert rrule["COUNT"] == [10] def test_params_accessible(calendars): """Verify params are accessible on parsed properties.""" cal = calendars.issue_1081_tzid_param event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert dtstart.params["TZID"] == "America/New_York" def test_broken_property_vbroken_fallback(calendars): """Verify broken properties fall back to vBroken.""" cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] # Should fall back to vBroken (which is vText subclass) assert isinstance(dtstart, vBroken) assert isinstance(dtstart, vText) # vBroken inherits from vText assert str(dtstart) == "INVALID-DATE" assert dtstart.property_name == "DTSTART" assert dtstart.expected_type is not None assert dtstart.parse_error is not None assert len(event.errors) == 1 assert event.errors[0][0] == "DTSTART" def test_broken_property_doesnt_block_others(calendars): """Verify one broken property doesn't prevent accessing others.""" cal = calendars.issue_1081_invalid_start_valid_end event = cal.walk("VEVENT")[0] dtend = event["DTEND"] assert isinstance(dtend, vDDDTypes) assert dtend.dt.year == 2025 dtstart = event["DTSTART"] assert isinstance(dtstart, vBroken) summary = event["SUMMARY"] assert str(summary) == "Test Event" def test_property_to_ical(calendars): """Verify to_ical() works correctly with properties.""" cal = calendars.issue_1081_event_with_rrule output = cal.to_ical() assert b"DTSTART:20250101T100000Z" in output assert b"RRULE:FREQ=DAILY;COUNT=10" in output assert b"SUMMARY:Test Event" in output def test_multiple_properties_in_list(calendars): """Verify lists of properties work correctly.""" cal = calendars.issue_1081_list_of_properties event = cal.walk("VEVENT")[0] exdates = event["EXDATE"] assert isinstance(exdates, list) assert len(exdates) == 2 assert all(isinstance(dt, vDDDLists) for dt in exdates) def test_property_with_tzid(calendars): """Verify TZID parameter is handled correctly.""" cal = calendars.issue_1081_tzid_param event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert isinstance(dtstart, vDDDTypes) assert dtstart.dt.year == 2025 assert dtstart.dt.month == 1 assert dtstart.dt.day == 1 def test_freebusy_comma_separated_values(calendars): """Verify FREEBUSY comma-separated values work with error-tolerant parsing.""" cal = calendars.issue_1081_freebusy_comma_separated fb = cal.walk("VFREEBUSY")[0] freebusy = fb["FREEBUSY"] assert isinstance(freebusy, list) assert len(freebusy) == 2 def test_empty_rdate_workaround(calendars): """Verify empty RDATE values are handled correctly.""" cal = calendars.issue_1081_empty_rdate event = cal.walk("VEVENT")[0] assert "RDATE" not in event def test_vbroken_repr(calendars): """Verify vBroken repr includes metadata.""" cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert "vBroken" in repr(dtstart) assert "INVALID-DATE" in repr(dtstart) assert "expected_type" in repr(dtstart) assert "property_name" in repr(dtstart) def test_ignore_exceptions_flag_respected(calendars): """Verify ignore_exceptions flag behavior.""" # Event has ignore_exceptions=True cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert isinstance(dtstart, vBroken) assert len(event.errors) > 0 def test_property_str(calendars): """Verify __str__ works.""" cal = calendars.issue_1081_with_summary event = cal.walk("VEVENT")[0] summary = event["SUMMARY"] assert str(summary) == "Test Event" def test_vbroken_metadata(calendars): """Verify vBroken stores parse error metadata.""" cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert isinstance(dtstart, vBroken) assert dtstart.property_name == "DTSTART" assert dtstart.expected_type is not None assert dtstart.parse_error is not None def test_errors_populated_immediately(calendars): """Verify errors available after from_ical without property access.""" cal = calendars.issue_1081_invalid_start_and_end event = cal.walk("VEVENT")[0] # Errors should be populated WITHOUT accessing properties assert len(event.errors) == 2 assert event.errors[0][0] == "DTSTART" assert event.errors[1][0] == "DTEND" def test_typeerror_handling_in_tolerant_mode(calendars): """Verify TypeError exceptions are caught in error-tolerant mode. This test addresses the concern raised in PR #1044 review that TypeError exceptions should be handled consistently with ValueError. """ # Event has ignore_exceptions=True (error-tolerant) # Using RRULE with invalid type that triggers TypeError during parsing cal = calendars.issue_1081_invalid_rrule_freq event = cal.walk("VEVENT")[0] rrule = event["RRULE"] assert isinstance(rrule, vBroken) assert len(event.errors) >= 1 assert any("RRULE" in error[0] for error in event.errors) def test_parse_error_is_exception_object(calendars): """Verify parse_error stores the actual exception, not a string.""" cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] assert isinstance(dtstart, vBroken) assert isinstance(dtstart.parse_error, Exception) @pytest.fixture def broken(): return vBroken( "INVALID", property_name="DTSTART", expected_type="vDDDTypes", parse_error=ValueError("bad value"), ) def test_vbroken_getattr_raises_broken_calendar_property(broken): """Verify accessing missing attributes raises BrokenCalendarProperty.""" with pytest.raises(BrokenCalendarProperty) as exc_info: broken.dt assert exc_info.value.__cause__ is broken.parse_error assert "DTSTART" in str(exc_info.value) assert "vDDDTypes" in str(exc_info.value) def test_vbroken_getattr_preserves_existing_attributes(broken): """Verify normal attributes still work on vBroken.""" assert broken.property_name == "DTSTART" assert broken.expected_type == "vDDDTypes" assert isinstance(broken.parse_error, ValueError) assert isinstance(broken.params, dict) assert broken.encoding is not None def test_event_dtstart_raises_broken_calendar_property(calendars): """Verify event.DTSTART raises BrokenCalendarProperty for broken values.""" cal = calendars.broken_dtstart event = cal.walk("VEVENT")[0] with pytest.raises(BrokenCalendarProperty) as exc_info: event.DTSTART assert exc_info.value.__cause__ is not None collective-icalendar-138c845/src/icalendar/tests/test_examples.py000066400000000000000000000062171524134033600251550ustar00rootroot00000000000000"""tests ensuring that *the* way of doing things works""" import datetime import pytest from icalendar import ( Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, Todo, ) def test_creating_calendar_with_unicode_fields(calendars, utc): """create a calendar with events that contain unicode characters in their fields""" cal = Calendar() cal.add("PRODID", "-//Plönë.org//NONSGML plone.app.event//EN") cal.add("VERSION", "2.0") cal.add("X-WR-CALNAME", "äöü ÄÖÜ €") cal.add("X-WR-CALDESC", "test non ascii: äöü ÄÖÜ €") cal.add("X-WR-RELCALID", "12345") event = Event() event.add("DTSTART", datetime.datetime(2010, 10, 10, 10, 0, 0, tzinfo=utc)) event.add("DTEND", datetime.datetime(2010, 10, 10, 12, 0, 0, tzinfo=utc)) event.add("CREATED", datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=utc)) event.add("UID", "123456") event.add("SUMMARY", "Non-ASCII Test: ÄÖÜ äöü €") event.add("DESCRIPTION", "icalendar should be able to de/serialize non-ascii.") event.add("LOCATION", "Tribstrül") cal.add_component(event) # test_create_event_simple event1 = Event() event1.add("DTSTART", datetime.datetime(2010, 10, 10, 0, 0, 0, tzinfo=utc)) event1.add("SUMMARY", "åäö") cal.add_component(event1) # test_unicode_parameter_name # test for issue #80 https://github.com/collective/icalendar/issues/80 event2 = Event() event2.add("DESCRIPTION", "äöüßÄÖÜ") cal.add_component(event2) assert cal.to_ical() == calendars.created_calendar_with_unicode_fields.raw_ics @pytest.mark.parametrize( ("component", "example"), [ (Alarm, "example"), (Alarm, "rfc_5545_end"), (Alarm, "start_date"), (Calendar, "example"), (Calendar, "example.ics"), (Event, "event_with_rsvp"), (FreeBusy, "example"), (Journal, "example"), (Timezone, "pacific_fiji"), (Todo, "example"), ], ) def test_component_has_examples( alarms, calendars, component, events, example, freebusy, journals, timezones, todos, tzp, ): """Check that the examples function works.""" mapping = { Calendar: calendars, Event: events, Timezone: timezones, Todo: todos, Alarm: alarms, Journal: journals, FreeBusy: freebusy, } example_component = component.example(example) expected_component = mapping[component][example] assert example_component == expected_component def test_invalid_examples_lists_the_others(): """We need a bit of guidance here.""" with pytest.raises(ValueError) as e: Calendar.example("does not exist") assert "example.ics" in str(e.value) @pytest.mark.parametrize( "component", [ Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Timezone, Todo, ], ) def test_default_example(component): """Check that we have a default example.""" example = component.example() assert isinstance(example, component) collective-icalendar-138c845/src/icalendar/tests/test_free_busy_duration.py000066400000000000000000000015011524134033600272160ustar00rootroot00000000000000from datetime import datetime, timedelta import pytest from icalendar import FreeBusy from icalendar.error import InvalidCalendar def test_freebusy_duration_without_start_or_end(): """Duration should be None if DTSTART or DTEND is missing.""" freebusy = FreeBusy.new() assert freebusy.duration is None def test_freebusy_duration_with_start_and_end(): """Duration should be computed from DTSTART and DTEND.""" start = datetime(2026, 4, 1, 10, 0) end = datetime(2026, 4, 1, 12, 30) freebusy = FreeBusy.new(start=start, end=end) assert freebusy.duration == timedelta(hours=2, minutes=30) def test_freebusy_invalid_date_order(): start = datetime(2026, 3, 20, 15, 0) end = datetime(2026, 3, 20, 13, 0) with pytest.raises(InvalidCalendar): FreeBusy.new(start=start, end=end) collective-icalendar-138c845/src/icalendar/tests/test_funding_json.py000066400000000000000000000021011524134033600260060ustar00rootroot00000000000000"""Test the funding.json validity.""" import json from pathlib import Path import pytest HERE = Path(__file__).parent REPOSITORY_ROOT = HERE.parent.parent.parent FUNDING_JSON_PATH = REPOSITORY_ROOT / "funding.json" @pytest.fixture def funding_json(): """Load the funding.json file.""" return FUNDING_JSON_PATH.read_text(encoding="utf-8") @pytest.fixture def funding_data(funding_json): """Parse the funding.json file.""" return json.loads(funding_json) @pytest.mark.parametrize( "key_present", [ "$schema", "version", "entity", "projects", "funding", ], ) def test_some_validity_of_funding_json(funding_data, key_present): """Check that the funding.json file is valid JSON.""" assert key_present in funding_data def test_consistent_format(funding_json, funding_data): """Check that the funding.json file is consistently formatted.""" reformatted = json.dumps(funding_data, indent=4) + "\n" print(reformatted) assert funding_json == reformatted, "The file should be properly formatted." collective-icalendar-138c845/src/icalendar/tests/test_generate_windows_to_olson_mapping.py000066400000000000000000000061671524134033600323360ustar00rootroot00000000000000import io import json import runpy from pathlib import Path from urllib import request import pytest CLDR_SHA = "0123456789abcdef0123456789abcdef01234567" CLDR_DATE = "2026-07-27" COMMITS_URL = ( "https://api.github.com/repos/unicode-org/cldr/commits" "?path=common/supplemental/windowsZones.xml&sha=main&per_page=1" ) RAW_URL = ( "https://raw.githubusercontent.com/unicode-org/cldr/" f"{CLDR_SHA}/common/supplemental/windowsZones.xml" ) class Response(io.BytesIO): def __enter__(self): return self def __exit__(self, *args): self.close() class Output(io.StringIO): def close(self): """Keep generated content available after the writer context exits.""" def test_generator_pins_mapping_to_latest_cldr_commit(monkeypatch): """The generated mapping and its version use the same CLDR commit.""" requested_urls = [] def urlopen(url): requested_urls.append(url) if url == COMMITS_URL: return Response( json.dumps( [ { "sha": CLDR_SHA, "commit": {"committer": {"date": f"{CLDR_DATE}T12:00:00Z"}}, } ] ).encode() ) if url == RAW_URL: return Response( b"" b'' b"" ) raise AssertionError(f"Unexpected URL: {url}") output = Output() def open_output(_path, _mode): return output monkeypatch.setattr(request, "urlopen", urlopen) monkeypatch.setattr(Path, "open", open_output) generator = Path(__file__).parents[3] / "generate_windows_to_olson_mapping.py" runpy.run_path(str(generator), run_name="__main__") generated = output.getvalue() assert requested_urls == [COMMITS_URL, RAW_URL] assert f'version = "{CLDR_SHA}"' in generated assert f'version_date = "{CLDR_DATE}"' in generated assert f"dated {CLDR_DATE}" in generated assert '"Test Standard Time": "Etc/Test"' in generated def test_generator_rejects_invalid_cldr_version(monkeypatch): """An invalid source version cannot be written into Python output.""" malicious_sha = '"; injected = True; version = "' def urlopen(url): if url == COMMITS_URL: return Response( json.dumps( [ { "sha": malicious_sha, "commit": {"committer": {"date": f"{CLDR_DATE}T12:00:00Z"}}, } ] ).encode() ) raise AssertionError(f"Unexpected URL: {url}") monkeypatch.setattr(request, "urlopen", urlopen) generator = Path(__file__).parents[3] / "generate_windows_to_olson_mapping.py" with pytest.raises(ValueError, match="Invalid CLDR commit SHA"): runpy.run_path(str(generator), run_name="__main__") collective-icalendar-138c845/src/icalendar/tests/test_ghsa_qjcq_q7h7_r74v_repeat_dos.py000066400000000000000000000051261524134033600312320ustar00rootroot00000000000000"""Tests for GHSA-qjcq-q7h7-r74v. A crafted REPEAT value on a VALARM component could exhaust memory or CPU. See https://github.com/collective/icalendar/security/advisories/GHSA-qjcq-q7h7-r74v """ from datetime import datetime, timedelta, timezone import pytest import icalendar.config as cfg from icalendar import Alarm from icalendar.alarms import Alarms from icalendar.prop.integer import vInt UTC = timezone.utc def _repeating_alarm(repeat: int) -> Alarm: a = Alarm() a.add("TRIGGER", datetime(2024, 1, 1, 12, 0, tzinfo=UTC)) a.add("DURATION", timedelta(minutes=5)) a.add("REPEAT", repeat) return a # vInt RFC 5545 range enforcement def test_vint_rejects_value_above_max(): with pytest.raises(ValueError): vInt.from_ical("2147483648") def test_vint_rejects_value_below_min(): with pytest.raises(ValueError): vInt.from_ical("-2147483649") def test_vint_accepts_boundary_values(): assert vInt.from_ical("2147483647") == 2147483647 assert vInt.from_ical("-2147483648") == -2147483648 def test_vint_jcal_rejects_out_of_range(): with pytest.raises(ValueError): vInt.from_jcal(["repeat", {}, "integer", 2147483648]) with pytest.raises(ValueError): vInt.parse_jcal_value(-2147483649) # Alarm.triggers cap (cal/alarm.py, available since 7.0.0) @pytest.mark.parametrize("repeat", [999_999_999, 10_001]) def test_alarm_triggers_capped(repeat, monkeypatch): monkeypatch.setattr(cfg, "MAX_ALARM_REPEAT", 10_000) alarm = _repeating_alarm(repeat) assert len(alarm.triggers.absolute) == 10_001 def test_alarm_triggers_small_repeat_unchanged(): assert len(_repeating_alarm(4).triggers.absolute) == 5 def test_alarm_triggers_negative_repeat(): assert len(_repeating_alarm(-1).triggers.absolute) == 1 def test_alarm_triggers_cap_disabled(monkeypatch): monkeypatch.setattr(cfg, "MAX_ALARM_REPEAT", -1) assert len(_repeating_alarm(11_000).triggers.absolute) == 11_001 # Alarms._repeat cap (alarms.py, available since 6.1.0) @pytest.mark.parametrize("repeat", [999_999_999, 10_001]) def test_alarms_times_capped(repeat, monkeypatch): monkeypatch.setattr(cfg, "MAX_ALARM_REPEAT", 10_000) assert len(Alarms(_repeating_alarm(repeat)).times) == 10_001 def test_alarms_times_small_repeat_unchanged(): assert len(Alarms(_repeating_alarm(4)).times) == 5 def test_alarms_times_negative_repeat(): assert len(Alarms(_repeating_alarm(-1)).times) == 1 def test_alarms_times_cap_disabled(monkeypatch): monkeypatch.setattr(cfg, "MAX_ALARM_REPEAT", -1) assert len(Alarms(_repeating_alarm(11_000)).times) == 11_001 collective-icalendar-138c845/src/icalendar/tests/test_ical_parsing_error.py000066400000000000000000000046411524134033600272020ustar00rootroot00000000000000from icalendar.error import ICalParsingError, InvalidCalendar def test_ical_parsing_error_is_invalid_calendar(): assert issubclass(ICalParsingError, InvalidCalendar) def test_ical_parsing_error_stores_attributes(): error = ICalParsingError( message="Invalid property", line="SUMMARY:Meeting", line_number=42, value={"property": "SUMMARY"}, ) assert error.message == "Invalid property" assert error.line == "SUMMARY:Meeting" assert error.line_number == 42 assert error.value == {"property": "SUMMARY"} def test_ical_parsing_error_required_parameters_only(): error = ICalParsingError("Invalid property") assert error.message == "Invalid property" assert error.line is None assert error.line_number is None assert error.value is None def test_ical_parsing_error_parameter_order(): error = ICalParsingError( "Invalid property", "SUMMARY:Meeting", 42, {"property": "SUMMARY"}, ) assert error.message == "Invalid property" assert error.line == "SUMMARY:Meeting" assert error.line_number == 42 assert error.value == {"property": "SUMMARY"} def test_ical_parsing_error_message_only(): error = ICalParsingError("Malformed date") assert str(error) == "Malformed date" def test_ical_parsing_error_message_with_value(): error = ICalParsingError( "Malformed date", value="20240399", ) assert str(error) == "Malformed date: '20240399'" def test_ical_parsing_error_message_with_line(): error = ICalParsingError( "Malformed date", line="DTSTART:20240399", ) assert str(error) == "Malformed date ('DTSTART:20240399')" def test_ical_parsing_error_message_with_line_number(): error = ICalParsingError( "Malformed date", line_number=42, ) assert str(error) == "Malformed date (line 42)" def test_ical_parsing_error_message_with_line_and_line_number(): error = ICalParsingError( "Malformed date", line="DTSTART:20240399", line_number=42, ) assert str(error) == "Malformed date (line 42: 'DTSTART:20240399')" def test_ical_parsing_error_message_with_all_context(): error = ICalParsingError( "Malformed date", line="DTSTART:20240399", line_number=42, value="20240399", ) assert str(error) == "Malformed date: '20240399' (line 42: 'DTSTART:20240399')" collective-icalendar-138c845/src/icalendar/tests/test_icalendar.py000066400000000000000000000247731524134033600252700ustar00rootroot00000000000000import unittest import pytest from icalendar.parser import ( Contentline, Contentlines, Parameters, _foldline, dquote, q_join, q_split, ) from icalendar.prop import vText class IcalendarTestCase(unittest.TestCase): def setUp(self): if not hasattr(self, "assertRaisesRegex"): self.assertRaisesRegex = self.assertRaisesRegexp def test_long_lines(self): c = Contentlines([Contentline("BEGIN:VEVENT")]) c.append(Contentline("".join("123456789 " * 10))) assert ( c.to_ical() == b"BEGIN:VEVENT\r\n123456789 123456789 123456789 123456789 " # noqa: ISC003 + b"123456789 123456789 123456789 1234\r\n 56789 123456789 " + b"123456789 \r\n" ) # from doctests # Notice that there is an extra empty string in the end of the content # lines. That is so they can be easily joined with: # '\r\n'.join(contentlines)) assert Contentlines.from_ical("A short line\r\n") == ["A short line", ""] assert Contentlines.from_ical("A faked\r\n long line\r\n") == [ "A faked long line", "", ] assert Contentlines.from_ical( "A faked\r\n long line\r\nAnd another lin\r\n\te that is folded\r\n" ) == ["A faked long line", "And another line that is folded", ""] def test_contentline_class(self): assert ( Contentline("Si meliora dies, ut vina, poemata reddit").to_ical() == b"Si meliora dies, ut vina, poemata reddit" ) # A long line gets folded c = Contentline("".join(["123456789 "] * 10)).to_ical() assert ( c == b"123456789 123456789 123456789 123456789 123456789 123456789 " # noqa: ISC003 + b"123456789 1234\r\n 56789 123456789 123456789 " ) # A folded line gets unfolded assert ( Contentline.from_ical(c) == "123456789 123456789 123456789 123456789 123456789 123456789 " # noqa: ISC003 + "123456789 123456789 123456789 123456789 " ) # https://tools.ietf.org/html/rfc5545#section-3.3.11 # An intentional formatted text line break MUST only be included in # a "TEXT" property value by representing the line break with the # character sequence of BACKSLASH, followed by a LATIN SMALL LETTER # N or a LATIN CAPITAL LETTER N, that is "\n" or "\N". # Newlines are not allowed in content lines self.assertRaises(AssertionError, Contentline, b"1234\r\n\r\n1234") assert Contentline("1234\\n\\n1234").to_ical() == b"1234\\n\\n1234" # We do not fold within a UTF-8 character c = Contentline( b"This line has a UTF-8 character where it should be " b"folded. Make sure it g\xc3\xabts folded before that " b"character." ) assert b"\xc3\xab" in c.to_ical() # Another test of the above c = Contentline(b"x" * 73 + b"\xc3\xab" + b"\\n " + b"y" * 10) assert c.to_ical().count(b"\xc3") == 1 # Don't fail if we fold a line that is exactly X times 74 characters # long Contentline("".join(["x"] * 148)).to_ical() # It can parse itself into parts, # which is a tuple of (name, params, vals) assert Contentline("dtstart:20050101T120000").parts() == ( "dtstart", Parameters({}), "20050101T120000", ) assert Contentline("dtstart;value=datetime:20050101T120000").parts() == ( "dtstart", Parameters({"VALUE": "datetime"}), "20050101T120000", ) c = Contentline( "ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com" ) assert c.parts() == ( "ATTENDEE", Parameters({"ROLE": "REQ-PARTICIPANT", "CN": "Max Rasmussen"}), "MAILTO:maxm@example.com", ) assert ( c.to_ical().decode("utf-8") == "ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com" ) # and back again # NOTE: we are quoting property values with spaces in it. parts = ( "ATTENDEE", Parameters({"ROLE": "REQ-PARTICIPANT", "CN": "Max Rasmussen"}), "MAILTO:maxm@example.com", ) assert ( Contentline.from_parts(*parts) == 'ATTENDEE;CN="Max Rasmussen";ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com' ) # and again parts = ("ATTENDEE", Parameters(), "MAILTO:maxm@example.com") assert Contentline.from_parts(*parts) == "ATTENDEE:MAILTO:maxm@example.com" # A value can also be any of the types defined in PropertyValues parts = ("ATTENDEE", Parameters(), vText("MAILTO:test@example.com")) assert Contentline.from_parts(*parts) == "ATTENDEE:MAILTO:test@example.com" # A value in UTF-8 parts = ("SUMMARY", Parameters(), vText("INternational char æ ø å")) assert Contentline.from_parts(*parts) == "SUMMARY:INternational char æ ø å" # A value can also be unicode parts = ("SUMMARY", Parameters(), vText("INternational char æ ø å")) assert Contentline.from_parts(*parts) == "SUMMARY:INternational char æ ø å" # Traversing could look like this. name, params, vals = c.parts() assert name == "ATTENDEE" assert vals == "MAILTO:maxm@example.com" assert sorted(params.items()) == sorted( [("ROLE", "REQ-PARTICIPANT"), ("CN", "Max Rasmussen")] ) # And the traditional failure with pytest.raises( ValueError, match="Content line could not be parsed into parts" ): Contentline("ATTENDEE;maxm@example.com").parts() # Another failure: with pytest.raises( ValueError, match="Content line could not be parsed into parts" ): Contentline(":maxm@example.com").parts() assert Contentline("key;param=:value").parts() == ( "key", Parameters({"PARAM": ""}), "value", ) assert Contentline('key;param="pvalue":value').parts() == ( "key", Parameters({"PARAM": "pvalue"}), "value", ) # Should bomb on missing param: with pytest.raises( ValueError, match="Content line could not be parsed into parts" ): Contentline.from_ical("k;:no param").parts() assert Contentline("key;param=pvalue:value", strict=False).parts() == ( "key", Parameters({"PARAM": "pvalue"}), "value", ) # If strict is set to True, uppercase param values that are not # double-quoted, this is because the spec says non-quoted params are # case-insensitive. assert Contentline("key;param=pvalue:value", strict=True).parts() == ( "key", Parameters({"PARAM": "PVALUE"}), "value", ) assert Contentline('key;param="pValue":value', strict=True).parts() == ( "key", Parameters({"PARAM": "pValue"}), "value", ) assert Contentline("REFRESH - INTERVAL; VALUE = DURATION:PT48H").parts() == ( "REFRESH-INTERVAL", Parameters({"VALUE": "DURATION"}), "PT48H", ) with pytest.raises( ValueError, match="Content line could not be parsed into parts" ): Contentline( "REFRESH - INTERVAL; VALUE = DURATION:PT48H", strict=True ).parts() contains_base64 = ( b"X-APPLE-STRUCTURED-LOCATION;" b'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";' b"X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;" b"X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;" b"X-TITLE=Heldenplatz:geo:48.206686,16.363235" ) assert Contentline(contains_base64, strict=True).parts() == ( "X-APPLE-STRUCTURED-LOCATION", Parameters( { "X-APPLE-RADIUS": "328.7978217977285", "X-ADDRESS": "Kaiserliche Hofburg, 1010 Wien", "X-APPLE-REFERENCEFRAME": "1", "X-TITLE": "HELDENPLATZ", "X-APPLE-MAPKIT-HANDLE": "CAESXQEZGR3QZXJYZWLJAA==", "VALUE": "URI", } ), "geo:48.206686,16.363235", ) def test_fold_line(self): assert _foldline("foo") == "foo" assert ( _foldline( "Lorem ipsum dolor sit amet, consectetur adipiscing " "elit. Vestibulum convallis imperdiet dui posuere.", ) == "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " "Vestibulum conval\r\n lis imperdiet dui posuere." ) # I don't really get this test # at least just but bytes in there # porting it to "run" under python 2 & 3 makes it not much better with pytest.raises(AssertionError): _foldline("привет".encode(), limit=3) assert _foldline("foobar", limit=4) == "foo\r\n bar" assert ( _foldline( "Lorem ipsum dolor sit amet, consectetur adipiscing elit" ". Vestibulum convallis imperdiet dui posuere.", ) == "Lorem ipsum dolor sit amet, consectetur adipiscing elit." # noqa: ISC003 + " Vestibulum conval\r\n lis imperdiet dui posuere." ) assert ( _foldline("DESCRIPTION:�БВГДЕ�ЖЗ�ЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ") == "DESCRIPTION:�БВГДЕ�ЖЗ�ЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭ\r\n ЮЯ" ) def test_value_double_quoting(self): assert dquote("Max") == "Max" assert dquote("Rasmussen, Max") == '"Rasmussen, Max"' assert dquote("name:value") == '"name:value"' def test_q_split(self): assert q_split('Max,Moller,"Rasmussen, Max"') == [ "Max", "Moller", '"Rasmussen, Max"', ] def test_q_split_bin(self): for s in ("X-SOMETHING=ABCDE==", ",,,"): for maxsplit in range(-1, 3): assert q_split(s, "=", maxsplit=maxsplit) == s.split("=", maxsplit) def test_q_join(self): assert ( q_join(["Max", "Moller", "Rasmussen, Max"]) == 'Max,Moller,"Rasmussen, Max"' ) collective-icalendar-138c845/src/icalendar/tests/test_image.py000066400000000000000000000147051524134033600244220ustar00rootroot00000000000000"""Test the image class to convert from and to binary data.""" from __future__ import annotations import base64 import pytest from icalendar import Calendar, Image, vBinary, vUri from icalendar.prop import vText, vUnknown TRANSPARENT_PIXEL = base64.b64decode("""iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA YAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jn m+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII=""") @pytest.fixture(params=["raw", "ical", "jcal"]) def issue_1561_images(calendars, request): """Return IMAGE properties before and after both serialization formats.""" calendar: Calendar = calendars.issue_1561_image_value if request.param == "ical": calendar = Calendar.from_ical(calendar.to_ical()) elif request.param == "jcal": calendar = Calendar.from_jcal(calendar.to_jcal()) return {component.uid: component["IMAGE"] for component in calendar.subcomponents} def test_image_value_parameter_selects_property_type(issue_1561_images): """IMAGE preserves its value type and parameters through serialization.""" uri = issue_1561_images["uri"] assert isinstance(uri, vUri) assert uri == "https://example.com/a.png" assert dict(uri.params) == {"VALUE": "URI"} binary = issue_1561_images["binary"] assert isinstance(binary, vBinary) assert binary.bytes == b"\x00\xff\x80" assert dict(binary.params) == {"ENCODING": "BASE64", "VALUE": "BINARY"} text = issue_1561_images["text"] assert isinstance(text, vText) assert text == "a;b,c" assert dict(text.params) == {"VALUE": "TEXT"} unknown = issue_1561_images["unknown"] assert isinstance(unknown, vUnknown) assert unknown == "https://example.com/b.png" assert dict(unknown.params) == {} @pytest.fixture def images(calendars) -> dict[str, Image]: """Return the images we get from the example calendars.""" calendar: Calendar = calendars.rfc_7986_image images: dict[str, Image] = {} for component in calendar.subcomponents: img = component.images[0] images[component.uid] = img return images @pytest.mark.parametrize( ("uid", "uri", "data", "fmt_type", "altrep", "display"), [ ( "uri-event", "http://example.com/images/party.png", None, "image/png", None, "BADGE", ), ( "uri-todo", "http://example.com/images/party.jpg", None, None, None, "BADGE", ), ( "uri-journal", "http://example.com/images/party.jpg", None, "image/jpg", None, "ICON", ), ( "data-event", None, TRANSPARENT_PIXEL, "image/png", None, None, ), ( "data-todo", None, TRANSPARENT_PIXEL, "image/png", "http://example.com/images/party.jpg", None, ), ( "data-journal", None, TRANSPARENT_PIXEL, "image/png", None, "BADGE", ), ], ) def test_image_parsing( images, uid, uri, data, fmt_type, altrep, display, ): """Test that the image property is parsed correctly.""" image = images[uid] assert image.uri == uri assert image.data == data assert image.fmttype == fmt_type assert image.altrep == altrep assert image.display == display def test_no_images(): """Test that an empty calendar has no images.""" calendar = Calendar() assert len(calendar.images) == 0 def test_create_image_invalid_type(): """Test that creating an image with invalid type raises TypeError.""" with pytest.raises(TypeError): Image.from_property_value("not a valid type") with pytest.raises(TypeError): Image.from_property_value(vText("not a valid type", params={"VALUE": "TEXT"})) with pytest.raises(TypeError): Image.from_property_value(vText("http://example.com/image.png")) def test_create_image_invalid_params(): """Test that creating an image with invalid params raises TypeError.""" class DummyValue: params = "invalid" with pytest.raises(TypeError): Image.from_property_value(DummyValue()) def test_create_with_vBinary(): """Test creating an Image from a vBinary property.""" vbin = vBinary(TRANSPARENT_PIXEL, params={"FMTTYPE": "image/png"}) image = Image.from_property_value(vbin) assert image.uri is None assert image.data == TRANSPARENT_PIXEL assert image.fmttype == "image/png" assert image.altrep is None assert image.display is None def test_create_with_vUri(): """Test creating an Image from a vUri property.""" uri = "http://example.com/image.png" vuri = vUri(uri, params={"FMTTYPE": "image/png", "DISPLAY": "BADGE"}) image = Image.from_property_value(vuri) assert image.uri == uri assert image.data is None assert image.fmttype == "image/png" assert image.altrep is None assert image.display == "BADGE" def test_create_image_with_vText_as_uri(): """Test that creating an image with vText and VALUE=URI works.""" img = Image.from_property_value( vText("http://example.com/image.png", params={"VALUE": "URI"}) ) assert img.uri == "http://example.com/image.png" assert img.data is None assert img.fmttype is None assert img.altrep is None assert img.display is None def test_create_image_with_vText_as_binary(): """Test that creating an image with vText and VALUE=BINARY works.""" b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii") img = Image.from_property_value(vText(b64data, params={"VALUE": "BINARY"})) assert img.uri is None assert img.data == TRANSPARENT_PIXEL assert img.fmttype is None assert img.altrep is None assert img.display is None def test_requires_uri_xor_binary(): """Test forbidden parameter combinations.""" with pytest.raises(ValueError): Image() with pytest.raises(ValueError): Image(b64data="", uri="http://example.com/image.png") def test_image_data_rejects_invalid_base64(): """Image.data raises ValueError for invalid base64, consistent with vBinary.from_ical.""" image = Image(b64data="not-valid-base64!!!") with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."): _ = image.data collective-icalendar-138c845/src/icalendar/tests/test_issue_1050_lazy_calendar.py000066400000000000000000000104211524134033600300140ustar00rootroot00000000000000"""Behavioral tests for :class:`~icalendar.cal.lazy.LazyCalendar`. Tests here cover serialization round-trips, property accessors, timezone handling, and ``with_uid()`` edge cases not covered by :mod:`test_issue_1050_lazy_parsing`. See :issue:`1050`. """ from datetime import datetime import pytest from icalendar import Calendar, Event def test_empty_calendar_has_no_subcomponents(lazy_calendars): """An empty calendar has no subcomponents after accessing them.""" cal = lazy_calendars.issue_1050_empty_calendar assert len(cal.subcomponents) == 0 assert not cal.is_lazy() def test_timezone_only_calendar_stays_lazy(lazy_calendars): """A calendar with only ``VTIMEZONE`` stays lazy after accessing timezones.""" cal = lazy_calendars.issue_1050_timezone_only_calendar assert cal.is_lazy() timezones = cal.walk("VTIMEZONE") assert len(timezones) == 1 assert cal.is_lazy() @pytest.mark.parametrize( ("prop", "summary"), [ ("todos", "Test Todo 1"), ("journals", "Test Journal 1"), ], ) def test_component_property(calendars, prop, summary): """Accessing ``.todos`` and ``.journals`` returns the correct components.""" cal = calendars.issue_1050_calendar_with_events_and_todos components = getattr(cal, prop) assert len(components) == 1 assert str(components[0]["SUMMARY"]) == summary def test_tzid_accessible_on_events(calendars): """Events carry their ``TZID`` parameter after parsing.""" cal = calendars.issue_1050_calendar_with_events_and_todos assert cal.events[0]["DTSTART"].params.get("TZID") == "America/New_York" def test_property_and_walk_access(calendars): """Mixing property access and ``walk()`` in any order returns correct counts.""" cal = calendars.issue_1050_calendar_with_events_and_todos assert len(cal.journals) == 1 assert len(cal.events) == 2 assert len(cal.walk("VTODO")) == 1 assert len(cal.todos) == 1 assert len(cal.events) == 2 def test_forward_timezone_reference(calendars): """``VTIMEZONE`` defined after its referencing event is still resolved.""" cal = calendars.issue_1050_forward_timezone_reference assert len(cal.events) == 1 dtstart = cal.events[0]["DTSTART"].dt assert dtstart.tzinfo is not None assert str(dtstart.tzinfo) == "Europe/Berlin" @pytest.mark.parametrize("access", ["none", "partial", "full"]) def test_to_ical_roundtrip(calendars, access): """Serializing at any parse state preserves all component counts.""" cal = calendars.issue_1050_calendar_with_events_and_todos if access == "partial": _ = cal.events elif access == "full": _ = cal.walk() output = cal.to_ical() cal2 = Calendar.from_ical(output) assert len(cal2.walk("VEVENT")) == 2 assert len(cal2.walk("VTODO")) == 1 assert len(cal2.walk("VJOURNAL")) == 1 assert len(cal2.walk("VTIMEZONE")) == 1 def test_add_component_appears_in_events(calendars): """``add_component()`` appends to the parsed event list.""" cal = calendars.issue_1050_simple_calendar new_event = Event() new_event.add("UID", "new-event@example.com") new_event.add("SUMMARY", "New Event") new_event.add("DTSTART", datetime(2025, 2, 1, 10, 0, 0)) cal.add_component(new_event) uids = sorted(str(e["UID"]) for e in cal.events) assert uids == ["new-event@example.com", "simple-event@example.com"] def test_multiple_calendars(calendars): """``from_ical(..., multiple=True)`` returns all calendars.""" cals = calendars.multiple.issue_1050_multiple_calendars assert len(cals) == 2 assert len(cals[0].events) == 1 assert len(cals[1].events) == 1 def test_uid_substring_in_description(calendars): """``with_uid()`` returns only the component whose ``UID`` matches exactly. :class:`~icalendar.parser.ical.lazy.LazySubcomponent` scans raw lines to decide whether to parse a component for ``with_uid()``. A ``DESCRIPTION`` that contains the target UID string causes that component to be parsed as a false positive — this is intentional (parse more, never miss) — but the returned list must contain only the exact match. """ cal = calendars.issue_1050_uid_in_description result = cal.with_uid("abc") assert len(result) == 1 assert result[0]["SUMMARY"] == "target" collective-icalendar-138c845/src/icalendar/tests/test_issue_1050_lazy_parsing.py000066400000000000000000000171211524134033600277120ustar00rootroot00000000000000"""Test lazy parsing of calendar components. See https://github.com/collective/icalendar/issues/1050 """ from typing import TYPE_CHECKING import pytest from icalendar import Alarm, Event, LazyCalendar from icalendar.parser.ical.lazy import LazySubcomponent if TYPE_CHECKING: from icalendar import Calendar def assert_is_lazy_calendar(calendar: LazyCalendar) -> None: """Assert that the calendar is a lazy calendar.""" assert isinstance(calendar, LazyCalendar) assert calendar._subcomponents.is_lazy() assert calendar.is_lazy() def assert_is_parsed_calendar(calendar: LazyCalendar) -> None: """Assert that the calendar is a parsed calendar.""" assert isinstance(calendar, LazyCalendar) assert not calendar._subcomponents.is_lazy() assert not calendar.is_lazy() class MockParser: def __init__(self, component: str) -> None: self._component = component self._calls = 0 def parse(self): """Return the component.""" self._calls += 1 return [self._component] @pytest.fixture def mock_parser(): """A mock parser that returns a fixed component.""" return MockParser("mock_component") @pytest.fixture def lazy_subcomponent(mock_parser): """A lazy subcomponent that uses the mock parser.""" return LazySubcomponent("VEVENT", mock_parser) def test_lazy_subcomponent_is_not_parsed_when_created(mock_parser, lazy_subcomponent): """The component is initially not parsed.""" assert not lazy_subcomponent.is_parsed() assert lazy_subcomponent._parser is mock_parser assert mock_parser._calls == 0 def test_lazy_subcomponent_returns_component(lazy_subcomponent, mock_parser): """We retrieve the component inside.""" component = lazy_subcomponent.parse() assert component == "mock_component" assert mock_parser._calls == 1 assert lazy_subcomponent.is_parsed() def test_parsing_twice_returns_same_component(lazy_subcomponent, mock_parser): """Parsing twice returns the same component.""" component1 = lazy_subcomponent.parse() component2 = lazy_subcomponent.parse() assert component1 is component2 assert mock_parser._calls == 1 def test_when_parsed_parser_is_freed(lazy_subcomponent, mock_parser): """Test that we can remove references to the content lines.""" component = lazy_subcomponent.parse() assert component == "mock_component" assert lazy_subcomponent._parser is None def test_lazy_calendar_returns_a_calendar(lazy_calendars): """LazyCalendar.from_ical returns a LazyCalendar instance.""" assert isinstance(lazy_calendars.empty, LazyCalendar) @pytest.mark.parametrize( "component_name", ["VEVENT", "VTODO", "VALARM", "X-COMPONENT", "VTIMEZONE"] ) def test_non_calendar_components_are_parsed_correctly(component_name): """LazyCalendar.from_ical() returns components as Calendar and as Component do.""" component = LazyCalendar.from_ical(f"BEGIN:{component_name}\nEND:{component_name}") assert component.name == component_name assert not component.is_lazy() def test_big_calendar_is_initially_lazy(lazy_calendars): """Test that the BigCalendar is initially lazy.""" calendar = lazy_calendars.example assert_is_lazy_calendar(calendar) def test_adding_a_component_to_a_lazy_calendar(lazy_calendars): """Test that we can add a component to a lazy calendar.""" calendar: LazyCalendar = lazy_calendars.empty calendar.add_component(Event()) assert_is_lazy_calendar(calendar) def test_getting_the_subcomponents_of_a_lazy_calendar(lazy_calendars, calendars): """Test that we can get the subcomponents of a lazy calendar.""" calendar: LazyCalendar = lazy_calendars.example subcomponents = calendar.subcomponents assert len(subcomponents) == 3 assert_is_parsed_calendar(calendar) assert calendars.example.subcomponents == subcomponents def test_parsing_a_lazy_calendar_twice(lazy_calendars): """Test that parsing a lazy calendar twice does not change the result.""" calendar: LazyCalendar = lazy_calendars.example subcomponents1 = calendar.subcomponents subcomponents2 = calendar.subcomponents assert subcomponents1 is subcomponents2 assert_is_parsed_calendar(calendar) name2getter = { "VTIMEZONE": "timezones", "VEVENT": "events", "VTODO": "todos", "VJOURNAL": "journals", "VAVAILABILITY": "availabilities", } @pytest.mark.parametrize("walk", [True, False]) @pytest.mark.parametrize( ("name", "parsed"), [ ("VTIMEZONE", ("VTIMEZONE",)), ("VEVENT", ("VTIMEZONE", "VEVENT")), ("VTODO", ("VTIMEZONE", "VTODO")), ("VJOURNAL", ("VTIMEZONE", "VJOURNAL")), ("VAVAILABILITY", ("VTIMEZONE", "VAVAILABILITY")), ], ) @pytest.mark.parametrize("fully_parsed", [True, False]) def test_accessing_timezones(walk, calendars, name, parsed, fully_parsed): """Check that only timezones are parsed. Skipping FREEBUSY for now is ok as they are for requests and need the whole calendar anyway. """ calendar: Calendar = calendars.issue_1050_all_components if fully_parsed: calendar.subcomponents components = calendar.walk(name) if walk else getattr(calendar, name2getter[name]) assert len(components) == 1, ( "We only have one component of each type in the calendar." ) assert components[0].name == name if isinstance(calendar, LazyCalendar) and not fully_parsed: assert_only_parsed(calendar, parsed) def assert_only_parsed(calendar, parsed): """Check that only certain types of components are parsed""" assert calendar.is_lazy(), "The calendar should not be fully parsed." parsed_in_calendar = { lc.name for lc in calendar._subcomponents._components if lc.is_parsed() } assert parsed_in_calendar == set(parsed), ( f"We expect only {', '.join(parsed)} to be parsed, not {', '.join(parsed_in_calendar)}." ) def test_walk_parses_the_whole_calendar(calendars): """Walk goes through them all.""" calendar = calendars.issue_1050_all_components components = calendar.walk() assert len(components) == 9 assert not calendar.is_lazy() def test_walk_and_find_an_alarm(calendars): """Walking and finding the alarm inside.""" alarms: list[Alarm] = calendars.issue_1050_all_components.walk("VALARM") assert len(alarms) == 1 alarm = alarms[0] assert alarm.name == "VALARM" @pytest.mark.parametrize( ("uid", "parsed"), [ ("event-1", ("VTIMEZONE", "VEVENT")), ("calendar-1", ("VTIMEZONE",)), ("todo-1", ("VTIMEZONE", "VTODO")), ("journal-1", ("VTIMEZONE", "VJOURNAL")), ("availability-1", ("VTIMEZONE", "VAVAILABILITY")), ("alarm-1", ("VTIMEZONE", "VEVENT")), ], ) @pytest.mark.parametrize("fully_parsed", [True, False]) def test_get_with_uid(calendars, uid, parsed, fully_parsed): calendar = calendars.issue_1050_all_components if fully_parsed: calendar.subcomponents components = calendar.with_uid(uid) assert len(components) == 1 assert components[0].uid == uid if isinstance(calendar, LazyCalendar) and not fully_parsed: assert_only_parsed(calendar, parsed) def test_absent_uid(calendars): calendar = calendars.issue_1050_all_components components = calendar.with_uid("non-existent-uid") assert len(components) == 0 def test_uid_is_present_but_not_in_the_uid_property(calendars): """Check that even if the UID is present somewhere the component is not returned. See https://github.com/collective/icalendar/pull/1201/changes#r2869287074 """ components = calendars.issue_1050_all_components.with_uid("alarm") assert len(components) == 0 collective-icalendar-138c845/src/icalendar/tests/test_issue_1065_new_subcomponents.py000066400000000000000000000033741524134033600307730ustar00rootroot00000000000000"""Tests for issue #1065: new() subcomponents parameter.""" import pytest from icalendar import ( Availability, Calendar, Component, Event, Timezone, Todo, ) def _make_child() -> Component: """Create a minimal child component.""" child = Component() child["X-TEST"] = "value" return child # Classes that accept subcomponents in new(), with any required extra kwargs. CLASSES_WITH_NEW = [ pytest.param(Component, {}, id="Component"), pytest.param(Event, {"summary": "Test"}, id="Event"), pytest.param(Todo, {"summary": "Test"}, id="Todo"), pytest.param(Calendar, {}, id="Calendar"), pytest.param(Timezone, {}, id="Timezone"), pytest.param(Availability, {}, id="Availability"), ] @pytest.mark.parametrize(("cls", "kwargs"), CLASSES_WITH_NEW) def test_new_with_subcomponents(cls, kwargs): child = _make_child() obj = cls.new(**kwargs, subcomponents=[child]) assert len(obj.subcomponents) == 1 assert obj.subcomponents[0] is child @pytest.mark.parametrize(("cls", "kwargs"), CLASSES_WITH_NEW) def test_new_without_subcomponents(cls, kwargs): obj = cls.new(**kwargs) assert obj.subcomponents == [] @pytest.mark.parametrize(("cls", "kwargs"), CLASSES_WITH_NEW) def test_subcomponents_list_identity(cls, kwargs): child = _make_child() original_list = [child] obj = cls.new(**kwargs, subcomponents=original_list) assert obj.subcomponents is original_list @pytest.mark.parametrize(("cls", "kwargs"), CLASSES_WITH_NEW) def test_subcomponents_tuple_becomes_list(cls, kwargs): child = _make_child() obj = cls.new(**kwargs, subcomponents=(child,)) assert isinstance(obj.subcomponents, list) assert len(obj.subcomponents) == 1 assert obj.subcomponents[0] is child collective-icalendar-138c845/src/icalendar/tests/test_issue_1066_calendar_uid.py000066400000000000000000000031541524134033600276320ustar00rootroot00000000000000"""Tests for issue #1066: Calendar.new() should auto-generate UID. See https://github.com/collective/icalendar/issues/1066 """ import uuid from icalendar import Calendar def test_calendar_new_auto_generates_uid(test_uid): """Test that Calendar.new() automatically generates a UID when not provided.""" calendar = Calendar.new() # UID should not be None assert calendar.uid is not None # UID should match the test fixture default assert str(calendar.uid) == test_uid def test_calendar_new_respects_explicit_uid(): """Test that Calendar.new() respects explicitly provided UID.""" custom_uid = "test-calendar-uid" calendar = Calendar.new(uid=custom_uid) assert calendar.uid == custom_uid def test_calendar_new_accepts_uuid_object(): """Test that Calendar.new() accepts UUID objects as uid parameter.""" custom_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") calendar = Calendar.new(uid=custom_uuid) # The uid property returns a string representation assert str(calendar.uid) == str(custom_uuid) def test_calendar_new_uid_appears_in_ical_output(): """Test that the auto-generated UID appears in the iCalendar output.""" calendar = Calendar.new(name="Test Calendar") ical_output = calendar.to_ical() # UID should be present in the output assert b"UID:" in ical_output # The UID value should match what we set assert str(calendar.uid).encode() in ical_output def test_issue_1066_original_assertion(): """Test the exact assertion from issue #1066.""" # This was failing before the fix assert Calendar.new().uid is not None collective-icalendar-138c845/src/icalendar/tests/test_issue_1073_pyodide_import.py000066400000000000000000000075401524134033600302500ustar00rootroot00000000000000"""Tests for issue #1073: Import failure in Pyodide/WebAssembly environments. The ZONEINFO class should use lazy initialization to avoid import-time failures in environments without timezone data. """ import threading from unittest import mock from icalendar.timezone.zoneinfo import ZONEINFO def test_zoneinfo_class_does_not_initialize_at_import_time() -> None: """Test that ZONEINFO class attributes are not computed at class definition. The class should have None placeholders for _utc and _available_timezones_cache that are only populated when the properties are accessed. """ # Verify that the class uses lazy initialization pattern # The class attributes should exist and potentially be None initially # (they may be populated from previous test runs, but that's OK) assert hasattr(ZONEINFO, "_utc") assert hasattr(ZONEINFO, "_available_timezones_cache") # The class should have property accessors for utc and _available_timezones # Check via __dict__ since they are instance properties assert "utc" in ZONEINFO.__dict__ assert isinstance(ZONEINFO.__dict__["utc"], property) assert "_available_timezones" in ZONEINFO.__dict__ assert isinstance(ZONEINFO.__dict__["_available_timezones"], property) def test_zoneinfo_utc_property_works() -> None: """Test that UTC timezone property returns a valid ZoneInfo.""" z = ZONEINFO() utc = z.utc assert utc is not None assert str(utc) == "UTC" # Second access should return the same cached instance assert z.utc is utc def test_zoneinfo_available_timezones_property_works() -> None: """Test that available_timezones property returns a valid set.""" z = ZONEINFO() timezones = z._available_timezones assert timezones is not None assert isinstance(timezones, set) assert "UTC" in timezones # Second access should return the same cached instance assert z._available_timezones is timezones def test_zoneinfo_knows_timezone_id() -> None: """Test that knows_timezone_id correctly uses the lazy _available_timezones.""" z = ZONEINFO() assert z.knows_timezone_id("UTC") is True assert z.knows_timezone_id("America/New_York") is True assert z.knows_timezone_id("NonExistent/Timezone") is False def test_zoneinfo_can_be_instantiated_before_timezone_access() -> None: """Test that ZONEINFO can be instantiated without triggering timezone lookups. This is the key test for Pyodide compatibility - we need to be able to create the provider instance without it immediately trying to access timezone data. """ # Use mock.patch to temporarily set class attributes to None # This is safer than directly modifying class state with mock.patch.object( # noqa: SIM117 ZONEINFO, "_utc", None ): with mock.patch.object(ZONEINFO, "_available_timezones_cache", None): # This should NOT raise even if zoneinfo data is unavailable z = ZONEINFO() # Just accessing the name should work without triggering timezone lookups assert z.name == "zoneinfo" def test_zoneinfo_thread_safe_initialization() -> None: """Test that lazy initialization is thread-safe.""" z = ZONEINFO() results = [] errors = [] def access_utc() -> None: try: utc = z.utc results.append(utc) except (ValueError, KeyError, OSError) as e: errors.append(e) # Create multiple threads that access utc simultaneously threads = [threading.Thread(target=access_utc) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() # All threads should get the same UTC timezone assert len(errors) == 0, f"Errors occurred: {errors}" assert len(results) == 10 assert all(r is results[0] for r in results), ( "All threads should get same UTC instance" ) collective-icalendar-138c845/src/icalendar/tests/test_issue_1124_no_utc_vtimezone.py000066400000000000000000000042531524134033600306030ustar00rootroot00000000000000"""UTC datetimes must not generate a spurious VTIMEZONE component. Per :rfc:`5545#section-3.2.19`, the TZID parameter MUST NOT be applied to DATE-TIME or TIME properties whose time values are specified in UTC. See https://github.com/collective/icalendar/issues/1124 """ from datetime import datetime, timezone from zoneinfo import ZoneInfo from icalendar import Calendar, Event, Timezone def test_add_missing_timezones_does_not_generate_utc(): """add_missing_timezones() must not add a VTIMEZONE for UTC. Using new(), we set DTSTAMP which is in UTC. This tests that the UTC timezone is not added. """ event = Event.new( summary="Meeting in Zurich", start=datetime(2022, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("Europe/Zurich")), end=datetime(2022, 1, 1, 13, 0, 0, tzinfo=ZoneInfo("Europe/Zurich")), ) calendar = Calendar.new(subcomponents=[event]) calendar.add_missing_timezones() tz_names = [tz.tz_name for tz in calendar.timezones] assert "UTC" not in tz_names assert "Europe/Zurich" in tz_names def test_utc_not_in_missing_tzids(): """UTC must not appear in get_missing_tzids() — no VTIMEZONE should be generated. Even if UTC were present in get_used_tzids() (e.g. from a legacy calendar with TZID=UTC), get_missing_tzids() must filter it out so that add_missing_timezones() never creates a VTIMEZONE for UTC. Per :rfc:`5545#section-3.2.19`, UTC datetimes use the Z suffix instead. """ calendar = Calendar() event = Event() event.add("dtstart", datetime(2022, 1, 1, 12, 0, 0, tzinfo=timezone.utc)) event.add("dtend", datetime(2022, 1, 1, 13, 0, 0, tzinfo=timezone.utc)) calendar.add_component(event) assert "UTC" not in calendar.get_missing_tzids() def test_get_missing_tzids_does_not_crash_with_extra_vtimezone(): """get_missing_tzids() must not crash when a VTIMEZONE exists for a TZID not referenced by any event property (e.g. added externally).""" calendar = Calendar() tz = Timezone.from_tzid("America/New_York") calendar.add_component(tz) # No events reference America/New_York — discard must not raise KeyError assert calendar.get_missing_tzids() == set() collective-icalendar-138c845/src/icalendar/tests/test_issue_1129.py000066400000000000000000000022161524134033600251360ustar00rootroot00000000000000from icalendar import Calendar, Component def test_component_from_ical_typing(): """Verify that from_ical has the expected type hints and overloads.""" # Runtime reflection of type hints involving TYPE_CHECKING imports like Self # is difficult without modifying the module's globals. # We will trust that static analysis (mypy/pyright) validates the syntax/validity # of the type hints themselves. # We verify the method exists and is callable. assert callable(Component.from_ical) def test_from_ical_behavior(): """Verify runtime behavior matches expectations for 'multiple'.""" data = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n" # Test single return cal = Calendar.from_ical(data, multiple=False) assert isinstance(cal, Calendar) # Test multiple return cals = Calendar.from_ical(data, multiple=True) assert isinstance(cals, list) assert len(cals) == 1 assert isinstance(cals[0], Calendar) def test_from_ical_default(): """Verify default behavior is single return.""" data = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n" cal = Calendar.from_ical(data) assert isinstance(cal, Calendar) collective-icalendar-138c845/src/icalendar/tests/test_issue_116.py000066400000000000000000000017511524134033600250540ustar00rootroot00000000000000import icalendar.cal.event def test_issue_116(): """Issue #116/#117 - How to add 'X-APPLE-STRUCTURED-LOCATION' https://github.com/collective/icalendar/issues/116 https://github.com/collective/icalendar/issues/117 """ event = icalendar.cal.event.Event() event.add( "X-APPLE-STRUCTURED-LOCATION", "geo:-33.868900,151.207000", parameters={ "VALUE": "URI", "X-ADDRESS": "367 George Street Sydney CBD NSW 2000", "X-APPLE-RADIUS": "72", "X-TITLE": "367 George Street", }, ) assert event.to_ical() == ( b"BEGIN:VEVENT\r\nX-APPLE-STRUCTURED-LOCATION;VALUE=URI;" b'X-ADDRESS="367 George Street Sydney \r\n CBD NSW 2000";' b'X-APPLE-RADIUS=72;X-TITLE="367 George Street":' b"geo:-33.868900\r\n ,151.207000\r\nEND:VEVENT\r\n" ) # roundtrip assert ( event.to_ical() == icalendar.cal.event.Event.from_ical(event.to_ical()).to_ical() ) collective-icalendar-138c845/src/icalendar/tests/test_issue_1226.py000066400000000000000000000006651524134033600251420ustar00rootroot00000000000000import pytest from icalendar.parser.string import _escape_char, escape_char def test__escape_char_accepts_bytes(): assert _escape_char(b"hello,world;test") == r"hello\,world\;test" def test_escape_char_accepts_bytes(): with pytest.warns( DeprecationWarning, match="escape_char is deprecated and will be removed in icalendar 8", ): assert escape_char(b"hello,world;test") == r"hello\,world\;test" collective-icalendar-138c845/src/icalendar/tests/test_issue_1231.py000066400000000000000000000102041524134033600251240ustar00rootroot00000000000000from datetime import date, datetime, timezone import pytest from icalendar import Available, Calendar, Event, Journal, Todo from icalendar.attr import RECURRENCE_ID from icalendar.error import InvalidCalendar from icalendar.prop import vText @pytest.fixture(params=[Event, Todo, Journal, Available]) def component_cls(request): return request.param @pytest.fixture def component(component_cls): return component_cls({}) def test_recurrence_id_absent_returns_none(component): """If RECURRENCE-ID is absent, RECURRENCE_ID getter returns None.""" assert "RECURRENCE-ID" not in component assert component.RECURRENCE_ID is None @pytest.mark.parametrize( "value", [ datetime(2025, 4, 28, 16, 5, tzinfo=timezone.utc), datetime(2025, 4, 28, 16, 5), ], ) def test_recurrence_id_returns_datetime(component, value): """RECURRENCE-ID set to datetime is returned as datetime.""" RECURRENCE_ID.__set__(component, value) assert "RECURRENCE-ID" in component result = component.RECURRENCE_ID assert isinstance(result, datetime) assert result == value def test_recurrence_id_returns_date(component): """RECURRENCE-ID set to date is returned as date.""" value = date(2025, 4, 28) component.RECURRENCE_ID = value assert "RECURRENCE-ID" in component result = component.RECURRENCE_ID assert isinstance(result, date) assert not isinstance(result, datetime) assert result == value @pytest.mark.parametrize("invalid", [42, object(), "not-a-date"]) def test_recurrence_id_invalid_type_raises(component, invalid): """Setting RECURRENCE-ID to an invalid Python type raises TypeError.""" with pytest.raises(TypeError): component.RECURRENCE_ID = invalid @pytest.mark.parametrize("raw", [vText("some text"), vText(b"some text")]) def test_recurrence_id_invalid_raw_value_raises_invalid_calendar(component_cls, raw): """Invalid raw vProp in RECURRENCE-ID raises InvalidCalendar on access.""" component = component_cls({"RECURRENCE-ID": raw}) with pytest.raises(InvalidCalendar): _ = component.RECURRENCE_ID def test_recurrence_id_invalid_vprop_raises_invalid_calendar(component_cls): """Invalid vProp object in RECURRENCE-ID raises InvalidCalendar.""" component = component_cls({"RECURRENCE-ID": vText("some text")}) with pytest.raises(InvalidCalendar): _ = component.RECURRENCE_ID def test_recurrence_id_set_none_deletes_property(component): """Setting RECURRENCE-ID to None removes the property.""" dt = datetime(2025, 4, 28, 16, 5) component.RECURRENCE_ID = dt assert "RECURRENCE-ID" in component component.RECURRENCE_ID = None assert "RECURRENCE-ID" not in component def test_recurrence_id_del_deletes_property(component): """Deleting RECURRENCE-ID removes the property.""" dt = datetime(2025, 4, 28, 16, 5) component.RECURRENCE_ID = dt assert "RECURRENCE-ID" in component del component.RECURRENCE_ID assert "RECURRENCE-ID" not in component assert component.RECURRENCE_ID is None def test_recurrence_id_with_timezone_in_to_ical(component_cls): """RECURRENCE-ID with timezone is preserved in to_ical().""" component = component_cls({}) dt = datetime(2025, 4, 28, 16, 5, tzinfo=timezone.utc) component.RECURRENCE_ID = dt ical_bytes = component.to_ical() lines = ical_bytes.splitlines() assert any( line.startswith((b"RECURRENCE-ID;TZID=", b"RECURRENCE-ID:20250428T160500Z")) for line in lines ) @pytest.mark.parametrize("tz_backend", ["pytz", "zoneinfo"]) def test_recurrence_id_parsed_from_calendar(tz_backend, calendars): """RECURRENCE-ID is correctly parsed from existing calendars.""" cal: Calendar = calendars["issue_1231_recurrence"] components = [ c for c in cal.walk() if c.name in {"VEVENT", "VTODO", "VJOURNAL"} and "RECURRENCE-ID" in c ] assert components, "Expected at least one component with RECURRENCE-ID" comp = components[0] expected = datetime(2025, 4, 28, 16, 5, tzinfo=timezone.utc) result = comp.RECURRENCE_ID assert isinstance(result, datetime) assert result == expected collective-icalendar-138c845/src/icalendar/tests/test_issue_1237_jcal_paring_of_known_types.py000066400000000000000000000007261524134033600326170ustar00rootroot00000000000000"""Tests for Issue https://github.com/collective/icalendar/issues/1237""" from datetime import datetime from icalendar import Calendar, vPeriod def test_reproduce_example(calendars): """Reproduce the example from the issue.""" calendar: Calendar = calendars.issue_1237_x_property period = calendar["x-filter-date-range"] assert isinstance(period, vPeriod) assert period.start == datetime(2025, 2, 2) assert period.end == datetime(2025, 2, 3) collective-icalendar-138c845/src/icalendar/tests/test_issue_1238.py000066400000000000000000000023701524134033600251400ustar00rootroot00000000000000""" Test for GitHub issue #1238: X-properties with VALUE= should be parsed using that type, not fall back to vUnknown. """ from datetime import datetime from icalendar import Calendar from icalendar.prop.broken import vBroken from icalendar.prop.dt import vPeriod def test_x_property_respects_value_param(calendars): """X-properties with a VALUE parameter are parsed as the correct type (issue #1238).""" calendar = calendars.issue_1238 result = calendar["X-FILTER-DATE-RANGE"] assert isinstance(result, vPeriod), f"Expected vPeriod, got {type(result).__name__}" assert result.start == datetime(2025, 2, 2, 0, 0) assert result.end == datetime(2025, 2, 3, 0, 0) def test_x_property_broken_value(): """X-properties with VALUE=PERIOD but invalid content should yield vBroken (issue #1238). View the discussion: https://github.com/collective/icalendar/pull/1288#discussion_r2962648845 """ cal_str = "BEGIN:VCALENDAR\r\nX-FILTER-DATE-RANGE;VALUE=PERIOD:this-is-not-a-period\r\nEND:VCALENDAR\r\n" calendar = Calendar.from_ical(cal_str) result = calendar["X-FILTER-DATE-RANGE"] assert isinstance(result, vBroken) assert str(result) == "this-is-not-a-period" assert result.expected_type == "vPeriod" collective-icalendar-138c845/src/icalendar/tests/test_issue_127_categories_comma_escaping.py000066400000000000000000000114711524134033600323100ustar00rootroot00000000000000"""Issue #127 - CATEGORIES comma escaping bug Tests for categories containing commas. See https://github.com/collective/icalendar/issues/127 """ from icalendar import Calendar, Event from icalendar.prop import vCategory def test_categories_with_comma_roundtrip(): """Test that categories containing commas survive round-trip.""" # Create event with categories containing commas event = Event() event.add("summary", "Test Event") event.add("categories", ["Meeting, John", "Project"]) # Serialize ical_bytes = event.to_ical() ical_str = ical_bytes.decode("utf-8") # Should contain escaped comma assert "Meeting\\, John" in ical_str assert "CATEGORIES:Meeting\\, John,Project" in ical_str # Parse back cal = Calendar.from_ical(ical_bytes) parsed_event = cal.walk("VEVENT")[0] cats = parsed_event.get("categories") # Should have 2 categories, not 3 cat_list = list(cats) if hasattr(cats, "__iter__") else cats.cats assert len(cat_list) == 2 assert str(cat_list[0]) == "Meeting, John" assert str(cat_list[1]) == "Project" def test_categories_multiple_escaped_commas(): """Test multiple commas in a single category.""" event = Event() event.add("categories", ["A, B, C", "D"]) ical_bytes = event.to_ical() # Parse back cal = Calendar.from_ical(ical_bytes) parsed_event = cal.walk("VEVENT")[0] cats = parsed_event.get("categories") cat_list = list(cats) if hasattr(cats, "__iter__") else cats.cats assert len(cat_list) == 2 assert str(cat_list[0]) == "A, B, C" assert str(cat_list[1]) == "D" def test_categories_no_commas_still_works(): """Ensure simple categories without commas still work.""" event = Event() event.add("categories", ["Work", "Personal", "Urgent"]) ical_bytes = event.to_ical() cal = Calendar.from_ical(ical_bytes) parsed_event = cal.walk("VEVENT")[0] cats = parsed_event.get("categories") cat_list = list(cats) if hasattr(cats, "__iter__") else cats.cats assert len(cat_list) == 3 assert str(cat_list[0]) == "Work" assert str(cat_list[1]) == "Personal" assert str(cat_list[2]) == "Urgent" def test_categories_from_ics_file(calendars): """Test parsing categories with escaped commas from ICS file.""" cal = calendars.issue_127_categories_with_commas event = cal.walk("VEVENT")[0] cats = event.get("categories") cat_list = list(cats) if hasattr(cats, "__iter__") else cats.cats # Should parse as 3 categories assert len(cat_list) == 3 assert str(cat_list[0]) == "Meeting, John" assert str(cat_list[1]) == "Work, Sarah" assert str(cat_list[2]) == "Project" def test_vcategory_direct_usage(): """Test vCategory class directly.""" # Create vCategory with commas cat = vCategory(["Meeting, John", "Project"]) # to_ical should escape commas ical = cat.to_ical() assert ical == b"Meeting\\, John,Project" # from_ical with list should work parsed = vCategory.from_ical(["Meeting, John", "Project"]) assert len(parsed) == 2 assert parsed[0] == "Meeting, John" assert parsed[1] == "Project" def test_categories_edge_cases(): """Test edge cases.""" # Empty category event = Event() event.add("categories", ["", "Work"]) ical = event.to_ical() cal = Calendar.from_ical(ical) cats = list(cal.walk("VEVENT")[0].get("categories")) assert len(cats) == 2 # Only commas event2 = Event() event2.add("categories", [",,,"]) ical2 = event2.to_ical() assert b"\\,\\,\\," in ical2 # Spaces around commas event3 = Event() event3.add("categories", ["A , B", "C"]) ical3 = event3.to_ical() cal3 = Calendar.from_ical(ical3) cats3 = list(cal3.walk("VEVENT")[0].get("categories")) assert str(cats3[0]) == "A , B" def test_categories_escaped_escape_sequences(): """Test escaped backslash followed by comma and double escaped comma.""" # Category with literal "\," (backslash-comma as two chars) event = Event() event.add("categories", ["\\,", "Work"]) ical = event.to_ical() # Should have escaped backslash AND escaped comma: \\\\, assert b"\\\\\\," in ical # Round-trip cal = Calendar.from_ical(ical) cats = list(cal.walk("VEVENT")[0].get("categories")) assert len(cats) == 2 assert str(cats[0]) == "\\," assert str(cats[1]) == "Work" # Category with literal "\\," (backslash, backslash, comma) event2 = Event() event2.add("categories", ["\\\\,", "Personal"]) ical2 = event2.to_ical() # Should have double escaped backslash and escaped comma assert b"\\\\\\\\\\," in ical2 # Round-trip cal2 = Calendar.from_ical(ical2) cats2 = list(cal2.walk("VEVENT")[0].get("categories")) assert len(cats2) == 2 assert str(cats2[0]) == "\\\\," assert str(cats2[1]) == "Personal" collective-icalendar-138c845/src/icalendar/tests/test_issue_1426.py000066400000000000000000000032301524134033600251330ustar00rootroot00000000000000"""Test for GitHub issue #1426: A property's VALUE parameter (e.g. RDATE;VALUE=PERIOD, TRIGGER;VALUE=DATE-TIME) was silently dropped on a jCal round-trip. Per :rfc:`7265#section-3.5.1` the value type is carried by the jCal type identifier and must be turned back into a VALUE parameter when it is not the property's default value type. The fixture-based tests here run across every calendar implementation (including ``LazyCalendar``) via the ``calendars`` fixture. Contributed by @lcampanella98. """ from icalendar import Component def _jcal_round_trip(component): return Component.from_jcal(component.to_jcal()) def test_non_default_value_param_survives_jcal_round_trip(calendars): copy = _jcal_round_trip(calendars.issue_1426) event = copy.walk("VEVENT")[0] alarm = event.walk("VALARM")[0] # PERIOD is not the default for RDATE; DATE-TIME is not the default for # TRIGGER (whose default is DURATION) -- so both must be preserved. assert event["RDATE"].params["VALUE"] == "PERIOD" assert alarm["TRIGGER"].params["VALUE"] == "DATE-TIME" # And they survive all the way back to iCalendar. ical = copy.to_ical() assert b"RDATE;VALUE=PERIOD:" in ical assert b"TRIGGER;VALUE=DATE-TIME:" in ical def test_value_param_kept_or_dropped_per_default_type(calendars): """Non-default VALUE is kept, default VALUE is dropped.""" event = _jcal_round_trip(calendars.issue_1426_value_parameters).walk("VEVENT")[0] # DATE is not the default for DTSTART (DATE-TIME) -- kept. assert event["DTSTART"].params["VALUE"] == "DATE" # URI is the default for CONFERENCE -- dropped. assert "VALUE" not in event["CONFERENCE"].params collective-icalendar-138c845/src/icalendar/tests/test_issue_1436_implicit_file_read.py000066400000000000000000000046261524134033600310320ustar00rootroot00000000000000"""Test the boundary between calendar data and file paths in ``from_ical``. A newline-free string passed to :meth:`Component.from_ical` is probed as a file path, while strings containing line breaks (i.e. real iCalendar data) are never read from disk. These tests pin down that boundary: * valid calendar data never touches the filesystem, * non-existent / invalid string paths raise a consistent ``ValueError`` (never ``OSError``) on every platform, * a missing :class:`pathlib.Path` raises ``FileNotFoundError`` like :meth:`pathlib.Path.read_bytes`. See issue: https://github.com/collective/icalendar/issues/1436 Related: https://github.com/collective/icalendar/issues/756 """ from pathlib import Path import pytest from icalendar import Calendar from icalendar.cal.examples import get_example def test_valid_calendar_string_never_touches_the_filesystem(monkeypatch): """Parsing real calendar data must not probe or read any file.""" cal_bytes = get_example("calendars", "example") cal_string = cal_bytes.decode() expected = Calendar.from_ical(cal_bytes).to_ical() def fail(*args, **kwargs): raise AssertionError("from_ical accessed the filesystem for calendar data") monkeypatch.setattr(Path, "is_file", fail) monkeypatch.setattr(Path, "read_bytes", fail) calendar = Calendar.from_ical(cal_string) assert calendar.to_ical() == expected def test_nonexistent_string_path_raises_value_error(tmp_path): """A newline-free string that is not a file is parsed as (invalid) data.""" missing = str(tmp_path / "does-not-exist.ics") with pytest.raises(ValueError): Calendar.from_ical(missing) @pytest.mark.parametrize( "bad_string", [ "not a calendar", "embedded\0null", # rejected by the OS as a path -> ValueError, not OSError "x" * 100_000, # too long to be a path on every platform ], ids=["plain-text", "null-byte", "too-long"], ) def test_invalid_string_paths_raise_value_error_not_os_error(bad_string): """Invalid path strings must surface a consistent ValueError on all OSes.""" with pytest.raises(ValueError): Calendar.from_ical(bad_string) def test_missing_path_object_raises_file_not_found(tmp_path): """A ``Path`` is always treated as a path: missing files raise clearly.""" missing = tmp_path / "does-not-exist.ics" with pytest.raises(FileNotFoundError): Calendar.from_ical(missing) collective-icalendar-138c845/src/icalendar/tests/test_issue_1445.py000066400000000000000000000207261524134033600251450ustar00rootroot00000000000000r"""Regression tests for issue #1445. :rfc:`7265` reserves the ``unknown`` value type for properties whose value type is not known (unrecognized properties, ``X-`` properties without a ``VALUE`` parameter, ``IMAGE``). Such values MUST be carried into and out of iCalendar *without processing* -- no :rfc:`5545` escaping or unescaping -- so they round-trip byte-for-byte (:rfc:`7265#section-5.1` / :rfc:`7265#section-5.2`). These tests pin down, in every direction: * unknown values are preserved verbatim; * known types (including ``VALUE=TEXT``) still get TEXT escaping (R1 boundary); * unrecognized *parameters* are still treated as TEXT, not verbatim (R2); * :class:`~icalendar.prop.unknown.vUnknown` no longer inherits from :class:`~icalendar.prop.text.vText`, but shares its plumbing. """ import pytest from icalendar.cal import Component from icalendar.prop import vDDDTypes, vText, vUnknown def _event(line: str) -> Component: """Parse a one-property VEVENT from a raw content ``line``.""" ics = f"BEGIN:VEVENT\r\nUID:1\r\n{line}\r\nEND:VEVENT\r\n" return Component.from_ical(ics) def _line(component: Component, name: str) -> str: """Return the serialized content line for ``name`` (unfolded, single line).""" for serialized in component.to_ical().decode().split("\r\n"): if serialized.startswith(name): return serialized raise AssertionError(f"{name} not serialized in {component.to_ical()!r}") # Value text exactly as it appears in the content line after the colon. Each is # an unknown value (X-MYSTERY) that must survive untouched. Mix of bare and # escaped specials, both newline spellings, and a date-time-looking value. VERBATIM_VALUES = [ r"Stenophylla;Guinea\,Africa", # RFC 7265 section 5.3 coffee example r"a;b", # bare semicolon r"a,b", # bare comma r"a\,b", # escaped comma r"a\;b", # escaped semicolon r"a\\b", # escaped backslash r"a\:b", # escaped colon (not even legal TEXT) r"a\nb", # literal backslash-n (must NOT become a real newline) r"a\Nb", # literal backslash-N "20110512T120000Z", # default type would be unknown ] @pytest.mark.parametrize("raw", VERBATIM_VALUES) def test_unknown_value_is_verbatim_in_every_direction(raw): """An unknown value is preserved byte-for-byte through every conversion.""" ev = _event(f"X-MYSTERY:{raw}") prop = ev["X-MYSTERY"] # the resolved type is the reserved unknown type assert isinstance(prop, vUnknown) # reading the value gives the unprocessed text (no unescaping) assert str(prop) == raw # iCal -> iCal assert _line(ev, "X-MYSTERY") == f"X-MYSTERY:{raw}" # iCal -> jCal: value is the raw text, type is "unknown" assert ev.to_jcal()[1][1] == ["x-mystery", {}, "unknown", raw] # jCal -> iCal assert _line(Component.from_jcal(ev.to_jcal()), "X-MYSTERY") == f"X-MYSTERY:{raw}" @pytest.mark.parametrize("raw", VERBATIM_VALUES) def test_unknown_value_round_trips_are_stable(raw): """iCal -> jCal -> iCal and jCal -> iCal -> jCal are fixpoints.""" ev = _event(f"X-MYSTERY:{raw}") ical_jcal_ical = Component.from_jcal(ev.to_jcal()).to_ical() assert ical_jcal_ical == ev.to_ical() jcal = ev.to_jcal() jcal_ical_jcal = Component.from_jcal(jcal).to_jcal() assert jcal_ical_jcal == jcal @pytest.mark.parametrize( ("ical", "jcal_value"), [ ("X-COMPLAINT-DEADLINE:20110512T120000Z", "20110512T120000Z"), (r"X-COFFEE-DATA:Stenophylla;Guinea\,Africa", r"Stenophylla;Guinea\,Africa"), ], ) def test_rfc_7265_section_5_3_examples(ical, jcal_value): """The two literal examples from RFC 7265 section 5.3.""" name = ical.split(":")[0] ev = _event(ical) assert ev.to_jcal()[1][1] == [name.lower(), {}, "unknown", jcal_value] assert _line(ev, name) == ical def test_read_only_access_returns_unprocessed_text(): """Reading an unknown value yields the verbatim text, not a decoded form.""" ev = _event(r"X-COFFEE-DATA:Stenophylla;Guinea\,Africa") assert str(ev["X-COFFEE-DATA"]) == r"Stenophylla;Guinea\,Africa" # decoded() returns the unprocessed text (verbatim), like a parsed vText assert ev.decoded("X-COFFEE-DATA") == r"Stenophylla;Guinea\,Africa" # --- R1 boundary: an explicit VALUE declares the type, so it is NOT unknown --- def test_value_text_still_escapes(): """VALUE=TEXT means the author declared TEXT: normal escaping applies.""" ev = _event(r"X-FOO;VALUE=TEXT:a;b\,c") assert isinstance(ev["X-FOO"], vText) assert not isinstance(ev["X-FOO"], vUnknown) # the bare ';' gets escaped, like any TEXT value assert _line(ev, "X-FOO") == r"X-FOO;VALUE=TEXT:a\;b\,c" assert ev.to_jcal()[1][1][2] == "text" def test_value_recognized_type_is_parsed(): """A recognized VALUE type is parsed as that type, not treated as unknown.""" ev = _event("X-WHEN;VALUE=DATE:20110512") assert isinstance(ev["X-WHEN"], vDDDTypes) assert ev.to_jcal()[1][1] == ["x-when", {}, "date", "2011-05-12"] def test_value_unrecognized_type_round_trips_losslessly(): """An unrecognized VALUE type keeps its name and a verbatim value. The jCal type field is the custom name (not ``unknown``), so the VALUE parameter survives the round-trip rather than being dropped. """ ev = _event(r"X-FOO;VALUE=FOOBAR:x;y\,z") assert isinstance(ev["X-FOO"], vUnknown) assert ev.to_jcal()[1][1] == ["x-foo", {}, "foobar", r"x;y\,z"] back = Component.from_jcal(ev.to_jcal()) assert _line(back, "X-FOO") == r"X-FOO;VALUE=FOOBAR:x;y\,z" # --- R2: unrecognized PARAMETERS are still treated as TEXT, not verbatim --- def test_unrecognized_parameter_is_decoded_not_verbatim(): """Parameters are decoded (RFC 7265 section 5.1), only the value is verbatim.""" ev = _event('X-FOO;X-ROAST="a,b;c";X-NOTE=line1^nline2:val;ue') prop = ev["X-FOO"] # value: verbatim, no decoding (bare ';' kept) assert str(prop) == "val;ue" # parameters: decoded, not verbatim assert prop.params["X-ROAST"] == "a,b;c" # de-quoted assert prop.params["X-NOTE"] == "line1\nline2" # RFC 6868 ^n -> newline assert ev.to_jcal()[1][1] == [ "x-foo", {"x-roast": "a,b;c", "x-note": "line1\nline2"}, "unknown", "val;ue", ] def test_image_without_value_is_verbatim(): """IMAGE without a VALUE resolves to unknown and stays verbatim.""" ev = _event(r"IMAGE:a;b\,c") assert isinstance(ev["IMAGE"], vUnknown) assert _line(ev, "IMAGE") == r"IMAGE:a;b\,c" assert ev.to_jcal()[1][1] == ["image", {}, "unknown", r"a;b\,c"] def test_known_text_property_is_unaffected(): """A known TEXT property still escapes and round-trips exactly as before.""" ev = _event(r"SUMMARY:a\;b\,c") assert isinstance(ev["SUMMARY"], vText) assert str(ev["SUMMARY"]) == "a;b,c" # TEXT is unescaped on read assert _line(ev, "SUMMARY") == r"SUMMARY:a\;b\,c" def test_jcal_unknown_value_with_raw_newline_fails_safely(): """A jCal unknown value with a raw newline cannot be a bare iCal value. This is only reachable from jCal (an iCal content line cannot carry a raw newline). It fails loudly rather than emitting invalid iCalendar. """ jcal = ["vevent", [["uid", {}, "text", "1"], ["x-m", {}, "unknown", "a\nb"]], []] with pytest.raises((AssertionError, ValueError)): Component.from_jcal(jcal).to_ical() # --- vUnknown / vText relationship --- def test_vunknown_does_not_inherit_vtext(): """The classes are deliberately independent (issue #1445 / RFC 7265).""" assert not issubclass(vUnknown, vText) assert vText.__name__ not in [c.__name__ for c in vUnknown.__mro__] @pytest.mark.parametrize("cls", [vText, vUnknown]) def test_shared_string_plumbing(cls): """vText and vUnknown share construction/jCal plumbing (the duplication must not drift).""" obj = cls("hello", params={"X-P": "v"}) assert str(obj) == "hello" assert obj.ical_value == "hello" assert obj.params["X-P"] == "v" assert isinstance(cls.from_ical("z"), cls) name, params, _vtype, value = obj.to_jcal("x-foo") assert (name, params, value) == ("x-foo", {"x-p": "v"}, "hello") def test_diverging_to_ical_behavior(): """The one place the two classes differ: escaping vs verbatim.""" assert vText("a;b").to_ical() == rb"a\;b" assert vUnknown("a;b").to_ical() == b"a;b" def test_diverging_jcal_type_field(): """vText serializes as ``text``; vUnknown as ``unknown``.""" assert vText("x").to_jcal("x-foo")[2] == "text" assert vUnknown("x").to_jcal("x-foo")[2] == "unknown" collective-icalendar-138c845/src/icalendar/tests/test_issue_1569_singletons_multiple_consistency.py000066400000000000000000000022221524134033600337440ustar00rootroot00000000000000"""A property must not be declared as both a singleton and multiple. ``Component.singletons`` lists properties that may appear at most once, while ``Component.multiple`` lists properties that may appear more than once. A property in both lists is contradictory. This guards the whole component registry against that mistake. See https://github.com/collective/icalendar/issues/1569 """ from __future__ import annotations import pytest from icalendar import ComponentFactory COMPONENT_CLASSES = sorted(set(ComponentFactory().values()), key=lambda cls: cls.name) @pytest.mark.parametrize("component", COMPONENT_CLASSES, ids=lambda cls: cls.name) def test_singletons_and_multiple_are_disjoint(component): """No property is classified as both a singleton and multiple.""" both = set(component.singletons) & set(component.multiple) assert not both, f"{component.name} lists {sorted(both)} in both lists" def test_alarm_attach_is_multiple_not_singleton(): """ATTACH may repeat on an email alarm (RFC 5545, 3.6.6), so it is multiple.""" from icalendar import Alarm assert "ATTACH" in Alarm.multiple assert "ATTACH" not in Alarm.singletons collective-icalendar-138c845/src/icalendar/tests/test_issue_1633.py000066400000000000000000000163361524134033600251460ustar00rootroot00000000000000"""Periods written with dates are read as midnight datetimes. RFC 5545, section 3.3.9, builds a period from datetimes only. A period written with dates used to parse without error and then raise an ``AttributeError`` from ``to_ical()``. See https://github.com/collective/icalendar/issues/1633 """ from datetime import date, datetime, timedelta, timezone import pytest from icalendar import Event, FreeBusy from icalendar.prop import vPeriod UTC_MIDNIGHT = datetime(1997, 1, 1, tzinfo=timezone.utc) def test_calendar_from_the_issue_round_trips(calendars): """The calendar serializes the dates as datetimes at midnight instead of raising.""" calendar = calendars.issue_1633_rdate_with_dates assert b"RDATE;VALUE=PERIOD:19970101T000000/19970102T000000" in calendar.to_ical() def test_no_error_is_recorded(calendars): """The value is understood, so the event is not marked as broken.""" event = calendars.issue_1633_rdate_with_dates.walk("VEVENT")[0] assert event.errors == [] @pytest.mark.parametrize( ("ical", "expected"), [ ("19970101/19970102", (datetime(1997, 1, 1), datetime(1997, 1, 2))), ("19970101/P1D", (datetime(1997, 1, 1), timedelta(days=1))), ( "19970101T180000/19970102", (datetime(1997, 1, 1, 18), datetime(1997, 1, 2)), ), ], ) def test_from_ical_converts_dates(ical, expected): """Parsing returns datetimes, so the value matches what is written out.""" assert vPeriod.from_ical(ical) == expected @pytest.mark.parametrize( ("ical", "expected"), [ ("19970101/19970102", b"19970101T000000/19970102T000000"), ("19970101/P1D", b"19970101T000000/P1D"), ("19970101T180000/19970102", b"19970101T180000/19970102T000000"), ("19970101T180000Z/19970102T070000Z", b"19970101T180000Z/19970102T070000Z"), ("19970101T180000Z/PT5H30M", b"19970101T180000Z/PT5H30M"), ], ) def test_to_ical(ical, expected): """Date-only halves are written out as datetimes, valid values are untouched.""" assert vPeriod(vPeriod.from_ical(ical)).to_ical() == expected @pytest.mark.parametrize( ("ical", "expected"), [ ("19970101/19970102T070000Z", b"19970101T000000Z/19970102T070000Z"), ("19970101T180000Z/19970102", b"19970101T180000Z/19970102T000000Z"), ], ) def test_a_converted_date_takes_the_timezone_of_the_other_half(tzp, ical, expected): """A date next to an aware datetime cannot stay naive. The two halves could not be subtracted to compute the duration. """ period = vPeriod.from_ical(ical) assert period[0].tzinfo is not None assert period[1].tzinfo is not None assert vPeriod(period).to_ical() == expected def test_a_borrowed_timezone_uses_the_offset_of_its_own_date(tzp): """The end is in summer, so it takes the summer offset of the timezone. A pytz timezone carries one offset per period of its history, so the offset of the start cannot simply be copied over. """ start = tzp.localize(datetime(1997, 7, 1, 12), "America/New_York") _start, end = vPeriod((start, date(1997, 7, 2))).dt assert end.utcoffset() == timedelta(hours=-4) def test_a_converted_start_borrows_a_named_timezone(tzp): """The start date takes the summer offset of the other half's zone.""" end = tzp.localize(datetime(1997, 7, 2, 12), "America/New_York") start, _end = vPeriod((date(1997, 7, 1), end)).dt assert start.utcoffset() == timedelta(hours=-4) def test_a_timezone_that_the_provider_does_not_use(tzp): """The timezone comes from the value, not from the active provider.""" period = vPeriod((date(1997, 1, 1), datetime(1997, 1, 2, 7, tzinfo=timezone.utc))) assert period.to_ical() == b"19970101T000000Z/19970102T070000Z" def test_the_start_keeps_its_moment_in_time(tzp): """Converting the start does not move it to another instant.""" start, _end = vPeriod.from_ical("19970101/19970102T070000Z") assert start == UTC_MIDNIGHT def test_freebusy_round_trips(calendars): """FREEBUSY is a period too, in a component that does not ignore errors.""" calendar = calendars.issue_1633_freebusy_with_dates assert b"FREEBUSY:19970101T000000/19970102T000000" in calendar.to_ical() @pytest.mark.parametrize( ("period", "expected"), [ ((date(1997, 1, 1), date(1997, 1, 2)), b"19970101T000000/19970102T000000"), ((date(1997, 1, 1), timedelta(days=1)), b"19970101T000000/P1D"), ( (date(1997, 1, 1), datetime(1997, 1, 2, 7)), b"19970101T000000/19970102T070000", ), ], ) def test_a_period_can_be_built_from_dates(period, expected): """Passing a date is a mistake that is understood rather than rejected.""" assert vPeriod(period).to_ical() == expected def test_dates_added_to_a_component(): """``add`` builds the period through vDDDLists.""" event = Event() event.add("RDATE", [(date(1997, 1, 1), date(1997, 1, 2))]) assert b"19970101T000000/19970102T000000" in event.to_ical() def test_dates_added_to_a_freebusy(): """``add`` builds the period through vPeriod itself.""" freebusy = FreeBusy() freebusy.add("FREEBUSY", [(date(1997, 1, 1), date(1997, 1, 2))]) assert b"19970101T000000/19970102T000000" in freebusy.to_ical() def test_a_comma_separated_period_list_round_trips(): """A date-only period next to a normal one survives the list split.""" event = Event() event.add( "RDATE", [ (date(1997, 1, 1), date(1997, 1, 2)), (datetime(1997, 7, 5, 12), datetime(1997, 7, 6)), ], ) ical = event.to_ical() assert b"19970101T000000/19970102T000000,19970705T120000/19970706T000000" in ical parsed = Event.from_ical(ical) assert parsed["RDATE"].dts[0].dt[0] == datetime(1997, 1, 1) def test_the_duration_is_computed_from_the_converted_values(): """The duration is available, where it used to be impossible to compute.""" period = vPeriod((date(1997, 1, 1), date(1997, 1, 3))) assert period.duration == timedelta(days=2) def test_a_period_with_a_tzid_is_unchanged(tzp, calendars): """This form already converted before the fix and still does.""" calendar = calendars.issue_1633_rdate_with_dates_and_tzid event = calendar.walk("VEVENT")[0] start, end = event["RDATE"].dts[0].dt assert start.replace(tzinfo=None) == datetime(1997, 1, 1) assert end.replace(tzinfo=None) == datetime(1997, 1, 2) assert start.tzinfo is not None assert ( b"RDATE;TZID=America/New_York;VALUE=PERIOD:" b"19970101T000000/19970102T000000" in calendar.to_ical() ) def test_an_invalid_period_is_still_invalid(): """Conversion does not make broken values parse.""" with pytest.raises(ValueError, match="Expected period format"): vPeriod.from_ical("19970101/not-a-date") def test_an_end_date_before_the_start_is_rejected(): """Midnight is the whole of the reading, the end is not stretched to the day. ``19970102T120000Z/19970102`` asks for an end that midnight puts before the start, so the period is invalid. Reading the end as the end of that day would invent a duration the calendar never stated. """ with pytest.raises(ValueError, match="Start time is greater than end time"): vPeriod(vPeriod.from_ical("19970102T120000Z/19970102")) collective-icalendar-138c845/src/icalendar/tests/test_issue_165_missing_event.py000066400000000000000000000005131524134033600300050ustar00rootroot00000000000000"""Issue #165 - Problem parsing a file with event recurring on weekdays https://github.com/collective/icalendar/issues/165 """ def test_issue_165_missing_event(calendars): events = list(calendars.issue_165_missing_event.walk("VEVENT")) assert len(events) == 1, "There was an event missing from the parsed events' list." test_issue_168_parsing_invalid_calendars_no_warning.py000066400000000000000000000010611524134033600344640ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests"""Issue #168 - Parsing invalid icalendars fails without any warning https://github.com/collective/icalendar/issues/168 """ def test_issue_168_parsing_inavlid_calendars_no_warning(calendars): expected_error = ( None, "Content line could not be parsed into parts: 'X-APPLE-RADIUS=49.91307046514149': X-APPLE-RADIUS=49.91307046514149", ) assert expected_error in calendars.issue_168_input.walk("VEVENT")[0].errors assert ( calendars.issue_168_input.to_ical() == calendars.issue_168_expected_output.raw_ics ) collective-icalendar-138c845/src/icalendar/tests/test_issue_187_type_and_value_do_not_match.py000066400000000000000000000036521524134033600326630ustar00rootroot00000000000000"""We can get time information that is actually of the WRONG value. We can still parse it and correct the value parameter. """ from datetime import date, datetime from zoneinfo import ZoneInfo import pytest from icalendar import Event @pytest.mark.parametrize( ("string", "expected_params", "expected_property_value"), [ (":20150217", {}, date(2015, 2, 17)), (";VALUE=DATETIME:20150217", {"VALUE": "DATETIME"}, date(2015, 2, 17)), ( ";VALUE=DATE:20150217T095800", {"VALUE": "DATE"}, datetime(2015, 2, 17, 9, 58, 0), ), ( ";VALUE=DATE:20150217T095800Z", {"VALUE": "DATE"}, datetime(2015, 2, 17, 9, 58, 0, tzinfo=ZoneInfo("UTC")), ), ( ";TZID=Europe/Berlin:20150217", {"TZID": "Europe/Berlin"}, datetime(2015, 2, 17, tzinfo=ZoneInfo("Europe/Berlin")), ), ( ";TZID=Europe/Berlin:20150217T095800Z", {"TZID": "Europe/Berlin"}, datetime(2015, 2, 17, 9, 58, 0, tzinfo=ZoneInfo("Europe/Berlin")), ), ], ) def test_wrong_dates_are_converted(string, expected_params, expected_property_value): """We convert these dates to the correct value if they turn up. We want to make sure that these are working, have values and do not throw exceptions. Information should not get lost except if it is contradictory. So, the tests can be changed if a different resolution is needed. The problem: The VALUE parameter might not correspond to the actual value. However, we correctly parse the value and do not lose information. """ event = Event.from_ical(f""" BEGIN:VEVENT DTSTART{string} END:VEVENT """) dt = event.get("dtstart") test = f"{dt} should be {expected_property_value} with {expected_params}" assert dt.params == expected_params, test assert dt.dt == expected_property_value, test collective-icalendar-138c845/src/icalendar/tests/test_issue_218_parse_calendar.py000066400000000000000000000034601524134033600301010ustar00rootroot00000000000000"""Parse the calendar and make sure the timezone is used. See https://github.com/collective/icalendar/issues/218 """ from __future__ import annotations from datetime import date, datetime, timedelta from typing import TYPE_CHECKING import pytest from icalendar.cal.timezone import TimezoneDaylight, TimezoneStandard from icalendar.timezone.tzid import tzid_from_dt if TYPE_CHECKING: from calendar import Calendar from icalendar.cal.event import Event @pytest.fixture def event(calendars) -> Event: """The event to check.""" return calendars.issue_218_bad_tzid.events[0] def test_event_has_start_and_end(event: Event): """The calendar should be parsed and the start and end have a timezone.""" assert event.start.replace(tzinfo=None) == datetime(2017, 2, 28, 23, 00) assert event.end.replace(tzinfo=None) == datetime(2017, 2, 28, 23, 30) def test_timezone(event: Event): """The event uses a timezone.""" assert event.start.tzinfo == event.end.tzinfo assert tzid_from_dt(event.start) == "UTC+11" assert event.start.utcoffset() == timedelta(hours=11) def test_dtstart_of_timezone_is_datetime(calendars): """Test the failing test case and try to narrow down the problem.""" calendar: Calendar = calendars.issue_218_bad_tzid tz = calendar.timezones[0] assert tz["TZID"] == "UTC+11" standard = tz.standard[0] assert standard["DTSTART"].dt == date(2017, 1, 1) assert standard["DTSTART"].params.value == "DATE" assert standard.DTSTART == datetime(2017, 1, 1) @pytest.mark.parametrize("TZ", [TimezoneStandard, TimezoneDaylight]) def test_setter_of_timezone_start(TZ): """The setter received conversion functionality.""" tz = TZ() tz.DTSTART = date(2017, 1, 1) assert tz.DTSTART == datetime(2017, 1, 1) assert tz["DTSTART"].params == {} collective-icalendar-138c845/src/icalendar/tests/test_issue_279_decoding_categories.py000066400000000000000000000010401524134033600311160ustar00rootroot00000000000000"""This tests decoding categories. See issue https://github.com/collective/icalendar/issues/279. """ from icalendar import Event, Todo def test_decode_categories_empty(): """We can decode no categories.""" event = Event() categories = event.decoded("categories", []) assert categories == [] def test_decode_categories_when_set(): """We can decode categories when set.""" todo = Todo() todo.categories = ["cat1", "cat2"] categories = todo.decoded("categories", []) assert categories == ["cat1", "cat2"] collective-icalendar-138c845/src/icalendar/tests/test_issue_27_period.py000066400000000000000000000012101524134033600263250ustar00rootroot00000000000000"""Issue #27 - multiple periods https://github.com/collective/icalendar/issues/27 """ def test_issue_27_multiple_periods(calendars): free_busy = list( calendars.issue_27_multiple_periods_in_freebusy_multiple_freebusies.walk( "VFREEBUSY" ) )[0] free_busy_period = free_busy["freebusy"] print(free_busy["freebusy"]) equivalent_way_of_defining_free_busy = list( calendars.issue_27_multiple_periods_in_freebusy_one_freebusy.walk("VFREEBUSY") )[0] free_busy_period_equivalent = equivalent_way_of_defining_free_busy["freebusy"] assert free_busy_period == free_busy_period_equivalent collective-icalendar-138c845/src/icalendar/tests/test_issue_301_add_rrule_as_string.py000066400000000000000000000006401524134033600311360ustar00rootroot00000000000000"""We want to add rrule as a string for convenience. See https://github.com/collective/icalendar/issues/301 """ from icalendar.cal.event import Event def test_rrule_add_example(): event = Event() event.add("RRULE", "FREQ=DAILY;INTERVAL=2") assert "FREQ=DAILY;INTERVAL=2" in event.to_ical().decode("utf-8") assert event.rrules[0]["freq"] == ["DAILY"] assert event.rrules[0]["interval"] == [2] collective-icalendar-138c845/src/icalendar/tests/test_issue_313_globally_unique_tzid.py000066400000000000000000000062111524134033600313540ustar00rootroot00000000000000"""A globally unique TZID (:rfc:`5545#section-3.2.19`) must resolve its timezone. Clients such as libical, Evolution and Mozilla Lightning emit the "globally unique" TZID form ``//`` (for example ``/freeassociation.sourceforge.net/Europe/Berlin``). Previously the vendor prefix stopped the timezone from being identified, so the value was parsed as timezone-naive and the UTC offset was silently lost. See https://github.com/collective/icalendar/issues/313 """ from datetime import timedelta import pytest from icalendar.error import GloballyUniqueTZIDGuessed from icalendar.timezone import tzid_from_tzinfo # UID -> (expected Olson name, expected UTC offset on 2020-04-26) for each event # in ``calendars/issue_313_globally_unique_tzid.ics``. EVENTS = { "libical-evolution@issue-313": ("Europe/Berlin", timedelta(hours=2)), "mozilla-lightning@issue-313": ("America/New_York", timedelta(hours=-4)), "multipart-olson@issue-313": ( "America/Argentina/Buenos_Aires", timedelta(hours=-3), ), } def test_events_with_global_tzids_are_timezone_aware(calendars): """Every event resolves its globally unique TZID instead of going naive.""" events = calendars.issue_313_globally_unique_tzid.walk("VEVENT") assert len(events) == len(EVENTS) for event in events: expected_tzid, expected_offset = EVENTS[str(event["UID"])] start = event["DTSTART"].dt assert start.tzinfo is not None assert start.utcoffset() == expected_offset assert tzid_from_tzinfo(start.tzinfo) == expected_tzid @pytest.mark.parametrize( ("tzid", "resolved_tzid"), [ # libical / Evolution: // ("/freeassociation.sourceforge.net/Europe/Berlin", "Europe/Berlin"), # libical Tzfile prefix in front of the Olson name ("/freeassociation.sourceforge.net/Tzfile/Europe/Paris", "Europe/Paris"), # Mozilla Lightning / Thunderbird: /// ("/mozilla.org/20070129_1/America/New_York", "America/New_York"), # Citadel: /// ("/citadel.org/20190914_1/Europe/Berlin", "Europe/Berlin"), # a single-component Olson name behind a vendor prefix ("/mozilla.org/20071231_1/UTC", "UTC"), # a multi-part Olson name behind a vendor prefix ( "/vendor.example/America/Argentina/Buenos_Aires", "America/Argentina/Buenos_Aires", ), # a plain id without a vendor prefix keeps resolving ("Europe/Berlin", "Europe/Berlin"), # an unknown trailing identifier stays unresolved (no false positives) ("/vendor.example/Not/ARealZone", None), ], ) def test_timezone_id_resolves(tzp, tzid, resolved_tzid): """The trailing Olson identifier is recovered regardless of prefix depth.""" is_global = tzid.startswith("/") if is_global and resolved_tzid is not None: with pytest.warns(GloballyUniqueTZIDGuessed): tz = tzp.timezone(tzid) else: tz = tzp.timezone(tzid) if resolved_tzid is None: assert tz is None else: assert tzid_from_tzinfo(tz) == resolved_tzid collective-icalendar-138c845/src/icalendar/tests/test_issue_315_uid_required.py000066400000000000000000000006721524134033600276170ustar00rootroot00000000000000"""This tests if the UID is set be default, closing issue 315. See https://github.com/collective/icalendar/issues/315 """ import pytest from icalendar import Event, Journal, Todo @pytest.mark.parametrize("cls", [Event, Journal, Todo]) def test_uid_is_included(cls): """Make sure we have a UID!""" component = cls.new() assert "uid" in component assert component.uid != "" assert "UID" in component.to_ical().decode() collective-icalendar-138c845/src/icalendar/tests/test_issue_318_skip_default_parameters.py000066400000000000000000000013441524134033600320330ustar00rootroot00000000000000"""Some parameters are specified as default by the RFC 5545. These tests make sure that these parameter values are not added to the properties. Example: DTSTART;VALUE=DATE-TIME:20190616T050000Z equals DTSTART:20190616T050000Z See https://github.com/collective/icalendar/issues/318 """ from datetime import datetime import pytest from icalendar.cal.event import Event @pytest.mark.parametrize( "attr", [ "DTSTART", "DTEND", "DTSTAMP", ], ) def test_datetime_in_event(attr): """Check that the "VALUE=DATE-TIME" is absent because not needed.""" event = Event() event.add(attr, datetime(2022, 10, 13, 9, 16, 42)) ics = event.to_ical() assert b"VALUE=DATE-TIME" not in ics collective-icalendar-138c845/src/icalendar/tests/test_issue_321_dst_offset.py000066400000000000000000000047711524134033600272770ustar00rootroot00000000000000"""Test for Issue #321 - AssertionError with VTIMEZONE containing only DAYLIGHT. When a VTIMEZONE component contains only a DAYLIGHT subcomponent (no STANDARD), converting it to a pytz timezone with lookup_tzid=False would fail with an AssertionError. The issue occurs because the code assumes it can find a non-DST (STANDARD) transition to calculate the dst_offset, but when only DAYLIGHT exists, this assumption fails. Original issue: https://github.com/collective/icalendar/issues/321 Original PR with test case: https://github.com/collective/icalendar/pull/379 Test case by: @niccokunzmann Fix by: @SashankBhamidi """ import pytest class TestIssue321DstOffset: """Test VTIMEZONE conversion with only DAYLIGHT component.""" @pytest.fixture def calendar_with_only_daylight(self, calendars): """Load calendar with VTIMEZONE containing only DAYLIGHT.""" return calendars.issue_321_assert_dst_offset_is_not_false def test_vtimezone_with_only_daylight_does_not_raise_assertion( self, calendar_with_only_daylight ): """Test that VTIMEZONE with only DAYLIGHT can be converted to timezone. This tests the specific case from issue #321 where a calendar generated by khal had a VTIMEZONE with only DAYLIGHT, causing an AssertionError when calling to_tz(). """ timezone = list(calendar_with_only_daylight.walk())[1] assert timezone.name == "VTIMEZONE" assert timezone["TZID"] == "Europe/Berlin" # This should not raise AssertionError # With lookup_tzid=True (default), it finds Europe/Berlin in the system tz = timezone.to_tz() assert tz is not None def test_vtimezone_with_only_daylight_forced_creation( self, calendar_with_only_daylight ): """Test forced timezone creation with lookup_tzid=False. This is a more stringent test that forces timezone creation from the VTIMEZONE definition rather than looking up an existing timezone. This was the specific case that triggered the AssertionError. """ pytest.importorskip("pytz", reason="This test requires pytz") from icalendar.timezone.pytz import PYTZ pytz_provider = PYTZ() timezone = list(calendar_with_only_daylight.walk())[1] # Force creation from VTIMEZONE definition # This used to fail with AssertionError tz = timezone.to_tz(tzp=pytz_provider, lookup_tzid=False) assert tz is not None assert tz.zone == "Europe/Berlin" test_issue_322_single_strings_characters_split_into_multiple_categories.py000066400000000000000000000007651524134033600406750ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests""" See https://github.com/collective/icalendar/issues/322 """ from icalendar.cal.calendar import Calendar from icalendar.cal.event import Event def test_issue_322_single_string_split_into_multiple_categories(calendars): calendar = Calendar() event = Event() event.add("summary", "Event with bare string as argument for categories") event.add("categories", "Lecture") calendar.add_component(event) assert calendar.to_ical() == calendars.issue_322_expected_calendar.raw_ics collective-icalendar-138c845/src/icalendar/tests/test_issue_336_dateutil_timezone.py000066400000000000000000000015461524134033600306670ustar00rootroot00000000000000"""We would like to be able to get the right timezone names. See https://github.com/collective/icalendar/issues/336 It appears that the timezone Brazil/DeNoronha is actually America/Noronha. """ from datetime import datetime from dateutil import tz from icalendar.cal.event import Event from icalendar.timezone import tzid_from_tzinfo valid_names = ("America/Noronha", "Brazil/DeNoronha") def test_timezone_name_directly(): """Try to get the name directly.""" tzinfo = tz.gettz("Brazil/DeNoronha") assert tzid_from_tzinfo(tzinfo) in valid_names def test_in_event(): """The example has an event in it and we want to have the id in it.""" event = Event() event.start = datetime(2025, 5, 13, 14, 23, tzinfo=tz.gettz("Brazil/DeNoronha")) ics = event.to_ical().decode() print(ics) assert any(name in ics for name in valid_names) collective-icalendar-138c845/src/icalendar/tests/test_issue_348_exception_parsing_value.py000066400000000000000000000014751524134033600320630ustar00rootroot00000000000000"""These are tests for Issue #348 see https://github.com/collective/icalendar/issues/348 """ def test_calendar_can_be_parsed_correctly(calendars): """Exception when there's no ':' when parsing value #348 see https://github.com/collective/icalendar/issues/348 """ freebusy = calendars.issue_348_exception_parsing_value.walk("VFREEBUSY")[0] assert freebusy["ORGANIZER"].params["CN"] == "Sixt SE" def test_parameters_are_not_truncated(calendars): """We skip to the end and we do not want to loose parameters. see https://github.com/collective/icalendar/pull/514#issuecomment-1505878801 """ freebusy = calendars.issue_348_exception_parsing_value.walk("VFREEBUSY")[0] assert freebusy["X-ORGANIZER2"].params["CN"] == "Sixt SE" assert freebusy["X-ORGANIZER2"].params["CN2"] == "Test!" collective-icalendar-138c845/src/icalendar/tests/test_issue_349_value_parameter.py000066400000000000000000000200231524134033600303110ustar00rootroot00000000000000"""Test VALUE parameter handling for Issue #349. Tests that the VALUE parameter is properly set when adding properties with Python native types (date, datetime, time, etc.) and properly used when parsing iCalendar data. Related Issues: - #349: Adding an EXDATE with type date does not set the element's VALUE as DATE - #187: Property values ignoring property parameters Related PRs: - #331: Fix VALUE parameter handling (2021, by thet) - #196: Original attempt (2016, by stlaz) """ import datetime from icalendar import Calendar, Event class TestValueParameterEncoding: """Test that VALUE parameter is automatically set during encoding.""" def test_exdate_with_date_sets_value_date(self): """Test that adding EXDATE with datetime.date sets VALUE=DATE.""" ev = Event() ev.add("UID", "test-value-param-date") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add("SUMMARY", "EXDATE test") ev.add("DTSTART", datetime.date(2022, 1, 1)) ev.add("RRULE", {"FREQ": "WEEKLY"}) ev.add("EXDATE", datetime.date(2022, 1, 8)) ical_str = ev.to_ical().decode() assert "EXDATE;VALUE=DATE:20220108" in ical_str def test_rdate_with_date_sets_value_date(self): """Test that adding RDATE with datetime.date sets VALUE=DATE.""" ev = Event() ev.add("UID", "test-rdate-date") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add("DTSTART", datetime.date(2022, 1, 1)) ev.add("RDATE", datetime.date(2022, 1, 15)) ical_str = ev.to_ical().decode() assert "RDATE;VALUE=DATE:20220115" in ical_str def test_dtstart_with_date_sets_value_date(self): """Test that DTSTART with datetime.date sets VALUE=DATE.""" ev = Event() ev.add("UID", "test-dtstart-date") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add("DTSTART", datetime.date(2022, 1, 1)) ical_str = ev.to_ical().decode() assert "DTSTART;VALUE=DATE:20220101" in ical_str def test_dtstart_with_datetime_no_value_param(self): """Test that DTSTART with datetime.datetime doesn't set VALUE (default DATE-TIME).""" ev = Event() ev.add("UID", "test-dtstart-datetime") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add( "DTSTART", datetime.datetime(2022, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), ) ical_str = ev.to_ical().decode() # DATE-TIME is the default, so VALUE parameter should not be explicitly set # (though some implementations may still add it) assert "DTSTART" in ical_str assert "20220101T100000Z" in ical_str def test_exdate_list_with_dates_sets_value_date(self): """Test that EXDATE list with datetime.date objects sets VALUE=DATE.""" ev = Event() ev.add("UID", "test-exdate-list") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add("DTSTART", datetime.date(2022, 1, 1)) ev.add("RRULE", {"FREQ": "DAILY"}) # Add dates individually - lists are handled by vDDDLists ev.add("EXDATE", datetime.date(2022, 1, 5)) ev.add("EXDATE", datetime.date(2022, 1, 10)) ical_str = ev.to_ical().decode() assert "EXDATE;VALUE=DATE:20220105" in ical_str assert "EXDATE;VALUE=DATE:20220110" in ical_str class TestValueParameterDecoding: """Test that VALUE parameter is properly used during decoding.""" def test_parse_exdate_with_value_date(self): """Test parsing EXDATE with VALUE=DATE parameter.""" ical_str = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-parse-exdate-date DTSTART;VALUE=DATE:20220101 EXDATE;VALUE=DATE:20220108 DTSTAMP:20220101T120000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR""" cal = Calendar.from_ical(ical_str) event = cal.walk("VEVENT")[0] exdate = event["EXDATE"] # EXDATE is always a vDDDLists, get the first item assert len(exdate.dts) == 1 first_date = exdate.dts[0].dt # Should be parsed as a date, not datetime assert isinstance(first_date, datetime.date) assert not isinstance(first_date, datetime.datetime) assert first_date == datetime.date(2022, 1, 8) def test_parse_dtstart_with_value_date(self): """Test parsing DTSTART with VALUE=DATE parameter.""" ical_str = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-parse-dtstart-date DTSTART;VALUE=DATE:20220101 DTSTAMP:20220101T120000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR""" cal = Calendar.from_ical(ical_str) event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] # Should be parsed as a date, not datetime assert isinstance(dtstart.dt, datetime.date) assert not isinstance(dtstart.dt, datetime.datetime) assert dtstart.dt == datetime.date(2022, 1, 1) def test_parse_dtstart_without_value_defaults_to_datetime(self): """Test that DTSTART without VALUE parameter defaults to DATE-TIME.""" ical_str = b"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-parse-dtstart-default DTSTART:20220101T100000Z DTSTAMP:20220101T120000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR""" cal = Calendar.from_ical(ical_str) event = cal.walk("VEVENT")[0] dtstart = event["DTSTART"] # Should be parsed as datetime assert isinstance(dtstart.dt, datetime.datetime) assert dtstart.dt == datetime.datetime( 2022, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc ) class TestValueParameterRoundTrip: """Test that VALUE parameter survives encoding/decoding round trips.""" def test_date_roundtrip(self): """Test that date values maintain their type through encode/decode.""" # Create event with date ev1 = Event() ev1.add("UID", "test-roundtrip") ev1.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev1.add("DTSTART", datetime.date(2022, 1, 1)) ev1.add("EXDATE", datetime.date(2022, 1, 8)) # Encode to iCalendar format ical_bytes = ev1.to_ical() # Decode back cal = Calendar.from_ical( b"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:test\n" + ical_bytes + b"\nEND:VCALENDAR" ) ev2 = cal.walk("VEVENT")[0] # Check that types are preserved assert isinstance(ev2["DTSTART"].dt, datetime.date) assert not isinstance(ev2["DTSTART"].dt, datetime.datetime) assert ev2["DTSTART"].dt == datetime.date(2022, 1, 1) # EXDATE is always a vDDDLists, get the first item assert len(ev2["EXDATE"].dts) == 1 exdate = ev2["EXDATE"].dts[0].dt assert isinstance(exdate, datetime.date) assert not isinstance(exdate, datetime.datetime) assert exdate == datetime.date(2022, 1, 8) class TestExplicitValueParameter: """Test handling of explicitly provided VALUE parameters.""" def test_explicit_value_param_overrides_inference(self): """Test that explicitly provided VALUE parameter is used.""" ev = Event() ev.add("UID", "test-explicit-value") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) # Explicitly set VALUE=DATE ev.add("DTSTART", datetime.date(2022, 1, 1), parameters={"VALUE": "DATE"}) ical_str = ev.to_ical().decode() assert "VALUE=DATE" in ical_str def test_parameters_preserved(self): """Test that other parameters are preserved when VALUE is added.""" ev = Event() ev.add("UID", "test-params-preserved") ev.add("DTSTAMP", datetime.datetime.now(datetime.timezone.utc)) ev.add( "DTSTART", datetime.datetime(2022, 1, 1, 10, 0, 0), parameters={"TZID": "America/New_York"}, ) dtstart = ev["DTSTART"] assert "TZID" in dtstart.params assert dtstart.params["TZID"] == "America/New_York" collective-icalendar-138c845/src/icalendar/tests/test_issue_350.py000066400000000000000000000004361524134033600250530ustar00rootroot00000000000000"""Issue #350 - Ignore X-... properties also at end of file? https://github.com/collective/icalendar/issues/350 """ def test_issue_350(calendars): calendar = list(calendars.issue_350.walk("X-COMMENT")) assert len(calendar) == 0, "X-COMMENT at the end of the file was parsed" collective-icalendar-138c845/src/icalendar/tests/test_issue_351_lenient_whitespace_parsing.py000066400000000000000000000021111524134033600325210ustar00rootroot00000000000000"""Regression tests for issue 351.""" from icalendar.parser.content_line import _strip_ows_around_delimiters def test_parse_calendar_with_whitespace_around_delimiters(calendars): cal = calendars["issue_351_whitespace_in_property_and_params"] refresh_interval = cal["REFRESH-INTERVAL"] assert refresh_interval.to_ical() == b"P2D" assert refresh_interval.params["VALUE"] == "DURATION" def test_strip_ows_around_delimiters_simple_regex_path(): assert ( _strip_ows_around_delimiters("VALUE = DURATION ; X-FOO = BAR") == "VALUE=DURATION;X-FOO=BAR" ) def test_strip_ows_around_delimiters_preserves_quoted_text(): assert ( _strip_ows_around_delimiters('ALTREP="cid:part1 ; keep = this" ; VALUE = TEXT') == 'ALTREP="cid:part1 ; keep = this";VALUE=TEXT' ) def test_strip_ows_around_delimiters_ignores_trailing_whitespace(): # Covers the trailing pending whitespace branch in the non-regex path. assert _strip_ows_around_delimiters('ALTREP="cid:part1 ; keep = this" ') == ( 'ALTREP="cid:part1 ; keep = this"' ) collective-icalendar-138c845/src/icalendar/tests/test_issue_355_url_escaping.py000066400000000000000000000044741524134033600276210ustar00rootroot00000000000000"""Tests for Issue 355. see https://github.com/collective/icalendar/issues/355 """ import pytest from icalendar.parser import unescape_backslash def test_facebook_link_is_correctly_parsed(events): """The facebook link must not be damaged. see https://github.com/collective/icalendar/pull/356#issuecomment-1222626128 """ assert ( events.issue_355_url_escaping["DESCRIPTION"] == "https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D" ) def test_other_facebook_link_is_correctly_parsed(events): """The facebook link must not be damaged. see https://github.com/collective/icalendar/pull/356#issuecomment-1265872696 """ expected_result = "https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D" assert events.issue_355_url_escaping_2["DESCRIPTION"] == expected_result def test_empty_quotes(events): """Make sure that empty quoted parameter values are supported.""" assert events.issue_355_url_escaping_empty_param["ORGANIZER"].params["CN"] == "" @pytest.mark.parametrize( ("input_string", "expected_result", "message"), [ ("test 123", "test 123", "No escapes, should remain unchanged."), (r"\\n", "\\n", "No escape after escaped backslash."), (r"\\,", "\\,", "No escape after escaped backslash."), (r"\\;", "\\;", "No escape after escaped backslash."), (r"\\:", "\\:", "No escape after escaped backslash."), (r"\\N", "\\N", "No escape after escaped backslash."), ("-\\n-", "-\n-", "Newline escape."), ("-\\,", "-,", "Comma escape."), ("-\\;", "-;", "Semicolon escape."), ("-\\:", "-:", "Colon escape."), ("-\\N-", "-\n-", "Newline escape."), ], ) def test_unescape_backslash(input_string, expected_result, message): """Test unescape_backslash function with various inputs.""" assert unescape_backslash(input_string) == expected_result, ( f"{message}: {input_string} -> {expected_result}" ) collective-icalendar-138c845/src/icalendar/tests/test_issue_500_vboolean_for_parameter.py000066400000000000000000000006521524134033600316430ustar00rootroot00000000000000from icalendar import vBoolean, vCalAddress from icalendar.cal.event import Event def test_vBoolean_can_be_used_as_parameter_issue_500(events): """https://github.com/collective/icalendar/issues/500""" attendee = vCalAddress("mailto:someone@example.com") attendee.params["rsvp"] = vBoolean(True) event = Event() event.add("attendee", attendee) assert event.to_ical() == events.event_with_rsvp.raw_ics collective-icalendar-138c845/src/icalendar/tests/test_issue_557_encode_native_parameters.py000066400000000000000000000116631524134033600321760ustar00rootroot00000000000000"""These are tests for Issue #557 TL;DR: Component._encode lost given parameters if the object to encode was already of native type, making its behavior unexpected. see https://github.com/collective/icalendar/issues/557 """ import unittest from icalendar.cal.component import Component class TestComponentEncode(unittest.TestCase): def test_encode_non_native_parameters(self): """Test _encode to add parameters to non-natives""" self.__assert_native_content(self.summary) self.__assert_native_kept_parameters(self.summary) def test_encode_native_keep_params_None(self): """_encode should keep parameters on natives if parameters=None """ new_sum = self.__add_params( self.summary, parameters=None, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) def test_encode_native_keep_params_empty(self): """_encode should keep paramters on natives if parameters={} """ new_sum = self.__add_params( self.summary, parameters={}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) def test_encode_native_append_params(self): """_encode should append paramters on natives keeping old parameters """ new_sum = self.__add_params( self.summary, parameters={"X-PARAM": "Test123"}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameter(new_sum, "X-PARAM", "Test123") def test_encode_native_overwrite_params(self): """_encode should overwrite single parameters if they have the same name as old ones""" new_sum = self.__add_params( self.summary, parameters={"LANGUAGE": "de"}, ) self.__assert_native_content(new_sum) self.assertParameter(new_sum, "LANGUAGE", "de") def test_encode_native_remove_params(self): """_encode should remove single parameters if they are explicitly set to None""" new_sum = self.__add_params( self.summary, parameters={"LANGUAGE": None}, ) self.__assert_native_content(new_sum) self.assertParameterMissing(new_sum, "LANGUAGE") def test_encode_native_remove_already_missing(self): """_encode should ignore removing a parameter that was already missing""" self.assertParameterMissing(self.summary, "X-MISSING") new_sum = self.__add_params( self.summary, parameters={"X-MISSING": None}, ) self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameterMissing(self.summary, "X-MISSING") def test_encode_native_full_test(self): """full test case with keeping, overwriting & removing properties""" # preperation orig_sum = self.__add_params( self.summary, parameters={ "X-OVERWRITE": "overwrite me!", "X-REMOVE": "remove me!", "X-MISSING": None, }, ) # preperation check self.__assert_native_content(orig_sum) self.__assert_native_kept_parameters(orig_sum) self.assertParameter(orig_sum, "X-OVERWRITE", "overwrite me!") self.assertParameter(orig_sum, "X-REMOVE", "remove me!") self.assertParameterMissing(orig_sum, "X-MISSING") # modification new_sum = self.__add_params( orig_sum, parameters={ "X-OVERWRITE": "overwritten", "X-REMOVE": None, "X-MISSING": None, }, ) # final asserts self.__assert_native_content(new_sum) self.__assert_native_kept_parameters(new_sum) self.assertParameter(new_sum, "X-OVERWRITE", "overwritten") self.assertParameterMissing(new_sum, "X-REMOVE") self.assertParameterMissing(new_sum, "X-MISSING") def setUp(self): self.summary = self.__gen_native() def __assert_native_kept_parameters(self, obj): self.assertParameter(obj, "LANGUAGE", "en") def __assert_native_content(self, obj): assert obj == "English Summary" def __add_params(self, obj, parameters): return Component._encode( "SUMMARY", obj, parameters=parameters, encode=True, ) def __gen_native(self): return Component._encode( "SUMMARY", "English Summary", parameters={ "LANGUAGE": "en", }, encode=True, ) def assertParameterMissing(self, obj, name): assert name not in obj.params def assertParameter(self, obj, name, val): assert name in obj.params assert obj.params[name] == val collective-icalendar-138c845/src/icalendar/tests/test_issue_596_calendar_with_attributes.py000066400000000000000000000031071524134033600322170ustar00rootroot00000000000000"""Create a new method for the Calendar class. See https://github.com/collective/icalendar/issues/596 """ import pytest from icalendar import Calendar, __version__ @pytest.mark.parametrize( ("name", "default_value"), [ ("name", None), ("description", None), ("prodid", f"-//collective//icalendar//{__version__}//EN"), ("version", "2.0"), ("calscale", None), ("method", None), ("color", None), ], ) def test_new_calendar_has_some_values(name, default_value): """When we create a new calendar, we want to have these values as default.""" c = Calendar.new() assert c.get(name) == default_value assert default_value is None or name in c @pytest.mark.parametrize( ("name", "value"), [ ("name", "foo"), ("description", "bar"), ("prodid", "my-product"), ("version", "2.1"), ("calscale", "GREGORIAN"), ("method", "PUBLISH"), ("color", "#ff0000"), ], ) def test_override_default_values(name, value): c = Calendar.new(**{name: value}) assert c.get(name) == value def test_default_getters(): c = Calendar.new() assert c.calendar_name is None assert c.description is None assert c.version == "2.0" assert c.prodid == f"-//collective//icalendar//{__version__}//EN" assert c.calscale == "GREGORIAN" assert c.method == "" assert c.color == "" assert c.categories == [] assert c.subcomponents == [] def test_categories(): c = Calendar.new(categories=["foo", "bar"]) assert c.categories == ["foo", "bar"] collective-icalendar-138c845/src/icalendar/tests/test_issue_662_component_properties.py000066400000000000000000000600201524134033600314120ustar00rootroot00000000000000"""This tests the properties of components and their types. See https://github.com/collective/icalendar/issues/662 """ from __future__ import annotations import itertools from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo import pytest from icalendar import ( Alarm, Event, IncompleteComponent, InvalidCalendar, Journal, Todo, vDatetime, vDDDTypes, vDuration, ) def prop(component: Event | Todo, prop: str) -> str: """Translate the end property. This allows us to run the same tests on Event and Todo. """ if isinstance(component, Todo) and prop.upper() == "DTEND": return "DUE" return prop @pytest.fixture(params=[Event, Todo]) def start_end_component(request): """The event to test.""" return request.param() @pytest.fixture( params=[ datetime(2022, 7, 22, 12, 7), date(2022, 7, 22), datetime(2022, 7, 22, 13, 7, tzinfo=ZoneInfo("Europe/Paris")), ] ) def dtstart(request, set_component_start, start_end_component): """Start of the event.""" set_component_start(start_end_component, request.param) return request.param def _set_component_start_init(component, start): """Create the event with the __init__ method.""" d = dict(component) d["dtstart"] = vDDDTypes(start) component.clear() component.update(type(component)(d)) def _set_component_dtstart(component, start): """Create the event with the dtstart property.""" component.DTSTART = start def _set_component_start_attr(component, start): """Create the event with the dtstart property.""" component.start = start def _set_component_start_ics(component, start): """Create the event with the start property.""" component.add("dtstart", start) ics = component.to_ical().decode() print(ics) component.clear() component.update(type(component).from_ical(ics)) @pytest.fixture( params=[ _set_component_start_init, _set_component_start_ics, _set_component_dtstart, _set_component_start_attr, ] ) def set_component_start(request): """Create a new event.""" return request.param def test_component_dtstart(dtstart, start_end_component): """Test the start of events.""" assert dtstart == start_end_component.DTSTART def test_event_start(dtstart, start_end_component): """Test the start of events.""" assert start_end_component.start == dtstart invalid_start_event_1 = Event() invalid_start_event_1.add("dtstart", datetime(2022, 7, 22, 12, 7)) invalid_start_event_1.add("dtstart", datetime(2022, 7, 22, 12, 8)) invalid_start_event_2 = Event.from_ical(invalid_start_event_1.to_ical()) invalid_start_event_3 = Event() invalid_start_event_3.add("DTSTART", (date(2018, 1, 1), date(2018, 2, 1))) invalid_start_todo_1 = Todo(invalid_start_event_1) invalid_start_todo_2 = Todo(invalid_start_event_2) invalid_start_todo_3 = Todo(invalid_start_event_3) @pytest.mark.parametrize( "invalid_event", [ invalid_start_event_1, invalid_start_event_2, invalid_start_event_3, invalid_start_todo_1, invalid_start_todo_2, invalid_start_todo_3, ], ) def test_multiple_dtstart(invalid_event): """Check that we get the right error.""" with pytest.raises(InvalidCalendar): invalid_event.start # noqa: B018, RUF100 with pytest.raises(InvalidCalendar): invalid_event.DTSTART # noqa: B018, RUF100 def test_no_dtstart(start_end_component): """DTSTART is optional. The following is REQUIRED if the component appears in an iCalendar object that doesn't specify the "METHOD" property; otherwise, it is OPTIONAL; in any case, it MUST NOT occur more than once. """ assert start_end_component.DTSTART is None with pytest.raises(IncompleteComponent): start_end_component.start # noqa: B018, RUF100 @pytest.fixture( params=[ datetime(2022, 7, 22, 12, 8), date(2022, 7, 23), datetime(2022, 7, 22, 14, 7, tzinfo=ZoneInfo("Europe/Paris")), ] ) def dtend(request, set_component_end, start_end_component): """end of the event.""" set_component_end(start_end_component, request.param) return request.param def _set_component_end_init(component, end): """Create the event with the __init__ method.""" d = dict(component) d[prop(component, "dtend")] = vDDDTypes(end) component.clear() component.update(type(component)(d)) def _set_component_end_property(component, end): """Create the event with the dtend property.""" setattr(component, prop(component, "DTEND"), end) def _set_component_end_attr(component, end): """Create the event with the dtend property.""" component.end = end def _set_component_end_ics(component, end): """Create the event with the end property.""" component.add(prop(component, "DTEND"), end) ics = component.to_ical().decode() print(ics) component.clear() component.update(type(component).from_ical(ics)) @pytest.fixture( params=[ _set_component_end_init, _set_component_end_ics, _set_component_end_property, _set_component_end_attr, ] ) def set_component_end(request): """Create a new event.""" return request.param def test_component_end_property(dtend, start_end_component): """Test the end of events.""" attr = prop(start_end_component, "DTEND") assert getattr(start_end_component, attr) == dtend def test_component_end(dtend, start_end_component): """Test the end of events.""" assert start_end_component.end == dtend @pytest.mark.parametrize("attr", ["DTSTART", "DTEND"]) def test_delete_attr(start_end_component, dtstart, dtend, attr): attr = prop(start_end_component, attr) delattr(start_end_component, attr) assert getattr(start_end_component, attr) is None delattr(start_end_component, attr) def _set_duration_vdddtypes(event: Event, duration: timedelta): """Set the vDDDTypes value""" event["DURATION"] = vDDDTypes(duration) def _set_duration_add(event: Event, duration: timedelta): """Set the vDDDTypes value""" event.add("DURATION", duration) def _set_duration_vduration(event: Event, duration: timedelta): """Set the vDDDTypes value""" event["DURATION"] = vDuration(duration) @pytest.fixture( params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration] ) def duration(start_end_component, dtstart, request): """... events have a DATE value type for the "DTSTART" property ... If such a "VEVENT" has a "DURATION" property, it MUST be specified as a "dur-day" or "dur-week" value. """ duration = ( timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2) ) request.param(start_end_component, duration) return duration def test_start_and_duration(start_end_component, dtstart, duration): """Check calculation of end with duration.""" dur = start_end_component.end - start_end_component.start assert dur == duration assert start_end_component.duration == duration # The "VEVENT" is also the calendar component used to specify an # anniversary or daily reminder within a calendar. These events # have a DATE value type for the "DTSTART" property instead of the # default value type of DATE-TIME. If such a "VEVENT" has a "DTEND" # property, it MUST be specified as a DATE value also. invalid_event_end_1 = Event() invalid_event_end_1.add("DTSTART", datetime(2024, 1, 1, 10, 20)) invalid_event_end_1.add("DTEND", date(2024, 1, 1)) invalid_event_end_2 = Event() invalid_event_end_2.add("DTEND", datetime(2024, 1, 1, 10, 20)) invalid_event_end_2.add("DTSTART", date(2024, 1, 1)) invalid_event_end_3 = Event() invalid_event_end_3.add("DTEND", datetime(2024, 1, 1, 10, 20)) invalid_event_end_3.add("DTSTART", datetime(2024, 1, 1, 10, 20)) invalid_event_end_3.add("DURATION", timedelta(days=1)) invalid_event_end_4 = Event() invalid_event_end_4.add("DTSTART", date(2024, 1, 1)) invalid_event_end_4.add("DURATION", timedelta(hours=1)) invalid_todo_end_1 = Todo() invalid_todo_end_1.add("DTSTART", datetime(2024, 1, 1, 10, 20)) invalid_todo_end_1.add("DUE", date(2024, 1, 1)) invalid_todo_end_2 = Todo() invalid_todo_end_2.add("DUE", datetime(2024, 1, 1, 10, 20)) invalid_todo_end_2.add("DTSTART", date(2024, 1, 1)) invalid_todo_end_3 = Todo() invalid_todo_end_3.add("DUE", datetime(2024, 1, 1, 10, 20)) invalid_todo_end_3.add("DTSTART", datetime(2024, 1, 1, 10, 20)) invalid_todo_end_3.add("DURATION", timedelta(days=1)) invalid_todo_end_4 = Todo() invalid_todo_end_4.add("DTSTART", date(2024, 1, 1)) invalid_todo_end_4.add("DURATION", timedelta(hours=1)) @pytest.mark.parametrize( ("invalid_component", "message"), [ ( invalid_event_end_1, "DTSTART and DTEND must be of the same type, either date or datetime.", ), ( invalid_event_end_2, "DTSTART and DTEND must be of the same type, either date or datetime.", ), ( invalid_event_end_3, "Only one of DTEND and DURATION may be in a VEVENT, not both.", ), ( invalid_event_end_4, "When DTSTART is a date, DURATION must be of days or weeks.", ), ( invalid_todo_end_1, "DTSTART and DUE must be of the same type, either date or datetime.", ), ( invalid_todo_end_2, "DTSTART and DUE must be of the same type, either date or datetime.", ), ( invalid_todo_end_3, "Only one of DUE and DURATION may be in a VTODO, not both.", ), ( invalid_todo_end_4, "When DTSTART is a date, DURATION must be of days or weeks.", ), ], ) @pytest.mark.parametrize("attr", ["start", "end"]) def test_invalid_event(invalid_component, message, attr): """Test that the end and start throuw the right error.""" with pytest.raises(InvalidCalendar) as e: getattr(invalid_component, attr) assert e.value.args[0] == message def test_event_duration_zero(): """ For cases where a "VEVENT" calendar component specifies a "DTSTART" property with a DATE-TIME value type but no "DTEND" property, the event ends on the same calendar date and time of day specified by the "DTSTART" property. """ event = Event() event.start = datetime(2024, 10, 11, 10, 20) assert event.end == event.start assert event.duration == timedelta(days=0) def test_event_duration_one_day(): """ For cases where a "VEVENT" calendar component specifies a "DTSTART" property with a DATE value type but no "DTEND" nor "DURATION" property, the event's duration is taken to be one day """ event = Event() event.start = date(2024, 10, 11) assert event.end == event.start + timedelta(days=1) assert event.duration == timedelta(days=1) def test_event_new_datetime_no_dtend_implicit_zero_duration(): """Event.new() with a naive datetime and no end/duration: implicit zero duration. See https://github.com/collective/icalendar/issues/1275 RFC 5545 §3.6.1: "For cases where a VEVENT specifies a DTSTART with a DATE-TIME value type but no DTEND property, the event ends on the same calendar date and time of day specified by DTSTART." """ start = datetime(2026, 3, 21, 6, 30, 0) event = Event.new(start=start) assert event.end == start assert event.duration == timedelta(0) assert "DTEND" not in event def test_event_new_datetime_tzaware_no_dtend_implicit_zero_duration(tzp): """Event.new() with a timezone-aware datetime and no end/duration: implicit zero duration. See https://github.com/collective/icalendar/issues/1275 This is the exact case reported in the issue: timezone-aware DTSTART with neither DTEND nor DURATION should yield zero duration, not raise an error. """ start = tzp.localize(datetime(2026, 3, 21, 6, 30, 0), "Europe/Berlin") event = Event.new(start=start) assert event.end == start assert event.duration == timedelta(0) assert "DTEND" not in event def test_event_new_date_no_dtend_implicit_one_day(): """Event.new() with a date and no end/duration: implicit one-day duration. See https://github.com/collective/icalendar/issues/1275 RFC 5545 §3.6.1: "For cases where a VEVENT specifies a DTSTART with a DATE value type but no DTEND nor DURATION property, the event's duration is taken to be one day." """ start = date(2026, 3, 21) event = Event.new(start=start) assert event.end == start + timedelta(days=1) assert event.duration == timedelta(days=1) assert "DTEND" not in event def test_todo_duration_zero(): """We do not know about the duration of a todo really.""" todo = Todo() todo.start = datetime(2024, 10, 11, 10, 20) assert todo.end == todo.start assert todo.duration == timedelta(days=0) def test_todo_duration_one_day(): """The end is at the end of the day, excluding midnight. RFC 5545: The following is an example of a "VTODO" calendar component that needs to be completed before May 1st, 2007. On midnight May 1st, 2007 this to-do would be considered overdue. """ event = Event() event.start = date(2024, 10, 11) assert event.end == event.start + timedelta(days=1) assert event.duration == timedelta(days=1) incomplete_event_1 = Event() incomplete_event_2 = Event() incomplete_event_2.add("DURATION", timedelta(hours=1)) incomplete_todo_1 = Todo() incomplete_todo_2 = Todo() incomplete_todo_2.add("DURATION", timedelta(hours=1)) @pytest.mark.parametrize( "incomplete_event_end", [ incomplete_event_1, incomplete_todo_1, ], ) @pytest.mark.parametrize("attr", ["start", "end", "duration"]) def test_incomplete_event(incomplete_event_end, attr): """Test that components without required properties throw the right error.""" with pytest.raises(IncompleteComponent): getattr(incomplete_event_end, attr) @pytest.mark.parametrize( "component_with_duration", [ incomplete_event_2, # Event with DURATION property incomplete_todo_2, # Todo with DURATION property ], ) def test_duration_property_accessible_without_dtstart(component_with_duration): """Test that DURATION property is accessible even without DTSTART (fixes issue #867).""" # DURATION property should be accessible directly assert component_with_duration.duration == timedelta(hours=1) # But start and end should still raise errors for incomplete components with pytest.raises(IncompleteComponent): _ = component_with_duration.start with pytest.raises(IncompleteComponent): _ = component_with_duration.end @pytest.mark.parametrize( "invalid_value", [ object(), timedelta(days=1), (datetime(2024, 10, 11, 10, 20), timedelta(days=1)), ], ) @pytest.mark.parametrize( ("Component", "attr"), [ (Event, "start"), (Event, "end"), (Event, "DTSTART"), (Event, "DTEND"), (Journal, "start"), (Journal, "end"), (Journal, "DTSTART"), (Todo, "start"), (Todo, "end"), (Todo, "DTSTART"), (Todo, "DUE"), ], ) def test_set_invalid_start(invalid_value, attr, Component): """Check that we get the right error. - other types that vDDDTypes accepts - object """ component = Component() with pytest.raises(TypeError) as e: setattr(component, attr, invalid_value) assert ( e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}." ) def setitem(d: dict, key, value): d[key] = value @pytest.mark.parametrize( "invalid_value", [ object(), None, (datetime(2024, 10, 11, 10, 20), timedelta(days=1)), date(2012, 2, 2), datetime(2022, 2, 2), ], ) def test_check_invalid_duration(start_end_component, invalid_value): """Check that we get the right error.""" start_end_component["DURATION"] = invalid_value with pytest.raises(InvalidCalendar) as e: start_end_component.DURATION # noqa: B018, RUF100 assert ( e.value.args[0] == f"DURATION must be a timedelta, not {type(invalid_value).__name__}." ) def test_setting_the_end_deletes_the_duration(start_end_component): """Setting the end should not break the event.""" DTEND = prop(start_end_component, "DTEND") start_end_component.DTSTART = datetime(2024, 10, 11, 10, 20) start_end_component.DURATION = timedelta(days=1) setattr(start_end_component, DTEND, datetime(2024, 10, 11, 10, 21)) assert "DURATION" not in start_end_component assert start_end_component.DURATION is None end = getattr(start_end_component, DTEND) assert end == datetime(2024, 10, 11, 10, 21) def test_setting_duration_deletes_the_end(start_end_component): """Setting the duration should not break the event.""" DTEND = prop(start_end_component, "DTEND") start_end_component.DTSTART = datetime(2024, 10, 11, 10, 20) setattr(start_end_component, DTEND, datetime(2024, 10, 11, 10, 21)) start_end_component.DURATION = timedelta(days=1) assert DTEND not in start_end_component assert getattr(start_end_component, DTEND) is None assert timedelta(days=1) == start_end_component.DURATION valid_values = pytest.mark.parametrize( ("attr", "value"), [ ("DTSTART", datetime(2024, 10, 11, 10, 20)), ("DTEND", datetime(2024, 10, 11, 10, 20)), ("DURATION", timedelta(days=1)), ], ) @valid_values def test_setting_to_none_deletes_value(start_end_component, attr, value): """Setting attributes to None deletes them.""" attr = prop(start_end_component, attr) setattr(start_end_component, attr, value) assert attr in start_end_component assert getattr(start_end_component, attr) == value setattr(start_end_component, attr, None) assert attr not in start_end_component @valid_values def test_setting_a_value_twice(start_end_component, attr, value): """Setting attributes twice replaces them.""" attr = prop(start_end_component, attr) setattr(start_end_component, attr, value + timedelta(days=1)) setattr(start_end_component, attr, value) assert getattr(start_end_component, attr) == value @pytest.mark.parametrize("attr", ["DTSTART", "DTEND", "DURATION"]) def test_invalid_none(start_end_component, attr): """Special case for None.""" attr = prop(start_end_component, attr) start_end_component[attr] = None with pytest.raises(InvalidCalendar): getattr(start_end_component, attr) def test_delete_duration(start_end_component): """Test the del command.""" start_end_component.DURATION = timedelta(days=1) del start_end_component.DURATION assert start_end_component.DURATION is None @pytest.mark.parametrize("attr", ["DTSTART", "end", "start"]) @pytest.mark.parametrize( "start", [ datetime(2024, 10, 11, 10, 20), date(2024, 10, 11), datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")), ], ) def test_journal_start(start, attr): """Test that we can set the start of a journal.""" j = Journal() setattr(j, attr, start) assert start == j.DTSTART assert j.start == start assert j.end == start assert j.duration == timedelta(0) @pytest.mark.parametrize("attr", ["start", "end"]) def test_delete_journal_start(attr): """Delete the start of the journal.""" j = Journal() j.start = datetime(2010, 11, 12, 13, 14) j.DTSTART = None assert j.DTSTART is None assert "DTSTART" not in j with pytest.raises(IncompleteComponent): getattr(j, attr) def setting_twice_does_not_duplicate_the_entry(): j = Journal() j.DTSTART = date(2024, 1, 1) j.DTSTART = date(2024, 1, 3) assert date(2024, 1, 3) == j.DTSTART assert j.start == date(2024, 1, 3) assert j.end == date(2024, 1, 3) @pytest.mark.parametrize( ("file", "trigger", "related"), [ ( "rfc_5545_absolute_alarm_example", vDatetime.from_ical("19970317T133000Z"), "START", ), ("rfc_5545_end", timedelta(days=-2), "END"), ("start_date", timedelta(days=-2), "START"), ], ) def test_get_alarm_trigger_property(alarms, file, trigger, related): """Get the trigger property.""" alarm = alarms[file] assert trigger == alarm.TRIGGER assert related == alarm.TRIGGER_RELATED def test_set_alarm_trigger(): """Set the alarm trigger.""" a = Alarm() a.TRIGGER = timedelta(hours=1) assert timedelta(hours=1) == a.TRIGGER assert a.TRIGGER_RELATED == "START" def test_set_alarm_trigger_related(): """Set the alarm trigger.""" a = Alarm() a.TRIGGER = timedelta(hours=1) a.TRIGGER_RELATED = "END" assert timedelta(hours=1) == a.TRIGGER assert a.TRIGGER_RELATED == "END" def test_get_related_without_trigger(): """The default is start""" assert Alarm().TRIGGER_RELATED == "START" def test_cannot_set_related_without_trigger(): """TRIGGER must be set to set the parameter.""" with pytest.raises(ValueError) as e: a = Alarm() a.TRIGGER_RELATED = "END" assert ( e.value.args[0] == "You must set a TRIGGER before setting the RELATED parameter." ) @pytest.mark.parametrize( ("file", "triggers"), [ ( "rfc_5545_absolute_alarm_example", ( (), (), ( vDatetime.from_ical("19970317T133000Z"), vDatetime.from_ical("19970317T134500Z"), vDatetime.from_ical("19970317T140000Z"), vDatetime.from_ical("19970317T141500Z"), vDatetime.from_ical("19970317T143000Z"), ), ), ), ("rfc_5545_end", ((), (timedelta(days=-2),), ())), ("start_date", ((timedelta(days=-2),), (), ())), ], ) def test_get_alarm_triggers(alarms, file, triggers): """Get the trigger property.""" alarm = alarms[file] print(tuple(alarm.triggers)) print(triggers) assert alarm.triggers == triggers def test_triggers_emtpy_alarm(): """An alarm with no trigger has no triggers.""" assert Alarm().triggers == ((), (), ()) h1 = timedelta(hours=1) def test_triggers_emtpy_with_no_repeat(): """Check incomplete values.""" a = Alarm() a.TRIGGER = h1 a.DURATION = h1 assert a.triggers == ((h1,), (), ()) def test_triggers_emtpy_with_no_duration(): """Check incomplete values.""" a = Alarm() a.TRIGGER = h1 a.REPEAT = 10 assert a.triggers == ((h1,), (), ()) @pytest.mark.parametrize( ("file", "triggers"), [ ( "rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"),)), ), ("rfc_5545_end", ((), (timedelta(days=-2),), ())), ("start_date", ((timedelta(days=-2),), (), ())), ], ) @pytest.mark.parametrize("duration", [timedelta(days=-1), h1]) @pytest.mark.parametrize("repeat", [1, 3]) def test_get_alarm_triggers_repeated(alarms, file, triggers, duration, repeat): """Get the trigger property.""" alarm = alarms[file].copy() alarm.REPEAT = repeat alarm.DURATION = duration for expected, triggers in zip(triggers, alarm.triggers, strict=False): # noqa: PLR1704 if not expected: assert triggers == () continue assert len(triggers) == 1 + repeat assert triggers[0] == expected[0] for x, y in itertools.pairwise(triggers): assert y - x == duration @pytest.mark.parametrize( "invalid_value", [ object(), "invalid string", 123, date(2024, 1, 1), ], ) @pytest.mark.parametrize("Component", [Todo, Event]) def test_set_duration_invalid_type(Component, invalid_value): """Test that setting duration with a non-timedelta raises TypeError. This ensures that we get the expected error message: 'Use timedelta, not {type}.' """ component = Component() with pytest.raises(TypeError) as e: component.duration = invalid_value assert e.value.args[0] == f"Use timedelta, not {type(invalid_value).__name__}." collective-icalendar-138c845/src/icalendar/tests/test_issue_716_alarm_time_computation.py000066400000000000000000000400551524134033600316760ustar00rootroot00000000000000"""Test the alarm time computation. Events can have alarms. Alarms can be in this state: - active - the user wants the alarm to pop up - acknowledged - the user no longer wants the alarm - snoozed - the user moved that alarm to another time The alarms can only work on the properties of the event like DTSTART, DTEND, and DURATION. See https://github.com/collective/icalendar/issues/716 """ from datetime import date, datetime, timedelta, timezone import pytest from icalendar.alarms import Alarms, AlarmTime from icalendar.cal.alarm import Alarm from icalendar.cal.event import Event from icalendar.error import IncompleteAlarmInformation from icalendar.prop import vDatetime from icalendar.tools import normalize_pytz UTC = timezone.utc EXAMPLE_TRIGGER = datetime(1997, 3, 17, 13, 30, tzinfo=UTC) def test_absolute_alarm_time_rfc_example(alarms): """Check that the absolute alarm is recognized. The following example is for a "VALARM" calendar component that specifies an audio alarm that will sound at a precise time and repeat 4 more times at 15-minute intervals: """ a = Alarms(alarms.rfc_5545_absolute_alarm_example) times = a.times assert len(times) == 5 for i, t in enumerate(times): assert t.trigger == EXAMPLE_TRIGGER + timedelta(minutes=15 * i) alarm_1 = Alarm() alarm_1.add("TRIGGER", EXAMPLE_TRIGGER) alarm_2 = Alarm() alarm_2["TRIGGER"] = vDatetime(EXAMPLE_TRIGGER) @pytest.mark.parametrize("alarm", [alarm_1, alarm_2]) def test_absolute_alarm_time_with_vDatetime(alarm): """Check that the absolute alarm is recognized. The following example is for a "VALARM" calendar component that specifies an audio alarm that will sound at a precise time and repeat 4 more times at 15-minute intervals: """ a = Alarms(alarm) times = a.times assert len(times) == 1 assert times[0].trigger == EXAMPLE_TRIGGER alarm_incomplete_1 = Alarm() alarm_incomplete_1.TRIGGER = timedelta(hours=2) alarm_incomplete_1.DURATION = timedelta(hours=1) alarm_incomplete_2 = Alarm() alarm_incomplete_2.TRIGGER = timedelta(hours=2) alarm_incomplete_2.REPEAT = 100 @pytest.mark.parametrize("alarm", [alarm_incomplete_1, alarm_incomplete_2]) def test_alarm_has_only_one_of_repeat_or_duration(alarm): """This is an edge case and we should ignore the repetition.""" a = Alarms(alarm) a.set_start(datetime(2027, 12, 2)) assert len(a.times) == 1 @pytest.fixture(params=[(0, timedelta(minutes=-30)), (1, timedelta(minutes=-25))]) def alarm_before_start(calendars, request): """An example alarm relative to the start of a component.""" index, td = request.param alarm = calendars.alarm_etar_future.subcomponents[-1].subcomponents[index] assert isinstance(alarm, Alarm) assert alarm.get("TRIGGER").dt == td alarm.test_td = td return alarm def test_cannot_compute_relative_alarm_without_start(alarm_before_start): """We have an alarm without a start of a component.""" with pytest.raises(IncompleteAlarmInformation) as e: Alarms(alarm_before_start).times # noqa: B018, RUF100 assert ( e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component." ) @pytest.mark.parametrize( ("dtstart", "timezone", "trigger"), [ ( datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC), ), (date(2024, 11, 16), None, datetime(2024, 11, 16, 0, 0)), ( datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC), ), (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), ], ) def test_can_complete_relative_calculation_if_a_start_is_given( alarm_before_start, dtstart, timezone, trigger, tzp ): """The start is given and required.""" start = dtstart if timezone is None else tzp.localize(dtstart, timezone) alarms = Alarms(alarm_before_start) alarms.set_start(start) assert len(alarms.times) == 1 time = alarms.times[0] expected_trigger = normalize_pytz(trigger + alarm_before_start.test_td) assert time.trigger == expected_trigger @pytest.mark.parametrize("dtstart", [date(1998, 10, 1), date(2023, 12, 31)]) def test_start_as_date_with_delta_as_date_stays_date(alarms, dtstart): """If we have an alarm with a day delta and the event is a day event, we should stay as a date.""" a = Alarms(alarms.start_date) a.set_start(dtstart) assert len(a.times) == 1 assert a.times[0].trigger == dtstart - timedelta(days=2) def test_cannot_compute_relative_alarm_without_end(alarms): """We have an alarm without an end of a component.""" with pytest.raises(IncompleteAlarmInformation) as e: Alarms(alarms.rfc_5545_end).times # noqa: B018, RUF100 assert ( e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component." ) @pytest.mark.parametrize( ("dtend", "timezone", "trigger"), [ ( datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC), ), (date(2024, 11, 16), None, date(2024, 11, 16)), ( datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC), ), (datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)), ], ) def test_can_complete_relative_calculation(alarms, dtend, timezone, trigger, tzp): """The start is given and required.""" start = dtend if timezone is None else tzp.localize(dtend, timezone) alarms = Alarms(alarms.rfc_5545_end) alarms.set_end(start) assert len(alarms.times) == 1 time = alarms.times[0] expected_trigger = normalize_pytz(trigger - timedelta(days=2)) assert time.trigger == expected_trigger @pytest.mark.parametrize("dtend", [date(1998, 10, 1), date(2023, 12, 31)]) def test_end_as_date_with_delta_as_date_stays_date(alarms, dtend): """If we have an alarm with a day delta and the event is a day event, we should stay as a date.""" a = Alarms(alarms.rfc_5545_end) a.set_end(dtend) assert len(a.times) == 1 assert a.times[0].trigger == dtend - timedelta(days=2) def test_add_multiple_alarms(alarms): """We can add multiple alarms.""" a = Alarms() a.add_alarm(alarms.start_date) a.add_alarm(alarms.rfc_5545_end) a.add_alarm(alarms.rfc_5545_absolute_alarm_example) with pytest.raises(IncompleteAlarmInformation): a.times # noqa: B018, RUF100 a.set_start(datetime(2012, 3, 5)) with pytest.raises(IncompleteAlarmInformation): a.times # noqa: B018, RUF100 a.set_end(datetime(2012, 3, 5)) assert len(a.times) == 7 def test_alarm_without_trigger_is_ignored(): """An incomplete alarm does not add a computed alarm time.""" alarms = Alarms() alarms.add_alarm(Alarm()) assert alarms.times == [] def test_alarm_acknowledgement_uses_alarm_property(): """RFC 9074 acknowledgement is used without a parent acknowledgement.""" acknowledged = datetime(2026, 7, 20, 1, tzinfo=UTC) alarm = Alarm() alarm.ACKNOWLEDGED = acknowledged alarm_time = AlarmTime(alarm, EXAMPLE_TRIGGER) assert alarm_time.acknowledged == acknowledged def test_alarm_time_action_uses_alarm_property(): """The computed occurrence exposes its underlying alarm action.""" alarm = Alarm() alarm.ACTION = "DISPLAY" alarm_time = AlarmTime(alarm, EXAMPLE_TRIGGER) assert alarm_time.action == "DISPLAY" def test_alarms_from_event_have_right_times(calendars): """We can collect from an event.""" event = calendars.alarm_etar_future.subcomponents[-1] a = Alarms(event) assert len(a.times) == 3 assert a.times[0].parent == event def test_add_component_returns_none(calendars): """Adding a component mutates the alarm collection without a return value.""" event = calendars.alarm_etar_future.subcomponents[-1] alarms = Alarms() assert alarms.add_component(event) is None def test_cannot_set_the_event_twice(calendars): """We cannot set an event twice. This make the state ambiguous.""" event = calendars.alarm_etar_future.subcomponents[-1] a = Alarms() a.add_component(event) a.add_component(event) # same component is ok with pytest.raises(ValueError): a.add_component(calendars.alarm_google_future.subcomponents[-1]) @pytest.mark.parametrize( ("calendar", "index", "count", "message"), [ ("alarm_etar_future", -1, 3, "Etar (1): we just created the alarm"), ("alarm_etar_notification", -1, 2, "Etar (2): the notification popped up"), ( "alarm_etar_notification_clicked", -1, 0, "Etar (3): the notification was dismissed", ), # TODO: check that that is really true ( "alarm_google_future", -1, 4, "Google (1): we just created the event with alarms", ), ( "alarm_google_acknowledged", -1, 2, "Google (2): 2 alarms happened at the same time", ), ("alarm_thunderbird_future", -1, 2, "Thunderbird (1.1): 2 alarms are set"), ( "alarm_thunderbird_snoozed_until_1457", -1, 2, "Thunderbird (1.2): 2 alarms are snoozed to another time", ), ( "alarm_thunderbird_closed", -1, 0, "Thunderbird (1.3): all alarms are dismissed (closed)", ), ("alarm_thunderbird_2_future", -1, 2, "Thunderbird (2.1): 2 alarms active"), ( "alarm_thunderbird_2_notification_popped_up", -1, 2, "Thunderbird (2.2): one alarm popped up as a notification", ), ( "alarm_thunderbird_2_notification_5_min_postponed", -1, 2, "Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes", ), ( "alarm_thunderbird_2_notification_5_min_postponed_and_popped_up", -1, 2, "Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up", ), ( "alarm_thunderbird_2_notification_5_min_postponed_and_closed", -1, 1, "Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged", ), ], ) def test_number_of_active_alarms_from_calendar_software( calendars, calendar, index, count, message ): """Check that we extract calculate the correct amount of active alarms.""" event = calendars[calendar].subcomponents[index] a = Alarms(event) active_alarms = ( a.active ) # We do not need to pass a timezone because the events have a timezone assert len(active_alarms) == count, ( f"{message} - I expect {count} alarms active but got {len(active_alarms)}." ) three_alarms = Alarm() three_alarms.REPEAT = 2 three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before @pytest.mark.parametrize( ("start", "acknowledged", "timezone", "count"), [ (datetime(2024, 10, 10), datetime(2024, 10, 9), "UTC", 3), (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 9, 1), "UTC", 2), (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 10, 1), "UTC", 1), (datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 11, 1), "UTC", 0), ( datetime(2024, 10, 10, 12, tzinfo=timezone.utc), datetime(2024, 10, 10, 11, 1), None, 0, ), ], ) def test_number_of_active_alarms_with_moving_time( start, acknowledged, count, tzp, timezone ): """Check how many alarms are active after a time they are acknowledged.""" a = Alarms() a.add_alarm(three_alarms) a.set_start(start) a.set_local_timezone(timezone) a.acknowledge_until(tzp.localize_utc(acknowledged)) active = a.active assert len(active) == count def test_incomplete_alarm_information_for_active_state(tzp): """Make sure we throw the right error.""" a = Alarms() a.add_alarm(three_alarms) a.set_start(date(2017, 12, 1)) a.acknowledge_until(tzp.localize_utc(datetime(2012, 10, 10, 12))) with pytest.raises(IncompleteAlarmInformation) as e: a.active # noqa: B018, RUF100 assert ( e.value.args[0] == f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()." ) @pytest.mark.parametrize( "calendar_name", [ "alarm_etar_future", "alarm_google_acknowledged", "alarm_thunderbird_closed", "alarm_thunderbird_future", "alarm_thunderbird_snoozed_until_1457", ], ) def test_thunderbird_recognition(calendars, calendar_name): """Check if we correctly discover Thunderbird's alarm algorithm.""" calendar = calendars[calendar_name] event = calendar.subcomponents[-1] assert isinstance(event, Event) assert event.is_thunderbird() == ("thunderbird" in calendar_name) @pytest.mark.parametrize( "snooze", [ datetime(2012, 10, 10, 11, 1), # before everything datetime(2017, 12, 1, 10, 1), datetime(2017, 12, 1, 11, 1), datetime(2017, 12, 1, 12, 1), datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event ], ) def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze): """When an alarm is snoozed, it pops up after the snooze time.""" a = Alarms() a.add_alarm(three_alarms) a.set_start(datetime(2017, 12, 1, 13)) a.set_local_timezone("UTC") snooze_utc = tzp.localize_utc(snooze) a.snooze_until(snooze_utc) active = a.active assert len(active) == 3 for alarm in active: assert alarm.trigger >= snooze_utc @pytest.mark.parametrize( ("event_index", "alarm_times"), [ # Assume that we have the following event with an alarm set to trigger 15 minutes before the meeting: (1, ("20210302T101500",)), # When the alarm is triggered, the user decides to "snooze" it for 5 minutes. # The client acknowledges the original alarm and creates a new "snooze" # alarm as a sibling of, and relates it to, the original alarm (note that # both occurrences of "VALARM" reside within the same "parent" VEVENT): (2, ("20210302T102000",)), # When the "snooze" alarm is triggered, the user decides to "snooze" it # again for an additional 5 minutes. The client once again acknowledges # the original alarm, removes the triggered "snooze" alarm, and creates another # new "snooze" alarm as a sibling of, and relates it to, the original alarm # (note the different UID for the new "snooze" alarm): (3, ("20210302T102500",)), # When the second "snooze" alarm is triggered, the user decides to dismiss it. # The client acknowledges both the original alarm and the second "snooze" alarm: (4, ()), ], ) def test_rfc_9074_alarm_times(events, event_index, alarm_times): """Test the examples from the RFC and their timing. Add times use America/New_York as timezone. """ a = events[f"rfc_9074_example_{event_index}"].alarms assert len(a.active) == len(alarm_times) expected_alarm_times = { vDatetime.from_ical(t, "America/New_York") for t in alarm_times } computed_alarm_times = {alarm.trigger for alarm in a.active} assert expected_alarm_times == computed_alarm_times def test_set_to_None(): """acknowledge_until, snooze_until, set_local_timezone.""" a = Alarms() a.set_start(None) a.set_end(None) a.set_local_timezone(None) a.acknowledge_until(None) a.snooze_until(None) assert vars(a) == vars(Alarms()) def test_delete_TRIGGER(): """Delete the TRIGGER property.""" a = Alarm() a.TRIGGER = datetime(2017, 12, 1, 10, 1) del a.TRIGGER assert a.TRIGGER is None collective-icalendar-138c845/src/icalendar/tests/test_issue_720_uid_property.py000066400000000000000000000003771524134033600276650ustar00rootroot00000000000000"""This tests the UID property. See https://github.com/collective/icalendar/issues/740 """ from icalendar.cal.alarm import Alarm def test_alarm_uses_x_property_too(): alarm = Alarm() alarm["X-ALARMUID"] = "1234" assert alarm.uid == "1234" collective-icalendar-138c845/src/icalendar/tests/test_issue_722_generate_vtimezone.py000066400000000000000000000401711524134033600310300ustar00rootroot00000000000000"""Generate VTIMEZONE components from actual timezone information. When we generate VTIMEZONE from actual tzinfo instances of - dateutil - zoneinfo - pytz Then, we cannot assume that the future information stays the same but we should be able to create tests that work for the past. See https://github.com/collective/icalendar/issues/722 """ import zoneinfo from datetime import date, datetime, timedelta from re import findall import pytest from dateutil.tz import gettz from icalendar import Calendar, Component, Event, Timezone from icalendar.timezone import tzid_from_tzinfo, tzids_from_tzinfo tzids = pytest.mark.parametrize( "tzid", [ "Europe/Berlin", "Asia/Singapore", "America/New_York", ], ) def assert_components_equal(c1: Component, c2: Component): """Print the diff of two components.""" ML = 32 ll1 = c1.to_ical().decode().splitlines() ll2 = c2.to_ical().decode().splitlines() pad = max(len(l) for l in ll1 if len(l) <= ML) diff = 0 for l1, l2 in zip(ll1, ll2, strict=False): a = len(l1) > 32 or len(l2) > 32 print( a * " " + l1, " " * (pad - len(l1)), a * "\n->" + l2, " " * (pad - len(l2)), "\tdiff!" if l1 != l2 else "", ) diff += l1 != l2 assert not diff, f"{diff} lines differ" @tzids def test_conversion_converges(tzp, tzid): """tzinfo -> VTIMEZONE -> tzinfo -> VTIMEZONE We can assume that both generated VTIMEZONEs are equivalent. """ if tzp.uses_pytz(): pytest.skip( "pytz will not converge on the first run. This is problematic. PYTZ-TODO" ) tzinfo1 = tzp.timezone(tzid) assert tzinfo1 is not None generated1 = Timezone.from_tzinfo(tzinfo1) generated1["TZID"] = ( "test-generated" # change the TZID so we do not use an existing one ) tzinfo2 = generated1.to_tz() generated2 = Timezone.from_tzinfo(tzinfo2, "test-generated") tzinfo3 = generated2.to_tz() generated3 = Timezone.from_tzinfo(tzinfo3, "test-generated") # pprint(generated1.get_transitions()) # pprint(generated2.get_transitions()) assert_components_equal(generated1, generated2) assert_components_equal(generated2, generated3) assert 2 <= len(generated1.standard + generated1.daylight) <= 3 assert 2 <= len(generated2.standard + generated2.daylight) <= 3 assert dict(generated1) == dict(generated2) assert generated1.to_ical().decode() == generated2.to_ical().decode() assert generated1.daylight == generated2.daylight assert generated1.standard == generated2.standard assert generated1 == generated2 @tzids def both_tzps_generate_the_same_info(tzid, tzp): """We want to make sure that we get the same info for all timezone implementations. We assume that - the timezone implementations have the same info within the days we test - the timezone transitions times do not change because they are before last_date """ # default generation tz1 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1)) tzp.use_zoneinfo() # we compare to zoneinfo tz2 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1)) assert_components_equal(tz1, tz2) assert tz1 == tz2 @tzids def test_tzid_matches(tzid, tzp): """Check the TZID.""" tz = Timezone.from_tzinfo(tzp.timezone(tzid)) assert tz["TZID"] == tzid def test_do_not_convert_utc(tzp): """We do not need to convert UTC but it should work.""" utc = Timezone.from_tzid("UTC") assert utc.daylight == [] assert len(utc.standard) == 1 standard = utc.standard[0] assert standard["TZOFFSETFROM"].td == timedelta(0) assert standard["TZOFFSETTO"].td == timedelta(0) assert standard["TZNAME"] == "UTC" def test_berlin_time(tzp): """Test the Europe/Berlin timezone conversion.""" tz = Timezone.from_tzid("Europe/Berlin") # we should have two timezones in it for x in tz.standard: print(x.name, x["TZNAME"], x["TZOFFSETFROM"].td, x["TZOFFSETTO"].td) print(x.to_ical().decode()) assert len(tz.daylight) == 1 assert len(tz.standard) in (1, 2), "We start in winter" dst = tz.daylight[-1] sta = tz.standard[-1] assert dst["TZNAME"] == "CEST" # summer assert sta["TZNAME"] == "CET" assert dst["TZOFFSETFROM"].td == timedelta(hours=1) # summer assert sta["TZOFFSETFROM"].td == timedelta(hours=2) assert dst["TZOFFSETTO"].td == timedelta(hours=2) # summer assert sta["TZOFFSETTO"].td == timedelta(hours=1) def test_range_is_not_crossed(): first_date = datetime(2023, 1, 1) last_date = datetime(2024, 1, 1) def check(dt): assert first_date <= dt <= last_date tz = Timezone.from_tzid("Europe/Berlin", last_date=last_date, first_date=first_date) for sub in tz.standard + tz.daylight: check(sub.DTSTART) for rdate in sub.get("RDATE", []): check(rdate) @tzids def test_use_the_original_timezone(tzid, tzp): """When we get the timezone again, usually, we should use the one of the library/tzp.""" tzinfo1 = tzp.timezone(tzid) assert tzinfo1 is not None generated1 = Timezone.from_tzinfo(tzinfo1) tzinfo2 = generated1.to_tz() assert type(tzinfo1) == type(tzinfo2) assert tzinfo1 == tzinfo2 @pytest.mark.parametrize( ("tzid", "dt", "tzname"), [ ("Asia/Singapore", datetime(1970, 1, 1), "+0730"), ("Asia/Singapore", datetime(1981, 12, 31), "+0730"), ("Asia/Singapore", datetime(1981, 12, 31, 23, 10), "+0730"), ("Asia/Singapore", datetime(1981, 12, 31, 23, 34), "+0730"), ("Asia/Singapore", datetime(1981, 12, 31, 23, 59, 59), "+0730"), ("Asia/Singapore", datetime(1982, 1, 1), "+08"), ("Asia/Singapore", datetime(1982, 1, 1, 0, 1), "+08"), ("Asia/Singapore", datetime(1982, 1, 1, 0, 34), "+08"), ("Asia/Singapore", datetime(1982, 1, 1, 1, 0), "+08"), ("Asia/Singapore", datetime(1982, 1, 1, 1, 1), "+08"), ("Europe/Berlin", datetime(1970, 1, 1), "CET"), ("Europe/Berlin", datetime(2024, 3, 31, 0, 0), "CET"), ("Europe/Berlin", datetime(2024, 3, 31, 1, 0), "CET"), ("Europe/Berlin", datetime(2024, 3, 31, 2, 0), "CET"), ("Europe/Berlin", datetime(2024, 3, 31, 2, 59, 59), "CET"), ("Europe/Berlin", datetime(2024, 3, 31, 3, 0), "CEST"), ("Europe/Berlin", datetime(2024, 3, 31, 3, 0, 1), "CEST"), ("Europe/Berlin", datetime(2024, 3, 31, 4, 0), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 0, 0), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 1, 0), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 2, 0), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 2, 30), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 2, 59, 59), "CEST"), ("Europe/Berlin", datetime(2024, 10, 27, 3, 0), "CET"), ("Europe/Berlin", datetime(2024, 10, 27, 3, 0, 1), "CET"), ("Europe/Berlin", datetime(2024, 10, 27, 4, 0), "CET"), # transition times from https://www.zeitverschiebung.net/de/timezone/america--new_york ("America/New_York", datetime(1970, 1, 1), "EST"), # Daylight Saving Time ("America/New_York", datetime(2024, 11, 3, 0, 0), "EDT"), ("America/New_York", datetime(2024, 11, 3, 1, 0), "EDT"), ("America/New_York", datetime(2024, 11, 3, 1, 59, 59), "EDT"), # 03.11.2024 2:00am -> 1:00am Standard # ("America/New_York", datetime(2024, 11, 3, 2, 0), "EDT"), ("America/New_York", datetime(2024, 11, 3, 2, 0, 1), "EST"), ("America/New_York", datetime(2024, 11, 3, 3, 0), "EST"), ("America/New_York", datetime(2025, 3, 9, 1, 0), "EST"), ("America/New_York", datetime(2025, 3, 9, 1, 59, 59), "EST"), ("America/New_York", datetime(2025, 3, 9, 2, 0), "EST"), # 09.03.2025 2:00am -> 3:00am Daylight Saving Time ("America/New_York", datetime(2025, 3, 9, 3, 0), "EDT"), ("America/New_York", datetime(2025, 3, 9, 3, 1, 1), "EDT"), ("America/New_York", datetime(2025, 3, 9, 4, 0), "EDT"), ], ) def test_check_datetimes_around_transition_times(tzp, tzid, dt, tzname): """We should make sure than the datetimes with the generated timezones work as expected: They have the right UTC offset, dst and tzname. """ message = f"{tzid}: {dt} (expected in {tzname})" expected_dt = tzp.localize(dt, tzid) component = Timezone.from_tzinfo(tzp.timezone(tzid)) generated_tzinfo = component.to_tz(tzp, lookup_tzid=False) generated_dt = dt.replace(tzinfo=generated_tzinfo) print(generated_tzinfo) if tzp.uses_pytz(): # generated_dt = generated_tzinfo.localize(dt) generated_dt = generated_tzinfo.normalize(generated_dt) if dt in ( datetime(2024, 10, 27, 1, 0), datetime(2024, 11, 3, 1, 59, 59), datetime(2024, 11, 3, 1, 0), datetime(2024, 11, 3, 0, 0), datetime(2024, 10, 27, 2, 59, 59), datetime(2024, 10, 27, 2, 30), datetime(2024, 10, 27, 2, 0), datetime(2024, 10, 27, 1, 0), ): pytest.skip("We do not know how to do this. PYTZ-TODO") assert generated_dt.tzname() == expected_dt.tzname() == tzname, message assert generated_dt.dst() == expected_dt.dst(), message assert generated_dt.utcoffset() == expected_dt.utcoffset(), message @pytest.mark.parametrize("uid", [0, 1, 2, 3]) def test_dateutil_timezone_when_time_is_going_backwards(calendars, tzp, uid): """When going from Daylight Saving Time to Standard Time, times can be ambiguous. For example, at 3:00 AM, the time falls back to 2:00 AM, repeating a full hour of times from 2:00 AM to 3:00 AM on the same date. By the RFC 5545, we cannot accommodate this case. All datetimes should be BEFORE the transition if ambiguous. However, dateutil can create a timezone that allows the event to be after this ambiguous time span, of course. Each event has its timezone saved in it. """ cal: Calendar = calendars.issue_722_timezone_transition_ambiguity event: Event = cal.events[uid] expected_tzname = str(event["X-TZNAME"]) actual_tzname = event.start.tzname() assert actual_tzname == expected_tzname, event["SUMMARY"] def query_tzid(query: str, cal: Calendar) -> str: """The tzid from the query.""" try: tzinfo = eval(query, {"cal": cal}) # noqa: S307 except Exception as e: raise ValueError(query) from e return tzid_from_tzinfo(tzinfo) # these are queries for all the places that have a TZID # according to RFC 5545 queries = [ "cal.events[0].start.tzinfo", # DTSTART "cal.events[0].end.tzinfo", # DTEND # EXDATE "cal.todos[0].end.tzinfo", # DUE "cal.events[0].get('RDATE').dts[0].dt[0].tzinfo", # RDATE "cal.events[1].get('RECURRENCE-ID').dt.tzinfo", # RECURRENCE-ID "cal.events[2].get('RDATE')[0].dts[0].dt.tzinfo", # RDATE multiple "cal.events[2].get('RDATE')[1].dts[0].dt.tzinfo", # RDATE multiple ] @pytest.mark.parametrize("query", queries) def test_add_missing_timezones_to_example(calendars, query): """Add the missing timezones to the calendar.""" cal: Calendar = calendars.issue_722_missing_timezones tzid = query_tzid(query, cal) tzs = cal.get_missing_tzids() assert tzid in tzs def test_queries_yield_unique_tzids(calendars): """We make sure each query tests a unique place to find for the algorithm.""" cal = calendars.issue_722_missing_timezones tzids = set() for query in queries: tzid = query_tzid(query, cal) print(query, "->", tzid) tzids.add(tzid) assert len(tzids) == len(queries) def test_we_do_not_miss_to_add_a_query(calendars): """Make sure all tzids are actually queried.""" cal = calendars.issue_722_missing_timezones raw = cal.raw_ics.decode() ids = set(findall("TZID=([a-zA-Z_/+-]+)", raw)) assert cal.get_used_tzids() == ids, "We find all tzids and they are unique." assert len(ids) == len(queries), "We query all the tzids." def test_unknown_tzid(calendars): """If we have an unknown tzid with no timezone component.""" cal = calendars.issue_722_missing_VTIMEZONE_custom assert "CUSTOM_tzid" in cal.get_used_tzids() assert "CUSTOM_tzid" in cal.get_missing_tzids() def test_custom_timezone_is_found_and_used(calendars): """Check the custom timezone component is not missing.""" cal = calendars.america_new_york assert "custom_America/New_York" in cal.get_used_tzids() assert "custom_America/New_York" not in cal.get_missing_tzids() def test_not_missing_anything(): """Check that no timezone is missing.""" cal = Calendar() assert cal.get_missing_tzids() == set() def test_utc_is_not_missing(calendars): """UTC should not be found missing.""" cal = calendars.issue_722_missing_timezones assert "UTC" not in cal.get_missing_tzids() assert "UTC" not in cal.get_used_tzids() def test_dateutil_timezone_is_not_found_with_tzname(calendars, no_pytz): """dateutil is an example of a timezone that has no tzid. In this test we make sure that the timezone is said to be missing. """ cal: Calendar = calendars.america_new_york cal.subcomponents.remove(cal.timezones[0]) assert cal.get_missing_tzids() == {"custom_America/New_York"} assert "dateutil" in repr(cal.events[0].start.tzinfo.__class__) @pytest.mark.parametrize("tzname", ["America/New_York", "Arctic/Longyearbyen"]) # @pytest.mark.parametrize("component", ["STANDARD", "DAYLIGHT"]) def test_dateutil_timezone_is_matched_with_tzname(tzname): """dateutil is an example of a timezone that has no tzid. In this test we make sure that the timezone is matched by its tzname() in the timezone in the STANDARD and DAYLIGHT components. """ cal = Calendar() event = Event() event.start = datetime(2024, 11, 12, tzinfo=gettz(tzname)) print(dir(event.start.tzinfo)) cal.add_component(event) assert cal.get_missing_tzids() == {tzname} cal.add_missing_timezones() assert cal.get_missing_tzids() == set() def test_dateutil_timezone_is_also_added(calendars): """We find and add a dateutil timezone. This is important as we use those in the zoneinfo implementation. """ @pytest.mark.parametrize( "calendar", [ "example", "america_new_york", # custom "timezone_same_start", # known tzid "period_with_timezone", # known tzid ], ) def test_timezone_is_not_missing(calendars, calendar): """Check that these calendars have no timezone missing.""" cal: Calendar = calendars[calendar] timezones = cal.timezones[:] assert set() == cal.get_missing_tzids() cal.add_missing_timezones() assert set() == cal.get_missing_tzids() assert cal.timezones == timezones def test_add_missing_known_timezones(calendars): """Add all timezones specified.""" cal: Calendar = calendars.issue_722_missing_timezones assert len(cal.timezones) == 0 cal.add_missing_timezones() assert len(cal.timezones) == len(queries), "all timezones are known" assert len(cal.get_missing_tzids()) == 0 def test_cannot_add_unknown_timezone(calendars): """I cannot add a timezone that is unknown.""" cal: Calendar = calendars.issue_722_missing_VTIMEZONE_custom assert len(cal.timezones) == 0 assert cal.get_missing_tzids() == {"CUSTOM_tzid"} cal.add_missing_timezones() assert cal.timezones == [], "we cannot add this timezone" assert cal.get_missing_tzids() == {"CUSTOM_tzid"} def test_cannot_create_a_timezone_from_an_invalid_tzid(): with pytest.raises(ValueError): Timezone.from_tzid("invalid/tzid") def test_dates_before_and_after_are_considered(): """When we add the timezones, we should check the calendar to see if all dates really occur in the span we use. We should also consider a huge default range. """ pytest.skip("todo") @pytest.mark.parametrize( "tzid", zoneinfo.available_timezones() - {"Factory", "localtime"} ) def test_we_can_identify_dateutil_timezones(tzid): """dateutil and others were badly supported. But if we know their shortcodes, we should be able to identify them. """ tz = gettz(tzid) assert tz is None or tzid in tzids_from_tzinfo(tz) collective-icalendar-138c845/src/icalendar/tests/test_issue_756_read_calendar_from_file.py000066400000000000000000000032401524134033600317270ustar00rootroot00000000000000"""Test reading a calendar from a file. See issue: https://github.com/collective/icalendar/issues/756 """ from pathlib import Path import pytest from icalendar import Calendar from icalendar.cal.examples import get_example @pytest.fixture def dummy_cal(tmp_path: Path) -> tuple[Calendar, Path]: cal_path = tmp_path / "calendar.ics" cal_bytes = get_example("calendars", "example") cal_path.write_bytes(cal_bytes) return Calendar.from_ical(cal_bytes), cal_path def test_reading_cal_from_path(dummy_cal): """Test reading a calendar from a valid path.""" expected_cal, path = dummy_cal actual_cal = Calendar.from_ical(path) assert actual_cal.to_ical() == expected_cal.to_ical() def test_reading_cal_from_string_path(dummy_cal): """Test reading a calendar from a valid string path.""" expected_cal, path = dummy_cal actual_cal = Calendar.from_ical(str(path)) assert actual_cal.to_ical() == expected_cal.to_ical() def test_reading_cal_from_long_string(dummy_cal): """Test reading a calendar from a long string (without path-too-long error).""" expected_cal, _ = dummy_cal expected_cal["key"] = "longvalue" * 1000 long_string = expected_cal.to_ical().decode() actual_cal = Calendar.from_ical(long_string) assert actual_cal.to_ical() == expected_cal.to_ical() def test_calendar_from_ical_is_declared_for_api_documentation(): """Test Calendar.from_ical is documented as a Calendar API method.""" cal_bytes = get_example("calendars", "example") assert "from_ical" in Calendar.__dict__ assert ( Calendar.from_ical(cal_bytes).to_ical() == Calendar.from_ical(cal_bytes).to_ical() ) collective-icalendar-138c845/src/icalendar/tests/test_issue_798_property_parameters.py000066400000000000000000000114451524134033600312640ustar00rootroot00000000000000"""Test the property parameters. See https://github.com/collective/icalendar/issues/798 """ from datetime import datetime from typing import Any import pytest from icalendar import ( CUTYPE, FBTYPE, PARTSTAT, RANGE, RELTYPE, vCalAddress, vDatetime, vPeriod, vText, ) from icalendar.parser import Parameters from icalendar.timezone.tzp import TZP class Prop: params: Parameters def __init__(self, **parameters: Any) -> None: """Create a new property.""" self.params = Parameters(parameters) def to_ical(self) -> str: """Parameters to bytes to string.""" return self.params.to_ical().decode("utf-8") from icalendar.param import ( ALTREP, CN, CUTYPE, DELEGATED_FROM, DELEGATED_TO, RELTYPE, ) @pytest.fixture def p(): """Empty test property.""" return Prop() def test_set_altrep(p): p.ALTREP = "http://example.com" assert p.params == {"ALTREP": "http://example.com"} assert p.ALTREP == "http://example.com" def test_altrep_must_be_quoted(): """altrepparam = "ALTREP" "=" DQUOTE uri DQUOTE""" assert Prop(ALTREP="1234aA").to_ical() == 'ALTREP="1234aA"' def test_del_altrep(p): """Del when empty""" del p.ALTREP assert p.params == {} assert p.ALTREP is None def test_del_altrep_full(p): """Del when empty""" p.ALTREP = "http://example.com" del p.ALTREP assert p.params == {} assert p.ALTREP is None def test_get_cutype(p): """The default is individual.""" assert p.CUTYPE == "INDIVIDUAL" def test_set_lowercase(): p = Prop(CUTYPE="individual") assert p.CUTYPE == "INDIVIDUAL" p.CUTYPE = "unknown" assert p.CUTYPE == CUTYPE.UNKNOWN def test_set_delegation_to_string(p): p.DELEGATED_FROM = "mailto:foo" assert p.DELEGATED_FROM == ("mailto:foo",) def test_set_delegation_to_tuple(p): p.DELEGATED_TO = ("mailto:foo", "mailto:bar") assert p.DELEGATED_TO == ("mailto:foo", "mailto:bar") def test_delete_delegation_to(p): p.DELEGATED_TO = ("mailto:foo", "mailto:bar") del p.DELEGATED_TO assert p.DELEGATED_TO == () @pytest.mark.parametrize( ("value", "expected"), [ ((), ""), (("mailto:foo",), 'DELEGATED-TO="mailto:foo"'), (("mailto:foo", "mailto:bar"), 'DELEGATED-TO="mailto:foo","mailto:bar"'), (('mailto:"asd"',), "DELEGATED-TO=\"mailto:^'asd^'\""), ], ) def test_serialize_delegation_to(p, value, expected): p.DELEGATED_TO = value assert p.to_ical() == expected @pytest.mark.parametrize( ("index", "expected"), [ (0, FBTYPE.BUSY_UNAVAILABLE), (1, FBTYPE.BUSY), (2, FBTYPE.FREE), ], ) def test_get_fbtype(calendars, index, expected): fb = calendars.issue_798_freebusy.freebusy[index] p: vPeriod = fb["FREEBUSY"] if isinstance(p, list): p = p[0] assert expected == p.FBTYPE @pytest.fixture def addr(): return vCalAddress("mailto:foo") def test_partstat_get(addr: vCalAddress): """test the default partstat""" assert addr.PARTSTAT == "NEEDS-ACTION" def test_set_the_partstat(addr: vCalAddress): addr.PARTSTAT = PARTSTAT.ACCEPTED assert addr.PARTSTAT == "ACCEPTED" def test_this_and_future(): assert vDatetime(datetime(2019, 12, 10)).RANGE is None def test_this_and_future_set(): d = vDatetime(datetime(2019, 12, 10)) d.RANGE = RANGE.THISANDFUTURE assert d.params["RANGE"] == "THISANDFUTURE" def test_rsvp_default(addr): assert not addr.RSVP @pytest.mark.parametrize("rsvp", [True, False]) def test_set_rsvp(addr: vCalAddress, rsvp): addr.RSVP = rsvp assert rsvp == addr.RSVP assert addr.params["RSVP"] == ("TRUE" if rsvp else "FALSE") def test_sent_by(addr: vCalAddress): assert addr.SENT_BY is None def test_set_sent_by(addr: vCalAddress): addr.SENT_BY = "mailto:asd" assert addr.SENT_BY == "mailto:asd" assert addr.params["SENT-BY"] == "mailto:asd" assert addr.params.to_ical() == b'SENT-BY="mailto:asd"' @pytest.mark.parametrize("tzid", [None, "Europe/Berlin"]) def test_tzid(tzid, tzp: TZP): """Check that the TZID parameter is determined by the datetime value.""" dt = vDatetime(tzp.localize(datetime(2019, 12, 10), tzid)) assert dt.TZID == tzid @pytest.mark.parametrize( ("index", "reltype"), [ (0, RELTYPE.PARENT), (1, RELTYPE.SIBLING), ], ) def test_reltype_example(calendars, index, reltype): """The reltype parameter in the examples.""" event = calendars.issue_798_related_to.events[index] print(event.to_ical().decode()) r: vText = event["RELATED-TO"] print(r) print(r.params) assert reltype == r.RELTYPE def test_set_reltype(p): p.RELTYPE = RELTYPE.CHILD assert p.RELTYPE == RELTYPE.CHILD assert p.params["RELTYPE"] == "CHILD" collective-icalendar-138c845/src/icalendar/tests/test_issue_802.py000066400000000000000000000027761524134033600250660ustar00rootroot00000000000000"""Test the sequence and other int properies. https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.4 https://github.com/collective/icalendar/issues/802 """ import pytest from icalendar.cal.component import Component from icalendar.cal.event import Event from icalendar.cal.journal import Journal from icalendar.cal.todo import Todo @pytest.fixture(params=[0, None]) def default_sequence(request): return request.param @pytest.fixture(params=[Event, Journal, Todo]) def component(request, default_sequence) -> Component: """Return a component.""" component: Component = request.param() if default_sequence is not None: component["SEQUENCE"] = default_sequence return component def test_sequence_is_0(component: Component): """Check the default value.""" assert component.sequence == 0 def test_increase_sequence(component: Component): """Check the default value.""" component.sequence += 1 assert component.sequence == 1 assert component["SEQUENCE"] == 1 def test_set_sequence(component: Component): """Check the default value.""" component.sequence = 400 assert component.sequence == 400 assert component["SEQUENCE"] == 400 def test_delete_sequence_default(component: Component): """Delete the value.""" del component.sequence assert component.sequence == 0 def test_delete_sequence_with_value(component: Component): """Delete the value.""" component.sequence = 400 del component.sequence assert component.sequence == 0 collective-icalendar-138c845/src/icalendar/tests/test_issue_821_stdin_parse.py000066400000000000000000000107641524134033600274560ustar00rootroot00000000000000# tests/test_cli_stdin_argparse.py import contextlib import io import sys import tempfile import unittest from datetime import datetime from pathlib import Path from unittest import mock from zoneinfo import ZoneInfo from icalendar import Calendar, cli INPUT = """ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT SUMMARY:Test Summary ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com ATTENDEE:attendee2@test.test COMMENT:Comment DTSTART;TZID=Europe/Warsaw:20220820T103400 DTEND;TZID=Europe/Warsaw:20220820T113400 LOCATION:New Amsterdam, 1000 Sunrise Test Street DESCRIPTION: Test Description END:VEVENT BEGIN:VEVENT ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com SUMMARY:Test summary DTSTART;TZID=Europe/Warsaw:20220820T200000 DTEND;TZID=Europe/Warsaw:20220820T203000 LOCATION:New Amsterdam, 1010 Test Street DESCRIPTION:Test Description\\nThis one is multiline END:VEVENT BEGIN:VEVENT UID:1 SUMMARY:TEST DTSTART:20220511 DURATION:P5D END:VEVENT END:VCALENDAR """.lstrip() def _local_datetime(dt: str) -> str: """ Match the CLI's behavior: display datetimes in the *local* timezone. The source times are Europe/Warsaw; convert to local tz for comparison. """ return ( datetime.strptime(dt, "%Y%m%dT%H%M%S") .replace(tzinfo=ZoneInfo("Europe/Warsaw")) .astimezone() .strftime("%c") ) # datetimes are displayed in the local timezone, so build expected strings at runtime _firststart = _local_datetime("20220820T103400") _firstend = _local_datetime("20220820T113400") _secondstart = _local_datetime("20220820T200000") _secondend = _local_datetime("20220820T203000") PROPER_OUTPUT = f""" Organizer: organizer Attendees: attendee1 attendee2 Summary : Test Summary Starts : {_firststart} End : {_firstend} Duration : 1:00:00 Location : New Amsterdam, 1000 Sunrise Test Street Comment : Comment Description: Test Description Organizer: organizer Attendees: attendee1 Summary : Test summary Starts : {_secondstart} End : {_secondend} Duration : 0:30:00 Location : New Amsterdam, 1010 Test Street Comment :{" "} Description: Test Description This one is multiline Organizer:{" "} Attendees: Summary : TEST Starts : Wed May 11 00:00:00 2022 End : Mon May 16 00:00:00 2022 Duration : 5 days, 0:00:00 Location :{" "} Comment :{" "} Description: {" "} """.replace("\r\n", "\n") # normalize newlines just in case class TestIcalendarCLIArgparseStdin(unittest.TestCase): def test_cli_with_stdin_dash(self): """ Validate that `type=argparse.FileType("r", ...)` maps '-' to sys.stdin and the CLI prints the expected, fully formatted output. """ # Provide INPUT via stdin fake_stdin = io.StringIO(INPUT) captured = io.StringIO() with ( mock.patch.object(sys, "stdin", fake_stdin), mock.patch.object(sys, "argv", ["icalendar", "-"]), contextlib.redirect_stdout(captured), ): cli.main() assert captured.getvalue() == PROPER_OUTPUT def test_cli_with_file_path(self): """ Validate that passing a real file path also yields the same output. """ with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tf: tf.write(INPUT) tmpname = tf.name try: captured = io.StringIO() with ( mock.patch.object(sys, "argv", ["icalendar", tmpname]), contextlib.redirect_stdout(captured), ): cli.main() assert captured.getvalue() == PROPER_OUTPUT finally: Path(tmpname).unlink() def test_view_direct_matches_cli(self): """ Optional: sanity check that the lower-level view(event) path still matches what the CLI emits, to guard against formatting drift. """ cal = Calendar.from_ical(INPUT) manual = [] for evt in cal.walk("vevent"): manual.append(cli.view(evt)) manual.append("") # the CLI prints a blank line after each event manual_output = "\n".join(manual) + "\n" # trailing newline assert manual_output == PROPER_OUTPUT if __name__ == "__main__": unittest.main() collective-icalendar-138c845/src/icalendar/tests/test_issue_828.py000066400000000000000000000103251524134033600250630ustar00rootroot00000000000000"""Events differ although their times are equal. See https://github.com/collective/icalendar/issues/828 """ import contextlib from datetime import date, datetime, timedelta, timezone from zoneinfo import ZoneInfo import pytest from icalendar import Event, Journal, vDate, vDatetime, vDDDLists, vDDDTypes def to_dt(a: date) -> date: return a def to_event(a): e = Event() e.start = a e.end = a + timedelta(days=1) e.add("RECURRENCE-ID", a - timedelta(days=1)) return e def to_journal(a): """return a journal for testing""" j = Journal() j.start = a j.end = a + timedelta(days=1) j.add("RECURRENCE-ID", a - timedelta(days=1)) return j def to_vDD(a): """Return a value type.""" if isinstance(a, datetime): return vDatetime(a) return vDate(a) def to_vDDDTypes(a): return vDDDTypes(a) def to_vDDDLists(a): return vDDDLists([a]) equal_dt_pairs = [ (date(1998, 10, 1), date(1998, 10, 1)), (datetime(2023, 12, 31), datetime(2023, 12, 31)), ( datetime(2023, 12, 31, tzinfo=timezone.utc), datetime(2023, 12, 31, tzinfo=ZoneInfo("UTC")), ), ( datetime(2023, 12, 31, tzinfo=ZoneInfo("UTC")), datetime(2023, 12, 31, tzinfo=ZoneInfo("UTC")), ), ( datetime(2025, 12, 31, tzinfo=ZoneInfo("UTC")), datetime(2025, 12, 31, tzinfo=ZoneInfo("GMT+0")), ), ( datetime(2025, 12, 31, 12, tzinfo=ZoneInfo("Europe/Zurich")), datetime(2025, 12, 31, 11, tzinfo=ZoneInfo("UTC")), ), ] param_equal_dts = pytest.mark.parametrize(("d1", "d2"), equal_dt_pairs) param_transform = pytest.mark.parametrize( "transform", [to_dt, to_event, to_journal, to_vDD, to_vDDDTypes, to_vDDDLists] ) @param_equal_dts @param_transform def test_equality_of_equal_datetimes(d1, d2, transform): """Check that the values are equal.""" a = transform(d1) b = transform(d2) assert_equal(a, b) def assert_equal(a, b): """Check all equality tests.""" assert a == b, f"1 equal: {a} == {b}" assert b == a, f"2 equal reversed: {b} == {a}" assert not a != b, f"3 not equal: {a} != {b}" # noqa: SIM202 assert not b != a, f"4 not equal reversed: {b} != {a}" # noqa: SIM202 assert bool(a) == bool(b), f"5 equal bool: {bool(a)} == {bool(b)}" with contextlib.suppress(TypeError): assert hash(a) == hash(b) def assert_not_eq(a, b): """Check all equality tests.""" assert a != b, f"1 unequal: {a} == {b}" assert b != a, f"2 unequal reversed: {b} == {a}" assert not a == b, f"3 not unequal: {a} != {b}" # noqa: SIM201 assert not b == a, f"4 not unequal reversed: {b} != {a}" # noqa: SIM201 dts = [to_dt, to_vDD, to_vDDDTypes, to_vDDDLists] @pytest.mark.parametrize("transform1", dts) @pytest.mark.parametrize("transform2", dts) @param_equal_dts def test_property_times_for_date_and_datetime(d1, d2, transform1, transform2): """Check the equality of the date and date implementations.""" a = transform1(d1) b = transform2(d2) assert_equal(a, b) def test_datetime_is_in_list_representation(): dt = datetime(2023, 12, 31) assert str(dt) in str(vDDDLists([dt])) unequal_pairs = [(a, b) for a, b in equal_dt_pairs if a != b] @pytest.mark.parametrize(("d1", "d2"), unequal_pairs) @param_transform def test_inequality(d1, d2, transform): """The values are not equal.""" a = transform(d1) b = transform(d2) assert_not_eq(a, b) @pytest.mark.parametrize("ddd_type", [to_vDD, to_vDDDTypes]) @param_equal_dts def test_not_equal_if_parameters_differ(d1, d2, ddd_type): """If the items are equal but the parameters differ, they should not be equal.""" d1 = ddd_type(d1) d2 = ddd_type(d2) d1.params["foo"] = "bar" assert_not_eq(d1, d2) @pytest.mark.parametrize("ddd_type", [to_vDD, to_vDDDTypes]) @param_equal_dts def test_equal_ignoring_x_params(d1, d2, ddd_type): """RFC 5545: Applications MUST ignore x-param and iana-param values they don't recognize.""" d1 = ddd_type(d1) d2 = ddd_type(d2) d1.params["x-foo"] = "bar" assert_equal(d1, d2) def test_list_is_not_hashable(): """We cannot hash a list because it changes.""" with pytest.raises(TypeError): hash(vDDDLists([datetime(2023, 12, 31)])) collective-icalendar-138c845/src/icalendar/tests/test_issue_836_do_not_quote_tzid.py000066400000000000000000000045011524134033600306720ustar00rootroot00000000000000"""Quoting the TZID parameter creates compatibility problems. See https://github.com/collective/icalendar/issues/836 :rfc:`5545`: .. code-block:: text param-value = paramtext / quoted-string paramtext = *SAFE-CHAR quoted-string = DQUOTE *QSAFE-CHAR DQUOTE SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII ; Any character except CONTROL, DQUOTE, ";", ":", "," NON-US-ASCII = UTF8-2 / UTF8-3 / UTF8-4 ; UTF8-2, UTF8-3, and UTF8-4 are defined in [RFC3629] """ from datetime import datetime, time import pytest from icalendar import Event, Parameters, vDatetime, vDDDTypes, vPeriod, vTime from icalendar.prop import vDDDLists # All the controls except HTAB CONTROL = {i for i in range(256) if 0x00 <= i <= 0x08 or 0x0A <= i <= 0x1F or i == 0x7F} # Any character except CONTROL, DQUOTE, ";", ":", "," SAFE_CHAR = set(range(256)) - CONTROL - set(b'";:,') param_tzid = pytest.mark.parametrize( "tzid", [ "Europe/London", "Eastern Standard Time", ], ) @param_tzid @pytest.mark.parametrize( "vdt", [ vDatetime(datetime(2024, 10, 11, 12, 0)), vTime(time(23, 59, 59)), vDDDTypes(datetime(2024, 10, 11, 12, 0)), vPeriod((datetime(2024, 10, 11, 12, 0), datetime(2024, 10, 11, 13, 0))), vDDDLists([datetime(2024, 10, 11, 12, 0)]), ], ) def test_parameter_is_not_quoted_when_not_needed(tzid, vdt): """Check that serializing the value works without quoting.""" e = Event() vdt.params["TZID"] = tzid e["DTSTART"] = vdt ics = e.to_ical().decode() print(ics) assert tzid in ics assert f'"{tzid}' not in ics assert f'{tzid}"' not in ics @pytest.mark.parametrize("safe_char", list(map(chr, sorted(SAFE_CHAR)))) def test_safe_char_is_not_escaped(safe_char): """Check that paramerter serialization is without quotes for safe chars.""" params = Parameters(tzid=safe_char) result = params.to_ical().decode() assert '"' not in result def test_get_calendar_and_serialize_it_wihtout_quotes(calendars): """The example calendar should not contain the timezone with quotes.""" ics = calendars.issue_836_do_not_quote_tzid.to_ical().decode() assert '"Eastern Standard' not in ics assert 'Standard Time"' not in ics assert "Eastern Standard Time" in ics collective-icalendar-138c845/src/icalendar/tests/test_issue_843_new_method_for_components.py000066400000000000000000000672241524134033600324160ustar00rootroot00000000000000"""This tests the creation of new components. To aid creating valid calendars with a few lines of code, the components receive a new() classmethod. This method MUST use property setters to set the properties defined in the keywords. New keywords can be added over time. See https://github.com/collective/icalendar/issues/843 """ from __future__ import annotations import itertools import traceback from datetime import date, datetime, timedelta, timezone from typing import TYPE_CHECKING, Any from zoneinfo import ZoneInfo import pytest from icalendar import ( Alarm, Availability, Available, Calendar, Component, Event, FreeBusy, Journal, Todo, vCalAddress, ) from icalendar.enums import BUSYTYPE from icalendar.prop import vText, vUid, vUri, vXmlReference from .conftest import NOW_UTC, UID_DEFAULT if TYPE_CHECKING: from collections.abc import Callable UTC = timezone.utc # Test parametrization param_summary_components = pytest.mark.parametrize( "component", [Event, Todo, Alarm, Journal, Available, Availability] ) param_description_components = pytest.mark.parametrize( "component", [Event, Todo, Alarm, Journal, Available, Availability] ) COMPONENTS_DTSTAMP = { Component, Event, Journal, Todo, FreeBusy, Available, Availability, } COMPONENTS_DTSTAMP_AUTOMATIC = {Event, Journal, Todo, FreeBusy, Available, Availability} COMPONENTS_UID_AUTOMATIC = {Event, Todo, Journal, Available, Availability, Calendar} COMPONENTS_UID = {Event, Todo, Journal, Alarm, Calendar, Available, Availability} COMPONENTS_SEQUENCE = {Event, Todo, Journal, Availability} COMPONENTS_CATEGORIES = {Event, Journal, Todo, Calendar, Availability, Available} COMPONENTS_ORGANIZER = {Availability, Event, FreeBusy, Journal, Todo} COMPONENTS_LOCATION = {Availability, Available, Event, Todo} COMPONENTS_URL = {Availability, Event, Todo, Journal, FreeBusy, Calendar} COMPONENTS_BUSYTYPE = {Availability} COMPONENTS_DESCRIPTION = { Event, Todo, Journal, Alarm, Available, Availability, Calendar, } COMPONENTS_SUMMARY = {Event, Todo, Journal, Alarm, Available, Availability} COMPONENTS_COMMENT = { Event, Todo, Journal, FreeBusy, Available, Availability, } # Standard and Daylight COMPONENTS_PRIORITY = {Event, Todo, Availability} COMPONENTS_CONTACT = {Event, Todo, Journal, FreeBusy, Available, Availability} COMPONENTS_START_END = {Event, Todo, FreeBusy, Available, Availability} COMPONENTS_STATUS = {Event, Todo, Journal} COMPONENTS_REQUEST_STATUS = {Event, Todo, Journal, FreeBusy} COMPONENTS_ATTENDEES = {Event, Todo, Journal, Alarm} # RFC 9253 properties are defines on ALL # So, if you add new components, do not forget to add them here. COMPONENTS_LINKS = COMPONENTS_RELATED_TO = COMPONENTS_CONCEPTS = COMPONENTS_REFID = { Alarm, Availability, Available, Calendar, Event, FreeBusy, Journal, Todo, } @param_summary_components def test_summary_default(component): """Test the summary property default.""" c = component() assert c.summary is None @param_description_components def test_description_default(component): """Test the summary property default.""" c = component() assert c.description is None @param_summary_components def test_summary_delete(component): """Test the summary property default.""" c = component() c.summary = "alksdj" del c.summary assert c.summary is None del c.summary assert c.summary is None @param_description_components def test_description_delete(component): """Test the summary property default.""" c = component() c.description = "alksdj" del c.description assert c.description is None del c.description assert c.description is None param_summary = pytest.mark.parametrize( "summary", ["akshdkjahskjdhas", "This is a more\ncomplex summary.", None] ) param_description = pytest.mark.parametrize( "description", ["akshdkjahskjdhas", "This is a more\ncomplex summary.", None] ) def assert_summary_equals(component: Component, summary: str | None): """Check this is the summary.""" assert_property_equals(component, "summary", summary) def assert_description_equals(component: Component, summary: str | None): """Check this is the summary.""" assert_property_equals(component, "description", summary) def assert_property_equals(component: Component, name: str, text: str | None): """Check the property.""" assert getattr(component, name.lower()) == text if text is None: assert name.upper() not in component else: assert component[name.upper()] == text @param_summary_components @param_summary def test_set_summary(component, summary): """Test the summary property default.""" c = component() c.summary = summary assert_summary_equals(c, summary) @param_summary_components @param_summary def test_new_with_summary(component, summary, dont_validate_new): """Test the summary property default.""" assert_summary_equals(component.new(summary=summary), summary) @param_description_components @param_description def test_set_description(component, description): """Test the description property default.""" c = component() c.description = description assert_description_equals(c, description) @param_description_components @param_description def test_new_with_description(component, description, dont_validate_new): """Test the description property default.""" assert_description_equals(component.new(description=description), description) def new_journal_description(description) -> Journal: """use new()""" return Journal.new(description=description) def set_journal_description(description) -> Journal: """Set the description""" journal = Journal() journal.descriptions = description return journal @pytest.mark.parametrize( ("description", "expected_description"), [ (None, []), ([], []), ("one description", ["one description"]), (("desc12", "desc23"), ["desc12", "desc23"]), ], ) @pytest.mark.parametrize( "get_journal", [new_journal_description, set_journal_description] ) def test_journal_description_is_a_list( get_journal: Callable[..., Journal], description, expected_description ): """A jounal entry can have several descriptions.""" journal = get_journal(description) assert journal.descriptions == expected_description if not description: assert "DESCRIPTION" not in journal else: assert "DESCRIPTION" in journal def test_multiple_descriptions_are_concatenated(): """For compatibility we also provide the description method that concatenates descriptions.""" journal = Journal.new(description=("one description", "two descriptions")) assert journal.description == "one description\r\n\r\ntwo descriptions" def component_setter( component_class: type[Component], property_name: str, value: Any ) -> Component: """We set the value of the component's attribute.""" component = component_class() try: setattr(component, property_name, value) except TypeError: # we show the exception in case the test fails for easier debug traceback.print_exc() pytest.skip("Cannot set value") return component def component_with_new( component_class: type[Component], property_name: str, value: Any ) -> Component: """Use new() to create the component.""" kw = {property_name.lower(): value} return component_class.new(**kw) def assert_component_attribute_has_value( component: Component, property_name: str, expected_value: Any, message ): """Make sure that a component's attribute has a value.""" actual_value = getattr(component, property_name) assert actual_value == expected_value, ( f"The attribute {property_name} of {component} " f"should be {expected_value!r} but got {actual_value!r}.\n{message}" ) automatic_time_test_cases = list( itertools.chain.from_iterable( [ ( COMPONENTS_DTSTAMP.copy(), property_name, key, date(2023, 10, 21), datetime(2023, 10, 21, tzinfo=timezone.utc), True, f"{key} becomes a UTC value 1", ), ( COMPONENTS_DTSTAMP.copy(), property_name, key, datetime(2023, 10, 22), datetime(2023, 10, 22, tzinfo=timezone.utc), True, f"{key} becomes a UTC value 2", ), ( COMPONENTS_DTSTAMP.copy(), property_name, key, datetime(2023, 10, 23, 12, 30, tzinfo=timezone.utc), datetime(2023, 10, 23, 12, 30, tzinfo=ZoneInfo("UTC")), True, f"{key} becomes a UTC value 3", ), ( COMPONENTS_DTSTAMP.copy(), property_name, key, datetime(2023, 10, 24, 21, 0, 1, tzinfo=timezone(timedelta(hours=1))), datetime(2023, 10, 24, 20, 0, 1, tzinfo=ZoneInfo("UTC")), True, f"{key} becomes a UTC value 4", ), ( COMPONENTS_DTSTAMP_AUTOMATIC.copy(), property_name, key, None, NOW_UTC, key == "DTSTAMP", f"we use the current time to create a datetime for {key}", ), ( COMPONENTS_DTSTAMP - COMPONENTS_DTSTAMP_AUTOMATIC, property_name, key, None, None, False, f"{key} is not automatically set", ), ] for property_name, key in ( ("stamp", "DTSTAMP"), ("created", "CREATED"), ("last_modified", "LAST-MODIFIED"), ) ) ) for ( component_classes, property_name, _key, _value, _expected_value, _expected, _message, ) in automatic_time_test_cases: if property_name != "stamp" and FreeBusy in component_classes: # FreeBusy only has stamp. component_classes.remove(FreeBusy) attendee1 = vCalAddress("mailto:attendee1@example.com") attendee2 = vCalAddress("mailto:attendee2@test.test") new_test_cases = [ ( COMPONENTS_UID, "uid", "uid", "test UID", "test UID", True, "Set the UID property", ), ( COMPONENTS_UID_AUTOMATIC, "uid", "uid", None, UID_DEFAULT, True, "Set the UID property by default for these components", ), ( COMPONENTS_UID - COMPONENTS_UID_AUTOMATIC, "uid", "uid", None, "", False, "UID is not automatically set", ), ( {Event, Todo, Journal, Availability, Available, FreeBusy}, "start", "dtstart", datetime(2023, 10, 24, 21, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), datetime(2023, 10, 24, 21, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), True, "set the start", ), ( {Event, Available, FreeBusy}, "end", "dtend", datetime(2023, 10, 24, 22, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), datetime(2023, 10, 24, 22, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), True, "set the end", ), ( {Todo}, "end", "due", datetime(2023, 10, 24, 22, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), datetime(2023, 10, 24, 22, 0, 1, tzinfo=ZoneInfo("Europe/Berlin")), True, "set the end", ), ( {Event, Todo, Journal, Available}, "RECURRENCE_ID", "RECURRENCE-ID", datetime(2025, 4, 28, 16, 5, tzinfo=ZoneInfo("UTC")), datetime(2025, 4, 28, 16, 5, tzinfo=ZoneInfo("UTC")), True, "set the recurrence_id", ), ( {Event, Todo, Journal, Available}, "RECURRENCE_ID", "RECURRENCE-ID", None, None, False, "no recurrence_id by default", ), ( {Todo, Event, Calendar, Journal}, "color", "color", "red", "red", True, "set the color", ), ( COMPONENTS_SEQUENCE, "sequence", "SEQUENCE", 1, 1, True, "set the sequence", ), ( COMPONENTS_SEQUENCE, "sequence", "SEQUENCE", None, 0, False, "get the default the sequence", ), ( COMPONENTS_CATEGORIES, "categories", "CATEGORIES", ["cat1", "cat2"], ["cat1", "cat2"], True, "set the categories", ), ( COMPONENTS_CATEGORIES, "categories", "CATEGORIES", (), [], False, "categories are absent", ), ( COMPONENTS_ORGANIZER, "organizer", "ORGANIZER", "mailto:bernard@example.com", "mailto:bernard@example.com", True, "set the organizer", ), ( COMPONENTS_ORGANIZER, "organizer", "ORGANIZER", None, None, False, "no organizer by default", ), ( COMPONENTS_LOCATION, "location", "LOCATION", "Berlin", "Berlin", True, "set the location", ), ( COMPONENTS_LOCATION, "location", "LOCATION", None, None, False, "no location by default", ), ( COMPONENTS_URL, "url", "URL", "https://icalendar.readthedocs.io/", "https://icalendar.readthedocs.io/", True, "set the url", ), ( COMPONENTS_URL, "url", "URL", None, "", False, "no url by default", ), ( COMPONENTS_BUSYTYPE, "busy_type", "BUSYTYPE", BUSYTYPE.BUSY, BUSYTYPE.BUSY, True, "set the url", ), ( COMPONENTS_BUSYTYPE, "busy_type", "BUSYTYPE", None, BUSYTYPE.BUSY_UNAVAILABLE, False, "default busy type", ), ( COMPONENTS_DESCRIPTION, "description", "DESCRIPTION", "This describes the component a bit more in detail.", "This describes the component a bit more in detail.", True, "set the description", ), ( COMPONENTS_DESCRIPTION, "description", "DESCRIPTION", None, None, False, "no description by default", ), ( COMPONENTS_SUMMARY, "summary", "SUMMARY", "component summary", "component summary", True, "set the summary", ), ( COMPONENTS_SUMMARY, "summary", "SUMMARY", None, None, False, "no summary by default", ), ( COMPONENTS_PRIORITY, "priority", "PRIORITY", 8, 8, True, "priority is set", ), ( COMPONENTS_PRIORITY, "priority", "PRIORITY", None, 0, False, "no priority by default", ), ( COMPONENTS_COMMENT, "comments", "COMMENT", None, [], False, "no comments", ), ( COMPONENTS_COMMENT, "comments", "COMMENT", "This is a comment", ["This is a comment"], True, "one comment is set", ), ( COMPONENTS_COMMENT, "comments", "COMMENT", ["one", "two", "three"], ["one", "two", "three"], True, "several comments", ), ( COMPONENTS_CONTACT, "contacts", "CONTACT", None, [], False, "no contacts", ), ( COMPONENTS_CONTACT, "contacts", "CONTACT", "This is a comment", ["This is a comment"], True, "one contact is set", ), ( COMPONENTS_CONTACT, "contacts", "CONTACT", ["one", "two", "three"], ["one", "two", "three"], True, "several contacts", ), ( {Event}, "transparency", "TRANSP", "TRANSPARENT", "TRANSPARENT", True, "value set to TRANSPARENT", ), ( {Event}, "transparency", "TRANSP", "OPAQUE", "OPAQUE", True, "value set to OPAQUE", ), ( {Event}, "transparency", "TRANSP", None, "OPAQUE", False, "value is not set, defaults to OPAQUE", ), ( COMPONENTS_STATUS, "status", "STATUS", "CANCELLED", "CANCELLED", True, "value set to CANCELLED", ), ( COMPONENTS_STATUS, "status", "STATUS", "NEEDS-ACTION", "NEEDS-ACTION", True, "value set to NEEDS-ACTION", ), ( COMPONENTS_STATUS, "status", "STATUS", None, "", False, "value is not set", ), ( COMPONENTS_STATUS, "status", "STATUS", "", "", False, "value is not set", ), ( COMPONENTS_ATTENDEES, "attendees", "ATTENDEE", [], [], True, "value is not set", ), ( COMPONENTS_ATTENDEES, "attendees", "ATTENDEE", None, [], False, "value is empty", ), ( COMPONENTS_ATTENDEES, "attendees", "ATTENDEE", [attendee1], [attendee1], True, "one attendee", ), ( COMPONENTS_ATTENDEES, "attendees", "ATTENDEE", attendee2, [attendee2], True, "one attendee", ), ( COMPONENTS_ATTENDEES, "attendees", "ATTENDEE", [attendee1, attendee2], [attendee1, attendee2], True, "two attendees", ), ] rfc_7986_test_cases = [ ( {Calendar}, "last_modified", "LAST-MODIFIED", datetime(2011, 10, 5, 13, 32, 25), datetime(2011, 10, 5, 13, 32, 25, tzinfo=UTC), True, "value set to 2011-10-05T13:32:25 UTC", ), ( {Calendar}, "last_modified", "LAST-MODIFIED", None, None, False, "value is not set", ), ( {Calendar}, "source", "SOURCE", "https://github.com/collective/icalendar", "https://github.com/collective/icalendar", True, "setting a string", ), ( {Calendar}, "refresh_interval", "REFRESH-INTERVAL", timedelta(hours=1), timedelta(hours=1), True, "setting a timedelta", ), ( {Calendar}, "refresh_interval", "REFRESH-INTERVAL", None, None, False, "setting nothing", ), ] rfc_9253_link_values = [ vUri("https://123"), vUid("123-123-123"), vXmlReference("http://example.com"), ] rfc_9253_related_to_values = [ vUri("https://123"), vUid("123-123-123"), vText("nananana", params={"RELTYPE": "SIBLING"}), ] rfc_9253_test_cases = [ ( COMPONENTS_LINKS, "links", "LINK", None, [], False, "setting nothing", ), ( COMPONENTS_LINKS, "links", "LINK", [], [], False, "setting nothing", ), ( COMPONENTS_LINKS, "links", "LINK", ["https://123"], [vUri("https://123")], True, "set one value", ), ( COMPONENTS_LINKS, "links", "LINK", rfc_9253_link_values, rfc_9253_link_values, True, "set several values", ), ( COMPONENTS_RELATED_TO, "related_to", "RELATED-TO", None, [], False, "setting nothing", ), ( COMPONENTS_RELATED_TO, "related_to", "RELATED-TO", [], [], False, "setting nothing", ), ( COMPONENTS_RELATED_TO, "related_to", "RELATED-TO", ["https://123"], [vText("https://123")], True, "set one value", ), ( COMPONENTS_RELATED_TO, "related_to", "RELATED-TO", rfc_9253_related_to_values, rfc_9253_related_to_values, True, "set several values", ), ( COMPONENTS_CONCEPTS, "concepts", "CONCEPT", None, [], False, "setting nothing", ), ( COMPONENTS_CONCEPTS, "concepts", "CONCEPT", [], [], False, "setting nothing", ), ( COMPONENTS_CONCEPTS, "concepts", "CONCEPT", ["https://123", vUri("https://asd")], [vUri("https://123"), vUri("https://asd")], True, "set two values", ), ( COMPONENTS_REFID, "refids", "REFID", None, [], False, "setting nothing", ), ( COMPONENTS_REFID, "refids", "REFID", [], [], False, "setting nothing", ), ( COMPONENTS_REFID, "refids", "REFID", "itinerary-2014-11-17", ["itinerary-2014-11-17"], True, "set a value", ), ( COMPONENTS_REFID, "refids", "REFID", ["itinerary-2014-11-17", "itinerary-2014-11-17-2"], ["itinerary-2014-11-17", "itinerary-2014-11-17-2"], True, "set two values", ), ( COMPONENTS_REQUEST_STATUS, "REQUEST_STATUS", "REQUEST-STATUS", None, [], False, "setting nothing", ), ( COMPONENTS_REQUEST_STATUS, "REQUEST_STATUS", "REQUEST-STATUS", [], [], False, "setting nothing", ), ( COMPONENTS_REQUEST_STATUS, "REQUEST_STATUS", "REQUEST-STATUS", "2.0;Success", ["2.0;Success"], True, "set a value", ), ( COMPONENTS_REQUEST_STATUS, "REQUEST_STATUS", "REQUEST-STATUS", ["2.0;Success", "3.1;Invalid property value"], ["2.0;Success", "3.1;Invalid property value"], True, "set two values", ), ] @pytest.mark.parametrize( ( "component_classes", "property_name", "key", "initial_value", "expected_value", "key_present", "message", ), automatic_time_test_cases + new_test_cases + rfc_7986_test_cases + rfc_9253_test_cases, ) @pytest.mark.parametrize( "create_component_with_property", [component_setter, component_with_new] ) def test_properties_and_new( create_component_with_property, component_classes, property_name, initial_value, expected_value, key_present, key, message, dont_validate_new, ): """We set and get the dtstamp.""" for component_class in component_classes: print( "processing:", component_class.name, "->", property_name, "=", initial_value, ":", message, ) component = create_component_with_property( component_class, property_name, initial_value ) if ( create_component_with_property is component_with_new or initial_value is not None ): assert (key in component) == key_present, message # the setter does not create default values # so we only check it if present assert_component_attribute_has_value( component, property_name, expected_value, message ) @pytest.mark.parametrize( ("factory_method", "required_kwargs"), [ ("new", {}), ( "new_display", {"description": "Reminder", "trigger": timedelta(minutes=-5)}, ), ("new_audio", {"trigger": timedelta(minutes=-5)}), ( "new_email", { "summary": "Reminder", "description": "The event starts soon.", "trigger": timedelta(minutes=-5), "attendees": [vCalAddress("mailto:user@example.com")], }, ), ], ids=["new", "new_display", "new_audio", "new_email"], ) @pytest.mark.parametrize( ("property_name", "value", "expected_value"), [ ("uid", "alarm-uid", vText("alarm-uid")), ("links", ["https://example.com/link"], [vUri("https://example.com/link")]), ("related_to", ["parent-uid"], [vText("parent-uid")]), ("refids", ["reference-id"], [vText("reference-id")]), ( "concepts", ["https://example.com/concept"], [vUri("https://example.com/concept")], ), ], ) def test_alarm_factory_methods_set_shared_properties( factory_method, required_kwargs, property_name, value, expected_value ): """All Alarm factory methods set their shared properties.""" alarm = getattr(Alarm, factory_method)(**required_kwargs, **{property_name: value}) assert getattr(alarm, property_name) == expected_value @pytest.mark.parametrize("component_class", COMPONENTS_START_END) def test_end_must_be_after_start(tzp, component_class): """The end must be after the start.""" with pytest.raises(ValueError) as e: component_class.new( start=tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)), end=tzp.localize_utc(datetime(2011, 10, 5, 12, 32, 25)), ) assert "end must be after start" in str(e.value) @pytest.mark.parametrize("component_class", COMPONENTS_START_END) def test_start_and_end_can_be_the_same(tzp, component_class): """The end must be after the start.""" start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) c = component_class.new(start=start, end=start) assert c.start == start assert c.end == start assert c.duration == timedelta(0) def test_journal_start(): """Journal does not have an end.""" j = Journal.new(start=datetime(2011, 10, 5, 13, 32, 25)) assert j.start == datetime(2011, 10, 5, 13, 32, 25) def test_modify_attendees(): """We modify the list and see of that works.""" attendees = [attendee1] j = Journal.new(attendees=attendees) j.attendees.append(attendee2) assert j.attendees == [attendee1, attendee2] ics = j.to_ical().decode() assert str(attendee1) in ics assert str(attendee2) in ics def test_modify_default_attendees(): """Empty attendees need to be modifiable.""" todo = Todo.new() todo.attendees.append(attendee1) assert todo.attendees == [attendee1] ics = todo.to_ical().decode() assert str(attendee1) in ics def test_empty_attendees_to_ical(): """We should be able to have no attendees set.""" e = Event.new() e.attendees = [] assert "ATTENDEE" not in e.to_ical().decode() def test_modify_empty_attendees_1(): """Empty value set and modified.""" e = Event() attendees = [] e.attendees = attendees e.attendees.append(attendee1) assert e.attendees == [attendee1] == attendees def test_modify_empty_attendees_2(): """Empty value set and modified.""" e = Event() attendees = [] e.attendees = attendees attendees.append(attendee1) assert e.attendees == [attendee1] == attendees collective-icalendar-138c845/src/icalendar/tests/test_issue_844_timezone_placement.py000066400000000000000000000161131524134033600310240ustar00rootroot00000000000000"""Tests for issue #844: timezone placement in calendar components.""" import uuid from datetime import datetime from zoneinfo import ZoneInfo from icalendar import Calendar, Event from icalendar.cal.timezone import Timezone def test_single_timezone_placed_before_event(): """Test that single timezone is placed before event when using `add_missing_timezones()`.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") event = Event() event.add("UID", str(uuid.uuid4())) event.start = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/London")) event.add("SUMMARY", "Test Event") calendar.add_component(event) calendar.add_missing_timezones() components = [comp.name for comp in calendar.subcomponents] assert components == ["VTIMEZONE", "VEVENT"] # Verify the timezone is correct assert calendar.timezones[0].tz_name == "Europe/London" def test_multiple_timezones_placed_before_events(): """Test that multiple timezones are placed before events.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") event1 = Event() event1.add("UID", str(uuid.uuid4())) event1.start = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/London")) event1.add("SUMMARY", "London Event") calendar.add_component(event1) event2 = Event() event2.add("UID", str(uuid.uuid4())) event2.start = datetime(2026, 3, 19, 15, 30, tzinfo=ZoneInfo("America/New_York")) event2.add("SUMMARY", "New York Event") calendar.add_component(event2) calendar.add_missing_timezones() components = [comp.name for comp in calendar.subcomponents] assert components[:2] == ["VTIMEZONE", "VTIMEZONE"] assert components[2:] == ["VEVENT", "VEVENT"] # Verify both timezones are present timezone_names = {tz.tz_name for tz in calendar.timezones} assert timezone_names == {"Europe/London", "America/New_York"} def test_existing_timezone_preserved_new_ones_added_correctly(): """Test that existing timezones are preserved and new ones added in correct position.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") # Add existing timezone manually existing_tz = Timezone.from_tzid("Europe/Berlin") calendar.add_component(existing_tz) event1 = Event() event1.add("UID", str(uuid.uuid4())) event1.start = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/Berlin")) event1.add("SUMMARY", "Berlin Event") calendar.add_component(event1) event2 = Event() event2.add("UID", str(uuid.uuid4())) event2.start = datetime(2026, 3, 19, 15, 30, tzinfo=ZoneInfo("America/New_York")) event2.add("SUMMARY", "New York Event") calendar.add_component(event2) calendar.add_missing_timezones() components = [comp.name for comp in calendar.subcomponents] assert components[:2] == ["VTIMEZONE", "VTIMEZONE"] assert components[2:] == ["VEVENT", "VEVENT"] # Verify both timezones are present (existing + new) timezone_names = {tz.tz_name for tz in calendar.timezones} assert timezone_names == {"Europe/Berlin", "America/New_York"} def test_no_missing_timezones_no_changes(): """Test that method handles case with no missing timezones. Gracefully.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") event = Event() event.add("UID", str(uuid.uuid4())) event.start = datetime(2026, 3, 19, 12, 30) # No timezone event.add("SUMMARY", "No Timezone Event") calendar.add_component(event) original_components = [comp.name for comp in calendar.subcomponents] calendar.add_missing_timezones() new_components = [comp.name for comp in calendar.subcomponents] assert original_components == new_components == ["VEVENT"] def test_empty_calendar_add_missing_timezones(): """Test that `add_missing_timezones()` handles empty calendar correctly.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") calendar.add_missing_timezones() components = [comp.name for comp in calendar.subcomponents] assert components == [] def test_timezone_placement_in_serialized_output(): """Test that timezones appear before events in serialized iCalendar output.""" calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") event = Event() event.add("UID", str(uuid.uuid4())) event.start = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/London")) event.add("SUMMARY", "Test Event") calendar.add_component(event) calendar.add_missing_timezones() # Check serialized output ical_content = calendar.to_ical().decode() lines = ical_content.split("\n") vtimezone_line = None vevent_line = None for i, line in enumerate(lines): if line.strip() == "BEGIN:VTIMEZONE": vtimezone_line = i elif line.strip() == "BEGIN:VEVENT": vevent_line = i assert vtimezone_line is not None assert vevent_line is not None assert vtimezone_line < vevent_line def test_forward_reference_compatibility(): """Test that our fix doesn't break forward reference parsing.""" # Create a calendar with forward reference (VEVENT before VTIMEZONE) ical_content = """BEGIN:VCALENDAR VERSION:2.0 PRODID:test BEGIN:VEVENT UID:test-event DTSTART;TZID=Europe/London:20260319T123000 SUMMARY:Test Event END:VEVENT BEGIN:VTIMEZONE TZID:Europe/London BEGIN:STANDARD DTSTART:20001029T020000 TZOFFSETFROM:+0100 TZOFFSETTO:+0000 TZNAME:GMT END:STANDARD END:VTIMEZONE END:VCALENDAR""" # This should still parse correctly due to existing forward reference handling calendar = Calendar.from_ical(ical_content) assert len(calendar.events) == 1 assert len(calendar.timezones) == 1 assert calendar.timezones[0].tz_name == "Europe/London" def test_timezone_placement_without_pytz(): """Test timezone placement. Works even if pytz is not available.""" import sys # Temporarily mock pytz to simulate nopytz environment original_pytz = sys.modules.get("pytz") class MockPytz: def __getattr__(self, name): raise ImportError("No module named 'pytz'") sys.modules["pytz"] = MockPytz() try: calendar = Calendar() calendar.add("VERSION", "2.0") calendar.add("PRODID", "test calendar") event = Event() event.add("UID", str(uuid.uuid4())) event.start = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/London")) event.add("SUMMARY", "Test Event") calendar.add_component(event) calendar.add_missing_timezones() components = [comp.name for comp in calendar.subcomponents] assert components == ["VTIMEZONE", "VEVENT"] assert len(calendar.timezones) == 1 assert calendar.timezones[0].tz_name == "Europe/London" finally: # Restore original pytz state if original_pytz: sys.modules["pytz"] = original_pytz else: sys.modules.pop("pytz", None) collective-icalendar-138c845/src/icalendar/tests/test_issue_857_enhanced_api.py000066400000000000000000000160331524134033600275450ustar00rootroot00000000000000"""Tests for issue #857: enhanced API usability improvements.""" from datetime import datetime, timedelta from zoneinfo import ZoneInfo import pytest from icalendar import Calendar, Event from icalendar.error import IncompleteComponent def test_calendar_new_with_organization_and_language(): """Test Calendar.new() with organization and language. Generates proper prodid.""" calendar = Calendar.new(organization="foo.org", name="My parties", language="en") assert calendar.calendar_name == "My parties" assert calendar.prodid == "-//foo.org//My parties//EN" def test_calendar_new_with_organization_no_language(): """Test Calendar.new() with organization but no language. Defaults to EN.""" calendar = Calendar.new(organization="example.com", name="Test Calendar") assert calendar.prodid == "-//example.com//Test Calendar//EN" def test_calendar_new_with_organization_no_name(): """Test Calendar.new() with organization but no name. Uses 'Calendar'.""" calendar = Calendar.new(organization="example.com", language="fr") assert calendar.prodid == "-//example.com//Calendar//FR" def test_calendar_new_explicit_prodid_overrides_organization(): """Test that explicit prodid overrides organization-based generation.""" calendar = Calendar.new( organization="foo.org", name="My parties", language="en", prodid="-//Custom//Product//ID", ) assert calendar.prodid == "-//Custom//Product//ID" def test_calendar_new_no_organization_uses_default_prodid(): """Test that without organization, default prodid is used.""" calendar = Calendar.new(name="Test Calendar") # Should use the default icalendar prodid assert "collective" in calendar.prodid assert "icalendar" in calendar.prodid def test_event_duration_setter(): """Test that setting event duration. Correctly calculates end time.""" event = Event.new(summary="Test Meeting", start=datetime(2026, 3, 19, 12, 30)) # Set duration event.duration = timedelta(hours=2) assert event.duration == timedelta(hours=2) assert event.end == datetime(2026, 3, 19, 14, 30) def test_event_duration_setter_with_timezone(): """Test duration setter. Works with timezone-aware events.""" start_time = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("Europe/London")) event = Event.new(summary="Test Meeting", start=start_time) event.duration = timedelta(minutes=90) expected_end = start_time + timedelta(minutes=90) assert event.duration == timedelta(minutes=90) assert event.end == expected_end def test_event_duration_setter_without_start_raises_error(): """Test that setting duration without start time. Raises error.""" event = Event.new(summary="Incomplete Event") with pytest.raises( IncompleteComponent, match="Cannot set duration without DTSTART" ): event.duration = timedelta(hours=1) def test_calendar_validate_missing_prodid(): """Test that validate() detects missing PRODID.""" calendar = Calendar() calendar.add("VERSION", "2.0") event = Event.new(summary="Test", start=datetime(2026, 3, 19, 12, 30)) calendar.add_component(event) with pytest.raises(IncompleteComponent, match="Calendar must have a PRODID"): calendar.validate() def test_calendar_validate_missing_version(): """Test that validate() detects missing VERSION.""" calendar = Calendar() calendar.add("PRODID", "-//Test//Test//EN") event = Event.new(summary="Test", start=datetime(2026, 3, 19, 12, 30)) calendar.add_component(event) with pytest.raises(IncompleteComponent, match="Calendar must have a VERSION"): calendar.validate() def test_calendar_validate_no_components(): """Test that validate() detects empty calendar.""" calendar = Calendar() calendar.add("PRODID", "-//Test//Test//EN") calendar.add("VERSION", "2.0") with pytest.raises( IncompleteComponent, match="Calendar must contain at least one component" ): calendar.validate() def test_calendar_validate_passes_with_valid_calendar(): """Test that validate() succeeds with valid calendar.""" calendar = Calendar.new(name="Valid Calendar") event = Event.new( summary="Test Event", start=datetime(2026, 3, 19, 12, 30), end=datetime(2026, 3, 19, 14, 30), ) calendar.add_component(event) # Should not raise any exception calendar.validate() # to_ical should still work normally ical_output = calendar.to_ical() assert b"BEGIN:VCALENDAR" in ical_output assert b"BEGIN:VEVENT" in ical_output def test_user_desired_workflow_from_issue_857(): """Test the exact workflow desired by the user in issue #857.""" # User's ideal API: Calendar(organization="foo.org", name="My parties", language="en") cal = Calendar.new(organization="foo.org", name="My parties", language="en") # User's ideal API: Event(start=mydatetime) mydatetime = datetime(2026, 3, 19, 12, 30, tzinfo=ZoneInfo("UTC")) event = Event.new(start=mydatetime) # User wanted: event.end = mydatetime + timedelta(minutes=15) event.duration = timedelta(minutes=15) # This achieves the same result # User wanted: event.name = "Some string" (summary works as alias) event.summary = "Some string" # Verify the result assert cal.calendar_name == "My parties" assert cal.prodid == "-//foo.org//My parties//EN" assert event.start == mydatetime assert event.end == mydatetime + timedelta(minutes=15) assert event.summary == "Some string" assert event.duration == timedelta(minutes=15) # Add event to calendar and generate valid output cal.add_component(event) ical_output = cal.to_ical() assert b"PRODID:-//foo.org//My parties//EN" in ical_output def test_enhanced_api_compared_to_original_example(): """Test that enhanced API is much simpler than original example from issue.""" # Original complex example from issue #857: # Required knowing: PRODID, VERSION, SUMMARY, DTSTART, DTEND, DTSTAMP fields # Required manual timezone handling # Enhanced API - much simpler: cal = Calendar.new(organization="example.com", name="My calendar") event = Event.new( summary="Python meeting about calendaring", start=datetime(2005, 4, 4, 8, 0, 0, tzinfo=ZoneInfo("UTC")), ) event.duration = timedelta(hours=2) # Much simpler than calculating DTEND cal.add_component(event) # Automatic timezone handling cal.add_missing_timezones() ical_output = cal.to_ical().decode() # Verify key improvements: # 1. Automatic PRODID generation from organization assert "-//example.com//My calendar//EN" in ical_output # 2. Automatic VERSION and other required fields assert "VERSION:2.0" in ical_output # 3. Automatic UID and DTSTAMP generation (from Event.new()) assert "UID:" in ical_output assert "DTSTAMP:" in ical_output # 4. Proper timezone handling and duration usage assert "DTSTART" in ical_output assert ( "DURATION" in ical_output ) # Uses DURATION instead of DTEND when duration is set collective-icalendar-138c845/src/icalendar/tests/test_issue_860_improved_setters.py000066400000000000000000000362731524134033600305470ustar00rootroot00000000000000"""Tests for issue #860: Improved setters for start/duration/end. This test suite defines the desired behavior for the improved setter implementations as described in https://github.com/collective/icalendar/issues/860 The tests ensure RFC 5545 compliance, proper property locking mechanisms, and backward compatibility with existing code. """ from datetime import date, datetime, timedelta import pytest from icalendar import Event, Todo from icalendar.error import IncompleteComponent, InvalidCalendar class TestDurationSetter: """Test the new duration setter functionality.""" def test_event_duration_setter_with_start_locked(self): """Test setting duration with start locked (default behavior).""" event = Event() event.add("UID", "test-duration-setter") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # 2 hours initially # Setting duration should keep start locked and adjust end event.duration = timedelta(hours=3) assert event.start == datetime(2026, 1, 1, 12, 0) # start unchanged assert event.end == datetime(2026, 1, 1, 15, 0) # end adjusted assert event.duration == timedelta(hours=3) assert "DURATION" in event # DURATION property should be set assert "DTEND" not in event # DTEND should be removed def test_event_duration_setter_creates_duration_property(self): """Test that setting duration creates DURATION property and removes DTEND.""" event = Event() event.add("UID", "test-duration-property") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Initially has DTEND assert "DTEND" in event assert "DURATION" not in event # Setting duration should switch to DURATION property event.duration = timedelta(hours=1) assert "DURATION" in event assert "DTEND" not in event assert event.duration == timedelta(hours=1) def test_todo_duration_setter_with_start_locked(self): """Test setting duration for Todo with start locked.""" todo = Todo() todo.add("UID", "test-todo-duration") todo.start = datetime(2026, 1, 1, 12, 0) todo.end = datetime(2026, 1, 1, 14, 0) # Setting duration should keep start locked and adjust end todo.duration = timedelta(hours=4) assert todo.start == datetime(2026, 1, 1, 12, 0) # start unchanged assert todo.end == datetime(2026, 1, 1, 16, 0) # end adjusted assert todo.duration == timedelta(hours=4) assert "DURATION" in todo assert "DUE" not in todo def test_duration_setter_without_start_raises_error(self): """Test that setting duration without start raises appropriate error.""" event = Event() event.add("UID", "test-no-start") with pytest.raises(IncompleteComponent): event.duration = timedelta(hours=2) class TestExplicitLockingMethods: """Test the explicit locking methods set_start(), set_end(), set_duration().""" def test_set_duration_with_start_locked(self): """Test set_duration() with start locked (default).""" event = Event() event.add("UID", "test-set-duration") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Default behavior: lock start event.set_duration(timedelta(hours=3)) assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 15, 0) assert event.duration == timedelta(hours=3) def test_set_duration_with_end_locked(self): """Test set_duration() with end locked.""" event = Event() event.add("UID", "test-set-duration-end-locked") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Lock end, adjust start event.set_duration(timedelta(hours=3), locked="end") assert event.start == datetime(2026, 1, 1, 11, 0) # start adjusted assert event.end == datetime(2026, 1, 1, 14, 0) # end unchanged assert event.duration == timedelta(hours=3) def test_set_duration_convert_to_duration_property(self): """Test set_duration() with None to convert to DURATION property.""" event = Event() event.add("UID", "test-convert-duration") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Convert to DURATION property event.set_duration(None) assert "DURATION" in event assert "DTEND" not in event assert event.duration == timedelta(hours=2) def test_set_start_with_duration_locked(self): """Test set_start() with duration locked.""" event = Event() event.add("UID", "test-set-start") event.start = datetime(2026, 1, 1, 12, 0) event.add("DURATION", timedelta(hours=2)) # Default behavior when DURATION exists: lock duration event.set_start(datetime(2026, 1, 1, 10, 0)) assert event.start == datetime(2026, 1, 1, 10, 0) assert event.end == datetime(2026, 1, 1, 12, 0) # end adjusted assert event.duration == timedelta(hours=2) # duration unchanged def test_set_start_with_end_locked(self): """Test set_start() with end locked.""" event = Event() event.add("UID", "test-set-start-end-locked") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Lock end, adjust duration event.set_start(datetime(2026, 1, 1, 10, 0), locked="end") assert event.start == datetime(2026, 1, 1, 10, 0) assert event.end == datetime(2026, 1, 1, 14, 0) # end unchanged assert event.duration == timedelta(hours=4) # duration adjusted def test_set_end_with_start_locked(self): """Test set_end() with start locked (default).""" event = Event() event.add("UID", "test-set-end") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Default behavior: lock start event.set_end(datetime(2026, 1, 1, 16, 0)) assert event.start == datetime(2026, 1, 1, 12, 0) # start unchanged assert event.end == datetime(2026, 1, 1, 16, 0) assert event.duration == timedelta(hours=4) # duration adjusted def test_set_end_with_duration_locked(self): """Test set_end() with duration locked.""" event = Event() event.add("UID", "test-set-end-duration-locked") event.start = datetime(2026, 1, 1, 12, 0) event.add("DURATION", timedelta(hours=2)) # Lock duration, adjust start event.set_end(datetime(2026, 1, 1, 16, 0), locked="duration") assert event.start == datetime(2026, 1, 1, 14, 0) # start adjusted assert event.end == datetime(2026, 1, 1, 16, 0) assert event.duration == timedelta(hours=2) # duration unchanged class TestImprovedPropertySetters: """Test the improved property setters with smart defaults.""" def test_start_setter_with_duration_property(self): """Test start setter when DURATION property exists (should lock duration).""" event = Event() event.add("UID", "test-start-duration") event.start = datetime(2026, 1, 1, 12, 0) event.add("DURATION", timedelta(hours=2)) # Smart default: lock duration when DURATION property exists event.start = datetime(2026, 1, 1, 10, 0) assert event.start == datetime(2026, 1, 1, 10, 0) assert event.end == datetime(2026, 1, 1, 12, 0) assert event.duration == timedelta(hours=2) def test_start_setter_with_dtend_property(self): """Test start setter when DTEND property exists (should lock end).""" event = Event() event.add("UID", "test-start-dtend") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Smart default: lock end when DTEND property exists event.start = datetime(2026, 1, 1, 10, 0) assert event.start == datetime(2026, 1, 1, 10, 0) assert event.end == datetime(2026, 1, 1, 14, 0) assert event.duration == timedelta(hours=4) def test_end_setter_with_duration_property(self): """Test end setter when DURATION property exists (should remove DURATION).""" event = Event() event.add("UID", "test-end-duration") event.start = datetime(2026, 1, 1, 12, 0) event.add("DURATION", timedelta(hours=2)) # Setting end should remove DURATION and set DTEND event.end = datetime(2026, 1, 1, 16, 0) assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 16, 0) assert event.duration == timedelta(hours=4) assert "DTEND" in event assert "DURATION" not in event class TestRFCCompliance: """Test that the improved setters maintain RFC 5545 compliance.""" def test_dtend_and_duration_mutual_exclusion(self): """Test that DTEND and DURATION are mutually exclusive.""" event = Event() event.add("UID", "test-mutual-exclusion") event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) # Setting duration should remove DTEND event.duration = timedelta(hours=3) assert "DURATION" in event assert "DTEND" not in event # Setting end should remove DURATION event.end = datetime(2026, 1, 1, 16, 0) assert "DTEND" in event assert "DURATION" not in event def test_date_duration_validation(self): """Test that date DTSTART with time-based DURATION raises error.""" event = Event() event.add("UID", "test-date-duration") event.start = date(2026, 1, 1) # DATE type # Should raise error for non-day/week duration with pytest.raises(InvalidCalendar): event.duration = timedelta(hours=2) # Should work for day/week duration event.duration = timedelta(days=1) assert event.duration == timedelta(days=1) def test_datetime_type_consistency(self): """Test that DTSTART and DTEND must have same type.""" event = Event() event.add("UID", "test-type-consistency") event.start = datetime(2026, 1, 1, 12, 0) # datetime type # Should raise error for date end with datetime start event.end = date(2026, 1, 1) with pytest.raises(InvalidCalendar): _ = event.end # Error occurs when accessing the property class TestBackwardCompatibility: """Test that the improved setters maintain backward compatibility.""" def test_existing_simple_usage_still_works(self): """Test that existing simple usage patterns still work.""" event = Event() event.add("UID", "test-backward-compat") # Simple usage should still work event.start = datetime(2026, 1, 1, 12, 0) event.end = datetime(2026, 1, 1, 14, 0) assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 14, 0) assert event.duration == timedelta(hours=2) def test_property_access_compatibility(self): """Test that property access remains compatible.""" event = Event() event.add("UID", "test-property-access") event.start = datetime(2026, 1, 1, 12, 0) event.add("DURATION", timedelta(hours=2)) # Property access should work as before assert event.DTSTART is not None assert event.DURATION is not None assert event.DTEND is None # Computed properties should work assert event.start == datetime(2026, 1, 1, 12, 0) assert event.end == datetime(2026, 1, 1, 14, 0) assert event.duration == timedelta(hours=2) class TestErrorConditions: """Test error conditions and edge cases for improved coverage.""" def test_set_duration_invalid_locked_value(self): """Test that invalid locked values raise appropriate errors.""" event = Event() event.add("UID", "test-invalid-locked") event.start = datetime(2026, 1, 1, 12, 0) with pytest.raises( ValueError, match="locked must be 'start' or 'end', not 'invalid'" ): event.set_duration(timedelta(hours=2), locked="invalid") def test_set_start_invalid_locked_value(self): """Test that invalid locked values raise appropriate errors.""" event = Event() event.add("UID", "test-invalid-locked") event.start = datetime(2026, 1, 1, 12, 0) with pytest.raises( ValueError, match="locked must be 'duration', 'end', or None, not 'invalid'" ): event.set_start(datetime(2026, 1, 1, 14, 0), locked="invalid") def test_set_end_invalid_locked_value(self): """Test that invalid locked values raise appropriate errors.""" event = Event() event.add("UID", "test-invalid-locked") event.start = datetime(2026, 1, 1, 12, 0) with pytest.raises( ValueError, match="locked must be 'start' or 'duration', not 'invalid'" ): event.set_end(datetime(2026, 1, 1, 14, 0), locked="invalid") def test_duration_setter_invalid_type(self): """Test that duration setter rejects invalid types.""" event = Event() event.add("UID", "test-invalid-type") event.start = datetime(2026, 1, 1, 12, 0) with pytest.raises(TypeError, match="Use timedelta, not str"): event.duration = "2 hours" def test_set_duration_invalid_type(self): """Test that set_duration rejects invalid types.""" event = Event() event.add("UID", "test-invalid-type") event.start = datetime(2026, 1, 1, 12, 0) with pytest.raises(TypeError, match="Use timedelta, not int"): event.set_duration(7200, locked="start") def test_todo_error_conditions(self): """Test error conditions for Todo class.""" todo = Todo() todo.add("UID", "test-todo-errors") todo.start = datetime(2026, 1, 1, 12, 0) # Test invalid locked values with pytest.raises( ValueError, match="locked must be 'start' or 'end', not 'invalid'" ): todo.set_duration(timedelta(hours=2), locked="invalid") with pytest.raises( ValueError, match="locked must be 'duration', 'end', or None, not 'invalid'" ): todo.set_start(datetime(2026, 1, 1, 14, 0), locked="invalid") with pytest.raises( ValueError, match="locked must be 'start' or 'duration', not 'invalid'" ): todo.set_end(datetime(2026, 1, 1, 14, 0), locked="invalid") def test_set_start_auto_detect_no_existing_properties(self): """Test auto-detection when no existing properties exist.""" event = Event() event.add("UID", "test-auto-detect") # When no DURATION or DTEND exists, should default to duration event.set_start(datetime(2026, 1, 1, 12, 0), locked=None) assert event.start == datetime(2026, 1, 1, 12, 0) # Adding end should trigger duration calculation event.end = datetime(2026, 1, 1, 14, 0) # The auto-detect should have preserved the end time calculation collective-icalendar-138c845/src/icalendar/tests/test_issue_864_examples.py000066400000000000000000000005761524134033600267700ustar00rootroot00000000000000"""Test that we have good examples.""" from icalendar import Event SUMMARY = "This is a Unicode summary. Das müssen wir dekodieren. Р���кий. ↧" def test_summary_is_text(): """The summary should be a text.""" event = Event.new(summary=SUMMARY) assert event.summary == SUMMARY event = Event.from_ical(event.to_ical()) assert event.summary == SUMMARY collective-icalendar-138c845/src/icalendar/tests/test_issue_867_todo_duration_fix.py000066400000000000000000000147771524134033600307050ustar00rootroot00000000000000"""Tests for issue #867: Todo.duration should work without DTSTART.""" from datetime import datetime, timedelta import pytest from icalendar import Event, Todo from icalendar.error import IncompleteComponent def test_todo_duration_without_dtstart(): """Test that Todo.duration works when `DURATION` property is set but `DTSTART` is missing.""" # This is the exact test case from issue #867 my_task = Todo.from_ical("""BEGIN:VTODO UID:taskwithoutdtstart DTSTAMP:20070313T123432Z DURATION:P5D SUMMARY:This is a task that is expected to take five days to complete STATUS:NEEDS-ACTION END:VTODO""") # Should return the DURATION property directly assert my_task.duration == timedelta(days=5) def test_todo_duration_calculated_from_start_and_due(): """Test that Todo.duration still works for calculated duration from `DTSTART` and `DUE`.""" todo = Todo() todo.add("UID", "test-calculated") todo.start = datetime(2026, 3, 19, 12, 0) todo.end = datetime(2026, 3, 19, 15, 30) # Should calculate duration from start and end assert todo.duration == timedelta(hours=3, minutes=30) def test_todo_duration_prefers_duration_property(): """Test that explicit `DURATION` property takes precedence over calculated duration.""" todo = Todo() todo.add("UID", "test-precedence") todo.start = datetime(2026, 3, 19, 12, 0) todo.end = datetime(2026, 3, 19, 15, 0) # This would be 3 hours todo.add("DURATION", timedelta(days=2)) # But DURATION says 2 days # Should return DURATION property, not calculated value assert todo.duration == timedelta(days=2) def test_todo_duration_with_dtstart_and_duration(): """Test Todo with `DTSTART` and `DURATION` (valid per RFC 5545).""" todo = Todo() todo.add("UID", "test-start-duration") todo.start = datetime(2026, 3, 19, 12, 0) todo.add("DURATION", timedelta(hours=4)) # Should return DURATION property assert todo.duration == timedelta(hours=4) def test_todo_duration_without_any_time_info_raises_error(): """Test that Todo.duration raises error when no time information is available.""" todo = Todo() todo.add("UID", "test-no-time") todo.add("SUMMARY", "Task without any time info") # Should raise error since no DURATION, DTSTART, or DUE is set with pytest.raises(IncompleteComponent): _ = todo.duration def test_todo_duration_complex_duration_values(): """Test Todo.duration with various complex duration formats.""" # Test ISO 8601 duration formats test_cases = [ ("PT30M", timedelta(minutes=30)), ("PT1H30M", timedelta(hours=1, minutes=30)), ("P1DT2H", timedelta(days=1, hours=2)), ("P1W", timedelta(weeks=1)), ( "P1Y2M3DT4H5M6S", timedelta(days=428, hours=4, minutes=5, seconds=6), ), # Approx 1 year 2 months ] for duration_str, expected_delta in test_cases: todo = Todo() todo.add("UID", f"test-{duration_str}") todo.add("DURATION", expected_delta) assert todo.duration == expected_delta def test_todo_duration_maintains_backward_compatibility(): """Test that the fix doesn't break existing functionality.""" # Create todo the old way (DTSTART + DUE) todo = Todo() todo.add("UID", "test-backward-compat") todo.add("SUMMARY", "Backward compatibility test") todo.start = datetime(2026, 3, 19, 9, 0) todo.end = datetime(2026, 3, 19, 17, 0) # Should still work as before assert todo.duration == timedelta(hours=8) # Properties should still work assert todo.start == datetime(2026, 3, 19, 9, 0) assert todo.end == datetime(2026, 3, 19, 17, 0) def test_todo_duration_edge_case_only_dtstart(): """Test Todo with only `DTSTART` (no `DUE` or `DURATION`).""" todo = Todo() todo.add("UID", "test-only-start") todo.start = datetime(2026, 3, 19, 12, 0) # This should use the fallback logic from the end property # which returns start + 1 day for date, or just start for datetime assert todo.duration == timedelta(0) # end defaults to start for datetime def test_issue_867_exact_reproduction(): """Test the exact scenario reported in issue #867.""" # This reproduces the caldav project test case that was failing my_task = Todo.from_ical("""BEGIN:VTODO UID:taskwithoutdtstart DTSTAMP:20070313T123432Z DURATION:P5D SUMMARY:This is a task that is expected to take five days to complete STATUS:NEEDS-ACTION END:VTODO""") # This assertion was failing before the fix assert my_task.duration == timedelta(days=5) # Verify the todo has the expected properties assert my_task.get("UID") == "taskwithoutdtstart" assert ( my_task.get("SUMMARY") == "This is a task that is expected to take five days to complete" ) assert "DURATION" in my_task assert "DTSTART" not in my_task # Confirming no DTSTART def test_todo_duration_preserves_property_access(): """Test that direct property access still works as expected.""" todo = Todo() todo.add("UID", "test-property-access") todo.add("DURATION", timedelta(hours=2)) # Direct property access should still work assert todo["DURATION"].dt == timedelta(hours=2) # And our computed property should match assert todo.duration == timedelta(hours=2) # Parametrized tests for both Event and Todo components @pytest.mark.parametrize("component_class", [Event, Todo]) def test_component_duration_prefers_duration_property(component_class): """Test that both Event and Todo prefer `DURATION` property over calculated duration.""" component = component_class() component.add("UID", f"test-{component_class.__name__.lower()}-duration") component.start = datetime(2026, 3, 19, 12, 0) component.end = datetime(2026, 3, 19, 15, 0) # This would be 3 hours component.add("DURATION", timedelta(hours=2)) # But DURATION says 2 hours # Should return DURATION property, not calculated value assert component.duration == timedelta(hours=2) @pytest.mark.parametrize("component_class", [Event, Todo]) def test_component_duration_calculated_fallback(component_class): """Test that both Event and Todo fall back to calculated duration when no `DURATION` property.""" component = component_class() component.add("UID", f"test-{component_class.__name__.lower()}-calculated") component.start = datetime(2026, 3, 19, 12, 0) component.end = datetime(2026, 3, 19, 14, 30) # Should calculate duration from start and end (no DURATION property) assert component.duration == timedelta(hours=2, minutes=30) collective-icalendar-138c845/src/icalendar/tests/test_issue_870_vcaladdress_new.py000066400000000000000000000207221524134033600303060ustar00rootroot00000000000000"""Tests for issue #870: Add new() method to vCalAddress class.""" from datetime import datetime import pytest from icalendar import Event from icalendar.prop import vCalAddress class TestVCalAddressNew: """Test the vCalAddress.new() method functionality.""" def test_new_with_email_only(self): """Test creating vCalAddress with email only.""" addr = vCalAddress.new("test@test.com") assert str(addr) == "mailto:test@test.com" assert len(addr.params) == 0 def test_new_with_mailto_prefix(self): """Test that existing mailto: prefix is preserved.""" addr = vCalAddress.new("mailto:test@test.com") assert str(addr) == "mailto:test@test.com" def test_new_with_case_insensitive_mailto(self): """Test case-insensitive mailto: handling.""" test_cases = [ "MAILTO:test@test.com", "Mailto:test@test.com", "MailTo:test@test.com", "mAiLtO:test@test.com", ] for email in test_cases: addr = vCalAddress.new(email) assert str(addr) == email # Preserve original case def test_new_with_cn_parameter(self): """Test creating vCalAddress with CN parameter.""" addr = vCalAddress.new("test@test.com", cn="Test User") assert str(addr) == "mailto:test@test.com" assert addr.params["CN"] == "Test User" @pytest.mark.parametrize( ("argument", "value", "parameter", "expected_value"), [ ("cutype", "INDIVIDUAL", "CUTYPE", "INDIVIDUAL"), ("cutype", "GROUP", "CUTYPE", "GROUP"), ("cutype", "RESOURCE", "CUTYPE", "RESOURCE"), ("cutype", "ROOM", "CUTYPE", "ROOM"), ("role", "REQ-PARTICIPANT", "ROLE", "REQ-PARTICIPANT"), ("role", "OPT-PARTICIPANT", "ROLE", "OPT-PARTICIPANT"), ("role", "NON-PARTICIPANT", "ROLE", "NON-PARTICIPANT"), ("role", "CHAIR", "ROLE", "CHAIR"), ("partstat", "NEEDS-ACTION", "PARTSTAT", "NEEDS-ACTION"), ("partstat", "ACCEPTED", "PARTSTAT", "ACCEPTED"), ("partstat", "DECLINED", "PARTSTAT", "DECLINED"), ("partstat", "TENTATIVE", "PARTSTAT", "TENTATIVE"), ("partstat", "DELEGATED", "PARTSTAT", "DELEGATED"), ("language", "en-US", "LANGUAGE", "en-US"), ("language", "de-DE", "LANGUAGE", "de-DE"), ("directory", "ldap://example.com", "DIR", "ldap://example.com"), ], ) def test_new_with_string_parameters( self, argument, value, parameter, expected_value ): """Test vCalAddress.new() with various string parameters.""" kwargs = {argument: value} addr = vCalAddress.new("test@test.com", **kwargs) assert str(addr) == "mailto:test@test.com" assert addr.params[parameter] == expected_value @pytest.mark.parametrize( ("rsvp_value", "expected_param"), [ (True, "TRUE"), (False, "FALSE"), ], ) def test_new_with_rsvp_parameter(self, rsvp_value, expected_param): """Test vCalAddress.new() with RSVP parameter.""" addr = vCalAddress.new("test@test.com", rsvp=rsvp_value) assert str(addr) == "mailto:test@test.com" assert addr.params["RSVP"] == expected_param @pytest.mark.parametrize( ("delegate_param", "delegate_value", "expected_param", "expected_value"), [ ( "delegated_from", "sender@test.com", "DELEGATED-FROM", "mailto:sender@test.com", ), ( "delegated_from", "mailto:sender@test.com", "DELEGATED-FROM", "mailto:sender@test.com", ), ( "delegated_to", "delegate@test.com", "DELEGATED-TO", "mailto:delegate@test.com", ), ( "delegated_to", "MAILTO:delegate@test.com", "DELEGATED-TO", "MAILTO:delegate@test.com", ), ("sent_by", "secretary@test.com", "SENT-BY", "mailto:secretary@test.com"), ( "sent_by", "mailto:secretary@test.com", "SENT-BY", "mailto:secretary@test.com", ), ], ) def test_new_with_email_parameters( self, delegate_param, delegate_value, expected_param, expected_value ): """Test vCalAddress.new() with email delegation parameters.""" kwargs = {delegate_param: delegate_value} addr = vCalAddress.new("test@test.com", **kwargs) assert str(addr) == "mailto:test@test.com" assert addr.params[expected_param] == expected_value def test_new_with_all_parameters(self): """Test creating vCalAddress with all parameters.""" addr = vCalAddress.new( "test@test.com", cn="Test User", cutype="INDIVIDUAL", delegated_from="sender@test.com", delegated_to="delegate@test.com", directory="ldap://example.com", language="en-US", partstat="ACCEPTED", role="REQ-PARTICIPANT", rsvp=True, sent_by="secretary@test.com", ) assert str(addr) == "mailto:test@test.com" assert addr.params["CN"] == "Test User" assert addr.params["CUTYPE"] == "INDIVIDUAL" assert addr.params["DELEGATED-FROM"] == "mailto:sender@test.com" assert addr.params["DELEGATED-TO"] == "mailto:delegate@test.com" assert addr.params["DIR"] == "ldap://example.com" assert addr.params["LANGUAGE"] == "en-US" assert addr.params["PARTSTAT"] == "ACCEPTED" assert addr.params["ROLE"] == "REQ-PARTICIPANT" assert addr.params["RSVP"] == "TRUE" assert addr.params["SENT-BY"] == "mailto:secretary@test.com" def test_new_with_none_parameters_ignored(self): """Test that None parameters are ignored.""" addr = vCalAddress.new( "test@test.com", cn=None, role=None, rsvp=None, ) assert str(addr) == "mailto:test@test.com" assert "CN" not in addr.params assert "ROLE" not in addr.params assert "RSVP" not in addr.params def test_new_integration_with_event(self): """Test that vCalAddress.new() works with Event attendees.""" addr = vCalAddress.new("test@test.com", cn="Test User", rsvp=True) event = Event.new(attendees=[addr]) assert event.attendees == [addr] attendee = event.attendees[0] assert str(attendee) == "mailto:test@test.com" assert attendee.params["CN"] == "Test User" assert attendee.params["RSVP"] == "TRUE" class TestVCalAddressNewErrorCases: """Test error cases for vCalAddress.new() method.""" def test_new_requires_email(self): """Test that email parameter is required.""" with pytest.raises(TypeError): vCalAddress.new() # Missing required email argument def test_new_email_must_be_string(self): """Test that email must be a string.""" with pytest.raises(TypeError, match="Email must be a string, not int"): vCalAddress.new(123) with pytest.raises(TypeError, match="Email must be a string, not list"): vCalAddress.new(["test@test.com"]) class TestVCalAddressNewExamples: """Test examples that demonstrate vCalAddress.new() usage.""" def test_basic_usage_example(self): """Test basic usage example from docstring.""" addr = vCalAddress.new("test@test.com") assert str(addr) == "mailto:test@test.com" def test_with_parameters_example(self): """Test example with parameters from docstring.""" addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR") assert addr.params["CN"] == "Test User" assert addr.params["ROLE"] == "CHAIR" def test_consistent_with_event_new(self): """Test that vCalAddress.new() integrates well with Event.new().""" # Both should create objects with .new() class method event = Event.new(summary="Test", start=datetime(2026, 1, 1, 12, 0)) addr = vCalAddress.new("test@test.com", cn="Test User") # Should be able to add the address to event attendees event.attendees = [addr] assert len(event.attendees) == 1 assert event.attendees[0].params["CN"] == "Test User" collective-icalendar-138c845/src/icalendar/tests/test_issue_884_get_duration.py000066400000000000000000000012021524134033600276230ustar00rootroot00000000000000"""If the duration property exists, and it's not a ``default`` object but a ``timedelta`` instance, then it should return a ``duration``, not ``None``. See https://github.com/collective/icalendar/issues/884 """ from datetime import timedelta import pytest from icalendar import Event, InvalidCalendar def test_duration_is_a_duration(): """The duration is a duration.""" event = Event() event["duration"] = timedelta(1) with pytest.raises(InvalidCalendar): event.DURATION def test_duration_is_properly_set(): event = Event() event.add("duration", timedelta(1)) assert event.DURATION == timedelta(1) collective-icalendar-138c845/src/icalendar/tests/test_issue_898_todo_end.py000066400000000000000000000034721524134033600267520ustar00rootroot00000000000000"""Test for issue #898 - TODO.end should equal TODO.start when no DURATION/DUE. For Events with only DTSTART: - date: end = start + 1 day - datetime: end = start For Todos with only DTSTART: - date: end = start (NOT start + 1 day) - datetime: end = start See https://github.com/collective/icalendar/issues/898 """ from datetime import date, datetime from icalendar import Event, Todo class TestIssue898TodoEnd: """Test TODO.end behavior when only DTSTART is provided.""" def test_event_with_date_dtstart_only(self): """Event with date DTSTART should have end = start + 1 day.""" event = Event() event.add("UID", "test-event-date") event.add("DTSTART", date(2026, 1, 1)) assert event.end == date(2026, 1, 2) def test_event_with_datetime_dtstart_only(self): """Event with datetime DTSTART should have end = start.""" event = Event() event.add("UID", "test-event-datetime") event.add("DTSTART", datetime(2026, 1, 1, 12, 0)) assert event.end == datetime(2026, 1, 1, 12, 0) def test_todo_with_date_dtstart_only(self): """Todo with date DTSTART should have end = start (NOT start + 1 day). The RFC doesn't specify this behavior explicitly. Returning None is another option, but returning start works best. """ todo = Todo() todo.add("UID", "test-todo-date") todo.add("DTSTART", date(2026, 1, 1)) # This should be start, not start + 1 day assert todo.end == date(2026, 1, 1) def test_todo_with_datetime_dtstart_only(self): """Todo with datetime DTSTART should have end = start.""" todo = Todo() todo.add("UID", "test-todo-datetime") todo.add("DTSTART", datetime(2026, 1, 1, 12, 0)) assert todo.end == datetime(2026, 1, 1, 12, 0) collective-icalendar-138c845/src/icalendar/tests/test_issue_899_copy_component.py000066400000000000000000000026551524134033600302160ustar00rootroot00000000000000"""Tests for copying components. See https://github.com/collective/icalendar/issues/899 """ import pytest from icalendar import Calendar, Event @pytest.fixture def component_with_subcomponent() -> Calendar: event = Event.new(description="Test") return Calendar.new(subcomponents=[event]) def test_shallow_copy_without_subcomponent(component_with_subcomponent): """We can copy a component and lose the subcomponent""" calendar2 = component_with_subcomponent.copy() assert len(calendar2.subcomponents) == 0 assert calendar2 is not component_with_subcomponent def test_full_copy_with_subcomponent(component_with_subcomponent): """We can copy a component with a child.""" calendar2 = component_with_subcomponent.copy(recursive=True) assert len(calendar2.subcomponents) == 1 assert calendar2 is not component_with_subcomponent event1 = component_with_subcomponent.subcomponents[0] event2 = calendar2.subcomponents[0] assert event1 is not event2 assert event1.uid is not event2.uid def test_full_copy_with_parameters(): """We can copy a component with a child.""" event1 = Event.new(description="Test") event1["description"].LANGUAGE = "en" event2 = event1.copy(recursive=True) assert event2["description"].LANGUAGE == "en" assert event2["description"].params == event1["description"].params assert event2["description"].params is not event1["description"].params collective-icalendar-138c845/src/icalendar/tests/test_issue_950.py000066400000000000000000000014001524134033600250510ustar00rootroot00000000000000"""Test for Issue #950: with_uid method on Component.""" from icalendar import Calendar, Event def test_with_uid_finds_event(): """Test that with_uid finds components with matching UID.""" cal = Calendar() event = Event() event.add("uid", "test-uid-12345") event.add("summary", "Test Event") cal.add_component(event) result = cal.with_uid("test-uid-12345") assert len(result) == 1 assert result[0].get("uid") == "test-uid-12345" def test_with_uid_returns_empty_when_not_found(): """Test that with_uid returns empty list when UID not found.""" cal = Calendar() event = Event() event.add("uid", "some-other-uid") cal.add_component(event) result = cal.with_uid("nonexistent-uid") assert result == [] collective-icalendar-138c845/src/icalendar/tests/test_issue_987.py000066400000000000000000000011161524134033600250670ustar00rootroot00000000000000"""Tests for issue #987: property class modularization. Verifies that mutable property types (vDDDLists, vRecur) are not hashable. """ from datetime import timedelta import pytest from icalendar import vDDDLists, vRecur def test_vdddtypes_cannot_be_hashed(): """Mutable objects are not hashable.""" with pytest.raises(TypeError): hash(vDDDLists([])) with pytest.raises(TypeError): hash(vDDDLists([timedelta(0)])) def test_vrecur_cannot_be_hashed(): """Mutable objects are not hashable.""" with pytest.raises(TypeError): hash(vRecur([])) collective-icalendar-138c845/src/icalendar/tests/test_issue_999_negative_event_duration.py000066400000000000000000000017671524134033600320760ustar00rootroot00000000000000"""Tests for issue #999: [Bug/Corner case] event.duration<0. This verifies that negative durations for events are ignored, while negative durations for todos are allowed. This is because the iCalendar specification requires that events have a non-negative duration, while todos can have a negative duration to indicate that they are overdue. """ from datetime import datetime, timedelta from icalendar import Event, Todo def test_event_negative_duration_ignored(): event = Event() event.start = datetime(2000, 1, 1, 0, 0, 0) event.duration = timedelta(seconds=-4) assert event.end == event.start def test_event_zero_duration_ignored(): event = Event() event.start = datetime(2000, 1, 1, 0, 0, 0) event.duration = timedelta(seconds=0) assert event.end == event.start def test_todo_negative_duration_allowed(): todo = Todo() todo.start = datetime(2000, 1, 1, 0, 0, 0) todo.duration = timedelta(seconds=-4) assert todo.end == datetime(1999, 12, 31, 23, 59, 56) collective-icalendar-138c845/src/icalendar/tests/test_jcal_recursion.py000066400000000000000000000126321524134033600263370ustar00rootroot00000000000000"""Regression test for RecursionError on deeply-nested jCal. The iCal parser/serializer was made iterative to survive deep nesting (see https://github.com/collective/icalendar/issues/1370), but the jCal counterparts :meth:`Component.from_jcal` and :meth:`Component.to_jcal` remained recursive: a small jCal payload nesting components a few hundred levels deep raised an uncaught ``RecursionError`` at the default recursion limit (1000). These tests mirror ``test_walk_handles_deeply_nested_components`` and use a depth comfortably above the recursion limit so the recursive implementation would fail while the iterative one succeeds. """ import sys import pytest from icalendar import Component from icalendar.error import JCalParsingError def _nested_jcal(depth: int) -> list: """Return a jCal VCALENDAR with ``depth`` levels of nested VEVENT.""" root = [ "vcalendar", [["version", {}, "text", "2.0"], ["prodid", {}, "text", "-//test//test//EN"]], [], ] inner = root for _ in range(depth): child = ["vevent", [["uid", {}, "text", "nested@example.com"]], []] inner[2].append(child) inner = child return root def _nesting_depth(component: Component) -> int: """Count how many levels of single-child nesting ``component`` has.""" depth = 0 while component.subcomponents: depth += 1 component = component.subcomponents[0] return depth def test_from_jcal_handles_deeply_nested_components(): """Deeply nested jCal must parse without exceeding the recursion limit.""" depth = sys.getrecursionlimit() + 50 calendar = Component.from_jcal(_nested_jcal(depth)) assert calendar.name == "VCALENDAR" assert _nesting_depth(calendar) == depth def test_to_jcal_handles_deeply_nested_components(): """Serializing a deeply nested component must not recurse.""" depth = sys.getrecursionlimit() + 50 calendar = Component.from_jcal(_nested_jcal(depth)) jcal = calendar.to_jcal() # Walk the produced jCal structure to confirm the full depth survived. node, levels = jcal, 0 while node[2]: levels += 1 node = node[2][0] assert levels == depth def test_deeply_nested_jcal_round_trips(): """from_jcal -> to_jcal -> from_jcal is stable at large depth.""" depth = sys.getrecursionlimit() + 50 jcal = _nested_jcal(depth) once = Component.from_jcal(jcal).to_jcal() twice = Component.from_jcal(once).to_jcal() # Compare by walking the chain: a plain ``once == twice`` would compare the # nested lists recursively and hit RecursionError at this depth, even though # producing them did not. a, b = once, twice levels = 0 while True: # A jCal component is exactly [name, properties, subcomponents]; assert # the length so the element-by-element comparison cannot skip an entry. assert len(a) == 3 assert len(b) == 3 assert a[0] == b[0] # component name assert a[1] == b[1] # properties (a shallow list) if not a[2]: assert not b[2] break assert len(a[2]) == len(b[2]) == 1 a, b = a[2][0], b[2][0] levels += 1 assert levels == depth def test_branching_jcal_round_trips(): """to_jcal preserves sibling order in branching (non-linear) trees. The round-trip test above only exercises a single child per node. The iterative stack walk appends each child to its parent before pushing it, so sibling order must be independent of the LIFO pop order -- this locks that in with multiple siblings at two levels. """ jcal = [ "vcalendar", [["version", {}, "text", "2.0"], ["prodid", {}, "text", "-//test//EN"]], [ [ "vevent", [["uid", {}, "text", "first@test"]], [ [ "valarm", [ ["action", {}, "text", "DISPLAY"], ["description", {}, "text", "A"], ["trigger", {}, "duration", "-PT15M"], ], [], ], [ "valarm", [ ["action", {}, "text", "AUDIO"], ["trigger", {}, "duration", "-PT5M"], ], [], ], ], ], ["vevent", [["uid", {}, "text", "second@test"]], []], ], ] result = Component.from_jcal(jcal).to_jcal() # Sibling VEVENTs keep their order. assert result[2][0][1][0][3] == "first@test" assert result[2][1][1][0][3] == "second@test" # The first VEVENT keeps both of its VALARM subcomponents, in order. assert len(result[2][0][2]) == 2 assert result[2][0][2][0][1][0][3] == "DISPLAY" assert result[2][0][2][1][1][0][3] == "AUDIO" def test_error_path_is_preserved_for_nested_jcal(): """Errors deep in the tree still report the accumulated jCal path.""" # A malformed (non-list) subcomponent nested two levels deep. bad = [ "vcalendar", [], [["vevent", [], [["valarm", [], [42]]]]], ] with pytest.raises( JCalParsingError, match=r"\[2\]\[0\]\[2\]\[0\]\[2\]\[0\] in Alarm: " r"A component must be a list with 3 items\.", ): Component.from_jcal(bad) collective-icalendar-138c845/src/icalendar/tests/test_multiple.py000066400000000000000000000016671524134033600251760ustar00rootroot00000000000000"""Testing multiple VCALENDAR components and multiple VEVENT components""" import pytest from icalendar import Event, vText def test_multiple(calendars): """Check opening multiple calendars.""" cals = calendars.multiple.multiple_calendar_components assert len(cals) == 2 assert [comp.name for comp in cals[0].walk()] == ["VCALENDAR", "VEVENT"] assert [comp.name for comp in cals[1].walk()] == ["VCALENDAR", "VEVENT", "VEVENT"] assert cals[0]["prodid"] == vText( "-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN" ) def test_multiple_events(): """Raises ValueError unless multiple=True""" event_components = """ BEGIN:VEVENT END:VEVENT BEGIN:VEVENT END:VEVENT """ with pytest.raises(ValueError): Event.from_ical(event_components, multiple=False) def test_missing_event(): """Raises ValueError if no component found""" with pytest.raises(ValueError): Event.from_ical("") collective-icalendar-138c845/src/icalendar/tests/test_oss_fuzz_errors.py000066400000000000000000000011241524134033600266050ustar00rootroot00000000000000"""This file collects errors that the OSS FUZZ build has found.""" from datetime import time import pytest from icalendar.cal.calendar import Calendar from icalendar.prop import vDDDLists def test_stack_is_empty(): """If we get passed an invalid string, we expect to get a ValueError.""" with pytest.raises(ValueError): Calendar.from_ical("END:CALENDAR") def test_vdd_list_type_mismatch(): """If we pass in a string type, we expect it to be converted to bytes""" vddd_list = vDDDLists([time(hour=6, minute=6, second=6)]) assert vddd_list.to_ical() == b"060606" collective-icalendar-138c845/src/icalendar/tests/test_parameter_access.py000066400000000000000000000020131524134033600266260ustar00rootroot00000000000000"""Test the access to parameters via getter/setter properties.""" import pytest from icalendar import Parameters from icalendar.enums import VALUE @pytest.fixture def p(): """Empty test property.""" return Parameters() def test_value_parameter_default(p): """Test default value.""" assert p.value is None def test_set_value(p): """Set the value, convert""" p.value = "test" assert p.value == "TEST" assert p["VALUE"] == "TEST" def test_conversion(p): """Converted to enum.""" p.value = "DATE-TIME" assert p.value == VALUE.DATE_TIME def test_delete_value(p): """Delete the value.""" p.value = "test" del p.value assert p.value is None assert "VALUE" not in p del p.value assert p.value is None assert "VALUE" not in p def test_delete_value_None(p): """Delete the value.""" p.value = "test" p.value = None assert p.value is None assert "VALUE" not in p p.value = None assert p.value is None assert "VALUE" not in p collective-icalendar-138c845/src/icalendar/tests/test_parser_quadratic_unfolding.py000066400000000000000000000031501524134033600307260ustar00rootroot00000000000000"""Regression tests for the quadratic unfolding/whitespace stripping in the content line parser. Both ``Contentline`` unfolding and ``_strip_ows_around_delimiters`` used regexes whose greedy run was re-tried at every position of a long run, so a small but adversarial calendar made parsing take seconds. The patched regexes match the same strings in linear time. """ import time import pytest from icalendar import Calendar from icalendar.parser.content_line import ( Contentline, _strip_ows_around_delimiters, ) def test_unfolding_preserves_behaviour(): assert str(Contentline.from_ical("DESCRIPTION:Hello\r\n World")) == ( "DESCRIPTION:HelloWorld" ) # a run of blank lines before the continuation collapses, as before assert str(Contentline.from_ical("A:b\r\n\r\n c")) == "A:bc" def test_strip_ows_around_adjacent_delimiters(): assert _strip_ows_around_delimiters("X ; ; Y") == "X;;Y" assert _strip_ows_around_delimiters("VALUE = TEXT") == "VALUE=TEXT" @pytest.mark.parametrize("payload", ["\n" * 200_000, " " * 200_000]) def test_pathological_input_parses_quickly(payload): if payload.startswith("\n"): ics = "BEGIN:VCALENDAR\r\n" + payload + "END:VCALENDAR\r\n" else: ics = ( "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nX;" + payload + ":v\r\nEND:VCALENDAR\r\n" ) start = time.perf_counter() Calendar.from_ical(ics) # the unpatched regexes need minutes for this input; the bound is generous # enough to never flake on slow CI while still catching a return of the # quadratic behaviour. assert time.perf_counter() - start < 5 collective-icalendar-138c845/src/icalendar/tests/test_parsing.py000066400000000000000000000305541524134033600250030ustar00rootroot00000000000000"""Tests checking that parsing works""" import base64 from datetime import datetime import pytest from icalendar import vBinary, vRecur from icalendar.cal.calendar import Calendar from icalendar.cal.component_factory import ComponentFactory from icalendar.cal.event import Event from icalendar.parser import ( Contentline, Parameters, _escape_char, _foldline, _unescape_char, ) @pytest.mark.parametrize( ("raw_content_line", "expected_output"), [ # Issue #142 - Multivalued parameters. This is needed for VCard 3.0. # see https://github.com/collective/icalendar/pull/142 ( "TEL;TYPE=HOME,VOICE:000000000", ("TEL", Parameters({"TYPE": ["HOME", "VOICE"]}), "000000000"), ), # Issue #143 - Allow dots in property names. Another vCard related issue. # see https://github.com/collective/icalendar/pull/143 ( "ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR:;;This is the Adress 08; Some City;;12345;Germany", ( "ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.ADR", Parameters(), ";;This is the Adress 08; Some City;;12345;Germany", ), ), ( "ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL:", ( "ITEMADRNULLTHISISTHEADRESS08158SOMECITY12345.X-ABLABEL", Parameters(), "", ), ), ], ) def test_content_lines_parsed_properly(raw_content_line, expected_output): assert Contentline.from_ical(raw_content_line).parts() == expected_output @pytest.mark.parametrize( ("raw_content_line", "expected_altrep"), [ # Literal percent escapes in a parameter value (common in URIs) must # survive parsing. They collided with the parser's internal # ``\\,`` -> ``%2C`` transport encoding and were silently decoded. ('X-P;ALTREP="http://x/a%2Cb%3Ac%3Bd%5Ce":v', "http://x/a%2Cb%3Ac%3Bd%5Ce"), ('X-P;ALTREP="http://x/%2520":v', "http://x/%2520"), # Same value unquoted, with a backslash-escaped colon protecting the # URI scheme separator. Percent escapes must survive here too. ("X-P;ALTREP=http\\://x/a%2Cb%3Ac%3Bd%5Ce:v", "http://x/a%2Cb%3Ac%3Bd%5Ce"), # A backslash-escaped delimiter still decodes to the bare character. ("X-P;ALTREP=a\\,b:v", "a,b"), ], ) def test_percent_escapes_in_parameter_values_are_preserved( raw_content_line, expected_altrep ): assert Contentline(raw_content_line).parts()[1]["ALTREP"] == expected_altrep @pytest.mark.parametrize( "timezone_info", [ # General timezone aware dates in ical string (b"DTSTART;TZID=America/New_York:20130907T120000"), (b"DTEND;TZID=America/New_York:20130907T170000"), # Specific timezone aware exdates in ical string (b"EXDATE;TZID=America/New_York:20131012T120000"), (b"EXDATE;TZID=America/New_York:20131011T120000"), ], ) def test_timezone_info_present_in_ical_issue_112(events, timezone_info): """Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 """ assert timezone_info in events.issue_112_missing_tzinfo_on_exdate.to_ical() def test_timezone_name_parsed_issue_112(events): """Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 """ assert ( events.issue_112_missing_tzinfo_on_exdate["exdate"][0].dts[0].dt.tzname() == "EDT" ) def test_issue_157_removes_trailing_semicolon(events): """Issue #157 - Recurring rules and trailing semicolons https://github.com/collective/icalendar/pull/157 """ recur = events.issue_157_removes_trailing_semicolon.decoded("RRULE") assert isinstance(recur, vRecur) assert recur.to_ical() == b"FREQ=YEARLY;BYDAY=1SU;BYMONTH=11" @pytest.mark.parametrize( "event_name", [ # https://github.com/collective/icalendar/pull/100 ("issue_100_transformed_doctests_into_unittests"), ("issue_184_broken_representation_of_period"), # PERIOD should be put back into shape "issue_156_RDATE_with_PERIOD", "issue_156_RDATE_with_PERIOD_list", "event_with_unicode_organizer", ], ) def test_event_to_ical_is_inverse_of_from_ical(events, event_name): """Make sure that an event's ICS is equal to the ICS it was made from.""" event = events[event_name] assert event.to_ical().splitlines() == event.raw_ics.splitlines() assert event.to_ical() == event.raw_ics def test_decode_rrule_attribute_error_issue_70(events): # Issue #70 - e.decode("RRULE") causes Attribute Error # see https://github.com/collective/icalendar/issues/70 recur = events.issue_70_rrule_causes_attribute_error.decoded("RRULE") assert isinstance(recur, vRecur) assert recur.to_ical() == b"FREQ=WEEKLY;UNTIL=20070619T225959;INTERVAL=1" def test_description_parsed_properly_issue_53(events): """Issue #53 - Parsing failure on some descriptions? https://github.com/collective/icalendar/issues/53 """ assert ( b"July 12 at 6:30 PM" in events.issue_53_description_parsed_properly["DESCRIPTION"].to_ical() ) @pytest.mark.parametrize( "escape_sequence", [ r"\n", r"\N", r"\,", r"\;", r"\\", "^n", "^^", "^'", ], ) @pytest.mark.parametrize("line_limit", range(60, 91)) def test_foldline_does_not_split_escape_sequences_issue_1501( escape_sequence, line_limit ): """Issue #1501 - line folding must not split escape sequences.""" prefix = "DESCRIPTION:" + ("a" * (line_limit - len("DESCRIPTION:") - 2)) line = prefix + escape_sequence + "after fold" folded = _foldline(line, limit=line_limit) assert "\r\n " in folded assert (escape_sequence[0] + "\r\n " + escape_sequence[1]) not in folded @pytest.mark.parametrize("escape_sequence", [r"\n", "^n"]) def test_foldline_does_not_split_escape_sequences_across_multiple_lines_issue_1501( escape_sequence, ): """Issue #1501 - escape-aware folding applies after the first fold too.""" line_limit = 60 prefix = "DESCRIPTION:" + ("a" * (line_limit - len("DESCRIPTION:") - 2)) repeated_segment = escape_sequence + ("b" * (line_limit - len(escape_sequence) - 1)) line = prefix + repeated_segment + escape_sequence + "after fold" folded = _foldline(line, limit=line_limit) assert folded.count("\r\n ") >= 2 assert (escape_sequence[0] + "\r\n " + escape_sequence[1]) not in folded def test_raises_value_error_for_properties_without_parent_pull_179(): """Found an issue where from_ical() would raise IndexError for properties without parent components. https://github.com/collective/icalendar/pull/179 """ with pytest.raises(ValueError): Calendar.from_ical("VERSION:2.0") def test_tzid_parsed_properly_issue_53(timezones): """Issue #53 - Parsing failure on some descriptions? https://github.com/collective/icalendar/issues/53 """ assert ( timezones.issue_53_tzid_parsed_properly["tzid"].to_ical() == b"America/New_York" ) def test_timezones_to_ical_is_inverse_of_from_ical(timezones): """Issue #55 - Parse error on utc-offset with seconds value see https://github.com/collective/icalendar/issues/55""" timezone = timezones["issue_55_parse_error_on_utc_offset_with_seconds"] assert timezone.to_ical() == timezone.raw_ics @pytest.mark.parametrize( ("date", "expected_output"), [ (datetime(2012, 7, 16, 0, 0, 0), b"DTSTART:20120716T000000Z"), (datetime(2021, 11, 17, 15, 9, 15), b"DTSTART:20211117T150915Z"), ], ) def test_no_tzid_when_utc(utc, date, expected_output): """Issue #58 - TZID on UTC DATE-TIMEs Issue #335 - UTC timezone identification is broken https://github.com/collective/icalendar/issues/58 https://github.com/collective/icalendar/issues/335 """ # According to RFC 5545: "The TZID property parameter MUST NOT be # applied to DATE-TIME or TIME properties whose time values are # specified in UTC. date = date.replace(tzinfo=utc) event = Event() event.add("dtstart", date) assert expected_output in event.to_ical() def test_vBinary_base64_encoded_issue_82(): """Issue #82 - vBinary __repr__ called rather than to_ical from container types https://github.com/collective/icalendar/issues/82 """ b = vBinary("text") b.params["FMTTYPE"] = "text/plain" assert b.to_ical() == base64.b64encode(b"text") def test_creates_event_with_base64_encoded_attachment_issue_82(events): """Issue #82 - vBinary __repr__ called rather than to_ical from container types https://github.com/collective/icalendar/issues/82 """ b = vBinary("text") b.params["FMTTYPE"] = "text/plain" event = Event() event.add("ATTACH", b) assert event.to_ical() == events.issue_82_expected_output.raw_ics @pytest.mark.parametrize( "calendar_name", [ # Issue #466 - [BUG] TZID timezone is ignored when forward-slash is used # https://github.com/collective/icalendar/issues/466 "issue_466_respect_unique_timezone", "issue_466_convert_tzid_with_slash", ], ) def test_handles_unique_tzid(calendars, in_timezone, calendar_name): calendar = calendars[calendar_name] event = calendar.walk("VEVENT")[0] print(vars(event)) start_dt = event["dtstart"].dt end_dt = event["dtend"].dt assert start_dt == in_timezone(datetime(2022, 10, 21, 20, 0, 0), "Europe/Stockholm") assert end_dt == in_timezone(datetime(2022, 10, 21, 21, 0, 0), "Europe/Stockholm") @pytest.mark.parametrize( ("event_name", "expected_cn", "expected_ics"), [ ( "event_with_escaped_characters", r"that, that; %th%%at%\ that:", "это, то; that\\ %th%%at%:", ), ("event_with_escaped_character1", r"Society, 2014", "that"), ("event_with_escaped_character2", r"Society\ 2014", "that"), ("event_with_escaped_character3", r"Society; 2014", "that"), ("event_with_escaped_character4", r"Society: 2014", "that"), ], ) def test_escaped_characters_read(event_name, expected_cn, expected_ics, events): event = events[event_name] assert event["ORGANIZER"].params["CN"] == expected_cn assert event["ORGANIZER"].to_ical() == expected_ics.encode("utf-8") def test_unescape_char(): assert _unescape_char(b"123") == b"123" assert _unescape_char(b"\\n") == b"\n" @pytest.mark.parametrize( ("value", "expected"), [ ("a\rb", r"a\nb"), ("a\r\nb", r"a\nb"), ("a\nb", r"a\nb"), ("a\rb\r\nc\nd", r"a\nb\nc\nd"), ("\ra\n", r"\na\n"), ], ) def test_escape_char_transforms_lone_carriage_return(value, expected): r"""A lone ``\r`` must be transformed, not left raw in the content line.""" assert _escape_char(value) == expected def test_lone_carriage_return_cannot_inject_content_line(): event = Event() event.add("SUMMARY", "safe\rINJECTED:evil") assert b"\rINJECTED" not in event.to_ical() assert event.to_ical() == ( b"BEGIN:VEVENT\r\nSUMMARY:safe\\nINJECTED:evil\r\nEND:VEVENT\r\n" ) def test_split_on_unescaped_comma(): """Test splitting on unescaped commas.""" from icalendar.parser import split_on_unescaped_comma # Simple case assert split_on_unescaped_comma("a,b,c") == ["a", "b", "c"] # Escaped comma assert split_on_unescaped_comma("a\\,b,c") == ["a,b", "c"] # Multiple escaped commas assert split_on_unescaped_comma("a\\,b\\,c") == ["a,b,c"] # Mixed assert split_on_unescaped_comma("Work,Personal\\, Urgent") == [ "Work", "Personal, Urgent", ] # Empty string assert split_on_unescaped_comma("") == [""] # Only commas assert split_on_unescaped_comma(",,,") == ["", "", "", ""] # Trailing comma assert split_on_unescaped_comma("a,b,") == ["a", "b", ""] # Leading comma assert split_on_unescaped_comma(",a,b") == ["", "a", "b"] # Other escaped chars assert split_on_unescaped_comma("a\\;b,c\\nd") == ["a;b", "c\nd"] def test_create_a_component(): """Create a component with the factory.""" factory = ComponentFactory() my_component_class = factory.get_component_class("My-Component") assert my_component_class.name == "MY-COMPONENT" assert my_component_class.__name__ == "MyComponent" collective-icalendar-138c845/src/icalendar/tests/test_period.py000066400000000000000000000101261524134033600246130ustar00rootroot00000000000000"""These tests make sure that we have some coverage on the usage of the PERIOD value type. See - https://github.com/collective/icalendar/issues/156 - https://github.com/pimutils/khal/issues/152#issuecomment-933635248 """ import datetime import pytest from icalendar.prop import vDDDTypes, vPeriod @pytest.mark.parametrize( ("calname", "tzname", "index", "period_string"), [ ( "issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 0, "20211101T160000/20211101T163000", ), ( "issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 1, "20211206T160000/20211206T163000", ), ( "issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 2, "20220103T160000/20220103T163000", ), ( "issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 3, "20220207T160000/20220207T163000", ), ] + [ ("issue_156_RDATE_with_PERIOD_TZID_khal", "America/Chicago", i, period) for i, period in enumerate( [ "20180327T080000/20180327T090000", "20180403T080000/20180403T090000", "20180410T080000/20180410T090000", "20180417T080000/20180417T090000", "20180424T080000/20180424T090000", "20180501T080000/20180501T090000", "20180508T080000/20180508T090000", "20180515T080000/20180515T090000", "20180522T080000/20180522T090000", "20180529T080000/20180529T090000", "20180605T080000/20180605T090000", "20180612T080000/20180612T090000", "20180619T080000/20180619T090000", "20180626T080000/20180626T090000", "20180703T080000/20180703T090000", "20180710T080000/20180710T090000", "20180717T080000/20180717T090000", "20180724T080000/20180724T090000", "20180731T080000/20180731T090000", ] ) ], ) def test_issue_156_period_list_in_rdate( calendars, calname, tzname, index, period_string ): """Check items in a list of period values.""" calendar = calendars[calname] rdate = calendar.walk("vevent")[0]["rdate"] period = rdate.dts[index] assert period.dt == vDDDTypes.from_ical(period_string, timezone=tzname) def test_duration_properly_parsed(events): """This checks the duration PT5H30M.""" start = vDDDTypes.from_ical("19970109T180000Z") duration = vDDDTypes.from_ical("PT5H30M") rdate = events.issue_156_RDATE_with_PERIOD_list["RDATE"] print(rdate) period = rdate.dts[1].dt print(dir(duration)) assert period[0] == start assert period[1].days == 0 assert period[1].seconds == (5 * 60 + 30) * 60 assert period[1] == duration def test_tzid_is_part_of_the_parameters(calendars): """The TZID should be mentioned in the parameters.""" event = list(calendars.period_with_timezone.walk("VEVENT"))[0] assert event["RDATE"].params["TZID"] == "America/Vancouver" def test_tzid_is_part_of_the_period_values(calendars, tzp): """The TZID should be set in the datetime.""" event = list(calendars.period_with_timezone.walk("VEVENT"))[0] start, end = event["RDATE"].dts[0].dt assert start == tzp.localize( datetime.datetime(2023, 12, 13, 12), "America/Vancouver" ) assert end == tzp.localize(datetime.datetime(2023, 12, 13, 15), "America/Vancouver") def test_period_overlaps(): # 30 minute increments datetime_1 = datetime.datetime(2024, 11, 20, 12, 0) # 12:00 datetime_2 = datetime.datetime(2024, 11, 20, 12, 30) # 12:30 datetime_3 = datetime.datetime(2024, 11, 20, 13, 0) # 13:00 period_1 = vPeriod((datetime_1, datetime_2)) period_2 = vPeriod((datetime_1, datetime_3)) period_3 = vPeriod((datetime_2, datetime_3)) assert period_1.overlaps(period_2) assert period_3.overlaps(period_2) assert not period_1.overlaps(period_3) collective-icalendar-138c845/src/icalendar/tests/test_prop_org_coverage.py000066400000000000000000000115531524134033600270400ustar00rootroot00000000000000"""Coverage tests for uncovered branches in :mod:`icalendar.prop.org`. These tests target the branches identified in the coverage survey: - ``vOrg.__eq__`` — the ``isinstance`` False branch (comparing with non-vOrg). - ``vOrg.__repr__`` — string representation. - ``vOrg.from_jcal`` — the happy path (valid jCal with string fields) and the ``validate_value_type`` loop (non-string field value). """ from __future__ import annotations import pytest from icalendar.error import JCalParsingError from icalendar.prop import vOrg class TestVOrgEquality: """Test ``vOrg.__eq__``.""" def test_eq_same_fields(self): """Two vOrg instances with the same fields are equal.""" org1 = vOrg(("ABC Inc.", "Marketing", "Sales")) org2 = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org1 == org2 def test_eq_different_fields(self): """Two vOrg instances with different fields are not equal.""" org1 = vOrg(("ABC Inc.", "Marketing")) org2 = vOrg(("ABC Inc.", "Sales")) assert org1 != org2 def test_eq_different_number_of_fields(self): """Two vOrg instances with a different number of fields are not equal.""" org1 = vOrg(("ABC Inc.", "Marketing")) org2 = vOrg(("ABC Inc.", "Marketing", "Sales")) assert org1 != org2 def test_eq_non_vorg_object(self): """A vOrg is not equal to a non-vOrg object (isinstance False branch).""" org = vOrg(("ABC Inc.", "Marketing")) assert org != "ABC Inc.;Marketing" assert org != ("ABC Inc.", "Marketing") assert org != 42 assert org != None # noqa: E711 class TestVOrgRepr: """Test ``vOrg.__repr__``.""" def test_repr_contains_class_name_and_fields(self): """repr includes the class name and the fields tuple.""" org = vOrg(("ABC Inc.", "Marketing", "Sales")) result = repr(org) assert result.startswith("vOrg(") assert "('ABC Inc.', 'Marketing', 'Sales')" in result def test_repr_contains_params(self): """repr includes the params.""" org = vOrg(("ABC Inc.", "Marketing"), params={"LANGUAGE": "en"}) result = repr(org) assert "params=" in result assert "LANGUAGE" in result def test_repr_round_trip_not_equal_to_string(self): """repr is a string, not the same as to_ical.""" org = vOrg(("ABC Inc.", "Marketing")) result = repr(org) assert isinstance(result, str) assert "ABC Inc." in result class TestVOrgFromJcal: """Test ``vOrg.from_jcal``.""" def test_from_jcal_valid(self): """Parse a valid jCal ORG property into a vOrg.""" jcal = ["org", {}, "text", "ABC Inc.", "Marketing", "Sales"] org = vOrg.from_jcal(jcal) assert org.fields == ("ABC Inc.", "Marketing", "Sales") assert org.params == {} def test_from_jcal_single_field(self): """Parse a jCal ORG property with only the organization name.""" jcal = ["org", {}, "text", "ABC Inc."] org = vOrg.from_jcal(jcal) assert org.fields == ("ABC Inc.",) assert org.name == "ABC Inc." assert org.units == () def test_from_jcal_with_params(self): """Parse a jCal ORG property that has parameters.""" jcal = ["org", {"language": "en"}, "text", "ABC Inc.", "Marketing"] org = vOrg.from_jcal(jcal) assert org.fields == ("ABC Inc.", "Marketing") assert org.params["language"] == "en" def test_from_jcal_round_trip(self): """to_jcal and from_jcal round-trip preserves the vOrg.""" original = vOrg(("ABC Inc.", "North American Division", "Marketing")) jcal = original.to_jcal("org") parsed = vOrg.from_jcal(jcal) assert parsed == original assert parsed.fields == original.fields def test_from_jcal_non_string_field(self): """A non-string field value raises JCalParsingError.""" jcal = ["org", {}, "text", 123, "Marketing"] with pytest.raises( JCalParsingError, match=r"\[3\] in vOrg: The value must be a string\.", ): vOrg.from_jcal(jcal) def test_from_jcal_non_string_field_at_index_4(self): """A non-string field value at a later index also raises.""" jcal = ["org", {}, "text", "ABC Inc.", 456] with pytest.raises( JCalParsingError, match=r"\[4\] in vOrg: The value must be a string\.", ): vOrg.from_jcal(jcal) def test_from_jcal_too_short_raises(self): """A jCal property with fewer than 4 elements raises JCalParsingError. This is caught by ``JCalParsingError.validate_property``. """ jcal = ["org", {}, "text"] with pytest.raises( JCalParsingError, match=r"in vOrg: The property must be a list with at least 4 items\.", ): vOrg.from_jcal(jcal) collective-icalendar-138c845/src/icalendar/tests/test_property_params.py000066400000000000000000000115631524134033600265660ustar00rootroot00000000000000import re import pytest from icalendar import Parameters, vCalAddress from icalendar.cal.calendar import Calendar from icalendar.cal.event import Event @pytest.mark.parametrize( ("parameter", "expected"), [ # Simple parameter:value pair (Parameters(parameter1="Value1"), b"PARAMETER1=Value1"), # Parameter with list of values must be separated by comma (Parameters({"parameter1": ["Value1", "Value2"]}), b"PARAMETER1=Value1,Value2"), # Multiple parameters must be separated by a semicolon ( Parameters({"RSVP": "TRUE", "ROLE": "REQ-PARTICIPANT"}), b"ROLE=REQ-PARTICIPANT;RSVP=TRUE", ), # Parameter values containing ',;:' must be double quoted (Parameters({"ALTREP": "http://www.wiz.org"}), b'ALTREP="http://www.wiz.org"'), # list items must be quoted separately ( Parameters( {"MEMBER": ["MAILTO:projectA@host.com", "MAILTO:projectB@host.com"]} ), b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"', ), ( Parameters( { "parameter1": "Value1", "parameter2": ["Value2", "Value3"], "ALTREP": ["http://www.wiz.org", "value4"], } ), b'ALTREP="http://www.wiz.org","value4";PARAMETER1=Value1;PARAMETER2=Value2,Value3', ), # Including empty strings (Parameters({"PARAM": ""}), b"PARAM="), # We can also parse parameter strings ( Parameters( { "PARAMETER1": "Value1", "ALTREP": ["http://www.wiz.org", "value4"], "PARAMETER2": ["Value2", "Value3"], } ), b'ALTREP="http://www.wiz.org","value4";PARAMETER1=Value1;PARAMETER2=Value2,Value3', ), ], ) def test_parameter_to_ical_is_inverse_of_from_ical(parameter, expected): assert parameter.to_ical() == expected assert Parameters.from_ical(expected.decode("utf-8")) == parameter def test_parse_parameter_string_without_quotes(): assert Parameters.from_ical("PARAM1=Value 1;PARA2=Value 2") == Parameters( {"PARAM1": "Value 1", "PARA2": "Value 2"} ) def test_parametr_is_case_insensitive(): parameter = Parameters(parameter1="Value1") assert parameter["parameter1"] == parameter["PARAMETER1"] == parameter["PaRaMeTer1"] def test_parameter_keys_are_uppercase(): parameter = Parameters(parameter1="Value1") assert list(parameter.keys()) == ["PARAMETER1"] @pytest.mark.parametrize( ("cn_param", "cn_quoted"), [ # not double-quoted ("Aramis", "Aramis"), # if a space is present - enclose in double quotes ("Aramis Alameda", '"Aramis Alameda"'), # a single quote in parameter value - double quote the value ("Aramis d'Alameda", '"Aramis d\'Alameda"'), ("�рами� д'�ламеда", '"�рами� д\'�ламеда"'), # Before, double quote is replaced with single quote # Since RFC 6868, we replace this with ^' ('Aramis d"Alameda', '"Aramis d^\'Alameda"'), ], ) def test_quoting(cn_param, cn_quoted): event = Event() attendee = vCalAddress("test@example.com") attendee.params["CN"] = cn_param event.add("ATTENDEE", attendee) assert f"ATTENDEE;CN={cn_quoted}:test@example.com" in event.to_ical().decode( "utf-8" ) def test_property_params(): """Property parameters with values containing a COLON character, a SEMICOLON character or a COMMA character MUST be placed in quoted text.""" cal_address = vCalAddress("mailto:john.doe@example.org") cal_address.params["CN"] = "Doe, John" ical = Calendar() ical.add("organizer", cal_address) ical_str = Calendar.to_ical(ical) exp_str = ( b"""BEGIN:VCALENDAR\r\nORGANIZER;CN="Doe, John":""" b"""mailto:john.doe@example.org\r\nEND:VCALENDAR\r\n""" ) assert ical_str == exp_str # other way around: ensure the property parameters can be restored from # an icalendar string. ical2 = Calendar.from_ical(ical_str) assert ical2.get("ORGANIZER").params.get("CN") == "Doe, John" def test_parse_and_access_property_params(calendars): """Parse an ics string and access some property parameters then. This is a follow-up of a question received per email. """ event = calendars.property_params.walk("VEVENT")[0] attendee = event["attendee"][0] assert attendee.to_ical() == b"MAILTO:rembrand@xs4all.nl" assert attendee.params.to_ical() == b"CN=RembrandXS;PARTSTAT=NEEDS-ACTION;RSVP=TRUE" assert attendee.params["cn"] == "RembrandXS" def test_repr(): """Test correct class representation.""" it = Parameters(parameter1="Value1") assert re.match(r"Parameters\({u?'PARAMETER1': u?'Value1'}\)", str(it)) collective-icalendar-138c845/src/icalendar/tests/test_pytz_zoneinfo_integration.py000066400000000000000000000102441524134033600306520ustar00rootroot00000000000000"""This tests the switch to different timezone implementations. These are mostly located in icalendar.timezone. """ try: import pytz from icalendar.timezone.pytz import PYTZ except ImportError: pytz = None import copy import pickle from datetime import datetime, timedelta import pytest from dateutil.rrule import MONTHLY, rrule from dateutil.tz.tz import _tzicalvtz from icalendar.prop import vDDDTypes from icalendar.timezone.zoneinfo import ZONEINFO, zoneinfo if pytz: PYTZ_TIMEZONES = pytz.all_timezones TZP_ = [PYTZ(), ZONEINFO()] NEW_TZP_NAME = ["pytz", "zoneinfo"] else: PYTZ_TIMEZONES = [] TZP_ = [ZONEINFO()] NEW_TZP_NAME = ["zoneinfo"] @pytest.mark.parametrize( "tz_name", PYTZ_TIMEZONES + list(zoneinfo.available_timezones()) ) @pytest.mark.parametrize("tzp_", TZP_) def test_timezone_names_are_known(tz_name, tzp_): """Make sure that all timezones are understood.""" if tz_name in ("Factory", "localtime"): pytest.skip() assert tzp_.knows_timezone_id(tz_name), ( f"{tzp_.__class__.__name__} should know {tz_name}" ) @pytest.mark.parametrize("func", [pickle.dumps, copy.copy, copy.deepcopy]) @pytest.mark.parametrize( "obj", [ _tzicalvtz("id"), rrule(freq=MONTHLY, count=4, dtstart=datetime(2028, 10, 1), cache=True), ], ) def test_can_pickle_timezone(func, tzp, obj): """Check that we can serialize and copy timezones.""" func(obj) def test_copied_rrule_is_the_same(): """When we copy an rrule, we want it to be the same after this.""" r = rrule(freq=MONTHLY, count=4, dtstart=datetime(2028, 10, 1), cache=True) assert str(copy.deepcopy(r)) == str(r) def test_tzp_properly_switches(tzp, tzp_name): """We want the default implementation to switch.""" assert (tzp_name == "pytz") == tzp.uses_pytz() def test_tzp_is_pytz_only(tzp, tzp_name, pytz_only): """We want the default implementation to switch.""" assert tzp_name == "pytz" assert tzp.uses_pytz() def test_cache_reuse_timezone_cache(tzp, timezones): """Make sure we do not cache the timezones twice and change them.""" tzp.cache_timezone_component(timezones.pacific_fiji) tzp1 = tzp.timezone("custom_Pacific/Fiji") assert tzp1 is tzp.timezone("custom_Pacific/Fiji") tzp.cache_timezone_component(timezones.pacific_fiji) assert tzp1 is tzp.timezone("custom_Pacific/Fiji"), "Cache is not replaced." @pytest.mark.parametrize("new_tzp_name", NEW_TZP_NAME) def test_cache_is_emptied_when_tzp_is_switched(tzp, timezones, new_tzp_name): """Make sure we do not reuse the timezones created when we switch the provider.""" tzp.cache_timezone_component(timezones.pacific_fiji) tz1 = tzp.timezone("custom_Pacific/Fiji") tzp.use(new_tzp_name) tzp.cache_timezone_component(timezones.pacific_fiji) tz2 = tzp.timezone("custom_Pacific/Fiji") assert tz1 is not tz2 def test_invalid_name(tzp): """Check that the provider name is OK.""" provider = "invalid_provider" with pytest.raises(ValueError) as e: tzp.use(provider) # f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'." message = e.value.args[0] assert f"Unknown provider {provider}." in message assert "zoneinfo" in message assert "pytz" in message @pytest.mark.parametrize( ("value", "expected_offset"), [ ("19970101", timedelta(hours=-5)), # EST ("19970701", timedelta(hours=-4)), # EDT ], ) def test_a_date_with_a_tzid_uses_the_offset_of_that_date(tzp, value, expected_offset): """A date is localized like a date-time, whichever provider is in use. pytz timezones default to their oldest offset, the local mean time one, until they are localized. """ tzid = "America/New_York" midnight = datetime.strptime(value, "%Y%m%d") # noqa: DTZ007 offset = vDDDTypes.from_ical(value, timezone=tzid).utcoffset() # what the timezone database says about midnight on that date assert offset == midnight.replace(tzinfo=zoneinfo.ZoneInfo(tzid)).utcoffset() # what the same value written as a date-time is read as assert offset == vDDDTypes.from_ical(f"{value}T000000", timezone=tzid).utcoffset() assert offset == expected_offset collective-icalendar-138c845/src/icalendar/tests/test_recurrence.py000066400000000000000000000117441524134033600254750ustar00rootroot00000000000000from datetime import date, datetime import pytest from icalendar.cal.event import Event def test_recurrence_properly_parsed(events): assert events.event_with_recurrence["rrule"] == {"COUNT": [100], "FREQ": ["DAILY"]} @pytest.mark.parametrize( ("i", "exception_date"), [ (0, datetime(1996, 4, 2, 1, 0)), (1, datetime(1996, 4, 3, 1, 0)), (2, datetime(1996, 4, 4, 1, 0)), ], ) def test_exdate_properly_parsed(events, i, exception_date, in_timezone): assert events.event_with_recurrence["exdate"].dts[i].dt == in_timezone( exception_date, "UTC" ) def test_exdate_properly_marshalled(events): actual = events.event_with_recurrence["exdate"].to_ical() assert actual == b"19960402T010000Z,19960403T010000Z,19960404T010000Z" # TODO: DOCUMENT BETTER! # In this case we have multiple EXDATE definitions, one per line. # Icalendar makes a list out of this instead of zipping it into one # vDDDLists object. Actually, this feels correct for me, as it also # allows to define different timezones per exdate line - but client # code has to handle this as list and not blindly expecting to be able # to call event['EXDATE'].to_ical() on it: def test_exdate_formed_from_exdates_on_multiple_lines_is_a_list(events): exdate = events.event_with_recurrence_exdates_on_different_lines["exdate"] assert isinstance(exdate, list) @pytest.mark.parametrize( ("i", "exception_date", "exception_date_ics"), [ (0, datetime(2012, 5, 29, 10, 0), b"20120529T100000"), (1, datetime(2012, 4, 3, 10, 0), b"20120403T100000"), (2, datetime(2012, 4, 10, 10, 0), b"20120410T100000"), (3, datetime(2012, 5, 1, 10, 0), b"20120501T100000"), (4, datetime(2012, 4, 17, 10, 0), b"20120417T100000"), ], ) def test_list_exdate_to_ical_is_inverse_of_from_ical( events, i, exception_date, exception_date_ics, in_timezone ): exdate = events.event_with_recurrence_exdates_on_different_lines["exdate"] assert exdate[i].dts[0].dt == in_timezone(exception_date, "Europe/Vienna") assert exdate[i].to_ical() == exception_date_ics @pytest.mark.parametrize( ("freq", "byday", "dtstart", "expected"), [ # Test some YEARLY BYDAY repeats ( "YEARLY", "1SU", date(2016, 1, 3), # 1st Sunday in year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 1SU\r\nDTSTART;VALUE=DATE:20160103\r\nRRULE:FREQ=YEARLY;BYDAY=1SU\r\nEND:VEVENT\r\n", ), ( "YEARLY", "53MO", date(1984, 12, 31), # 53rd Monday in (leap) year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 53MO\r\nDTSTART;VALUE=DATE:19841231\r\nRRULE:FREQ=YEARLY;BYDAY=53MO\r\nEND:VEVENT\r\n", ), ( "YEARLY", "-1TU", date(1999, 12, 28), # Last Tuesday in year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY -1TU\r\nDTSTART;VALUE=DATE:19991228\r\nRRULE:FREQ=YEARLY;BYDAY=-1TU\r\nEND:VEVENT\r\n", ), ( "YEARLY", "-17WE", date(2000, 9, 6), # 17th-to-last Wednesday in year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY -17WE\r\nDTSTART;VALUE=DATE:20000906\r\nRRULE:FREQ=YEARLY;BYDAY=-17WE\r\nEND:VEVENT\r\n", ), # Test some MONTHLY BYDAY repeats ( "MONTHLY", "2TH", date(2003, 4, 10), # 2nd Thursday in month b"BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY 2TH\r\nDTSTART;VALUE=DATE:20030410\r\nRRULE:FREQ=MONTHLY;BYDAY=2TH\r\nEND:VEVENT\r\n", ), ( "MONTHLY", "-3FR", date(2017, 5, 12), # 3rd-to-last Friday in month b"BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY -3FR\r\nDTSTART;VALUE=DATE:20170512\r\nRRULE:FREQ=MONTHLY;BYDAY=-3FR\r\nEND:VEVENT\r\n", ), ( "MONTHLY", "-5SA", date(2053, 11, 1), # 5th-to-last Saturday in month b"BEGIN:VEVENT\r\nSUMMARY:Event MONTHLY -5SA\r\nDTSTART;VALUE=DATE:20531101\r\nRRULE:FREQ=MONTHLY;BYDAY=-5SA\r\nEND:VEVENT\r\n", ), # Specifically test examples from the report of Issue #518 # https://github.com/collective/icalendar/issues/518 ( "YEARLY", "9MO", date(2023, 2, 27), # 9th Monday in year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 9MO\r\nDTSTART;VALUE=DATE:20230227\r\nRRULE:FREQ=YEARLY;BYDAY=9MO\r\nEND:VEVENT\r\n", ), ( "YEARLY", "10MO", date(2023, 3, 6), # 10th Monday in year b"BEGIN:VEVENT\r\nSUMMARY:Event YEARLY 10MO\r\nDTSTART;VALUE=DATE:20230306\r\nRRULE:FREQ=YEARLY;BYDAY=10MO\r\nEND:VEVENT\r\n", ), ], ) def test_byday_to_ical(freq, byday, dtstart, expected): """Test the BYDAY rule is correctly processed by to_ical().""" event = Event() event.add("SUMMARY", f"Event {freq} {byday}") event.add("DTSTART", dtstart) event.add("RRULE", {"FREQ": [freq], "BYDAY": byday}) assert event.to_ical() == expected collective-icalendar-138c845/src/icalendar/tests/test_repr_recursion.py000066400000000000000000000053451524134033600264010ustar00rootroot00000000000000"""Regression test for repr() RecursionError on deeply-nested calendars. See https://github.com/collective/icalendar/issues/1370. A crafted ``.ics`` payload of only ~13 KB nesting ``BEGIN:VEVENT`` many levels deep used to make ``repr()`` / ``str()`` / ``f"{cal}"`` raise an uncaught ``RecursionError`` at default recursion limit (1000). The parser itself is iterative and accepts the input fine; only the ``Component.__repr__`` traversal was recursive. This test exercises both moderate (default-recursion-limit-breaking) and pathological depths. """ import pytest from icalendar import Calendar, Event def _make_nested_calendar(depth: int) -> str: """Return an .ics payload with ``depth`` levels of nested VEVENT.""" lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//test//test//EN"] lines += ["BEGIN:VEVENT"] * depth lines += ["UID:nested@example.com", "DTSTAMP:20260101T000000Z"] lines += ["END:VEVENT"] * depth lines += ["END:VCALENDAR"] return "\r\n".join(lines) + "\r\n" @pytest.mark.parametrize("depth", [10, 100, 500, 1000]) @pytest.mark.parametrize("fn", [repr, str], ids=["repr", "str"]) def test_repr_and_str_do_not_raise_recursion_error(fn, depth): """``repr()`` and ``str()`` must not raise ``RecursionError`` regardless of nesting depth. ``str()`` falls through to ``__repr__``, so the same depths are exercised for both. """ ics = _make_nested_calendar(depth) cal = Calendar.from_ical(ics) # Default recursion limit (1000) used to crash for depth >= ~498. result = fn(cal) assert result.startswith("VCALENDAR(") assert result.endswith(")") # Each nested VEVENT is reflected in the output. assert result.count("VEVENT(") == depth @pytest.mark.parametrize("depth", [1, 2, 10, 500, 1000]) def test_repr_exact_output_for_nested_components(depth): """The exact ``repr()`` string matches the format produced by the previous (recursive) implementation: ``VCALENDAR({}, VEVENT({}, ...))``. Built from empty :class:`Component` instances so the expected output can be spelled out exactly without depending on the ``repr`` of any parsed property value. """ cal = Calendar() parent = cal for _ in range(depth): child = Event() parent.add_component(child) parent = child expected = "VCALENDAR({}" + ", VEVENT({}" * depth + ")" * (depth + 1) assert repr(cal) == expected def test_repr_exact_output_for_sibling_components(): """The exact ``repr()`` string for sibling subcomponents matches the previous recursive format: ``VCALENDAR({}, VEVENT({}), VEVENT({}))``. """ cal = Calendar() cal.add_component(Event()) cal.add_component(Event()) assert repr(cal) == "VCALENDAR({}, VEVENT({}), VEVENT({}))" collective-icalendar-138c845/src/icalendar/tests/test_request_status.py000066400000000000000000000043151524134033600264270ustar00rootroot00000000000000"""This tests the REQUEST-STATUS property. See - https://github.com/collective/icalendar/issues/1666 - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3 """ import pytest from icalendar.cal.event import Event from icalendar.cal.free_busy import FreeBusy from icalendar.cal.journal import Journal from icalendar.cal.todo import Todo ETJF = Event | Todo | Journal | FreeBusy @pytest.fixture(params=[Event, Todo, Journal, FreeBusy]) def component(request): """An empty component with possible request status.""" return request.param() def test_no_request_status_at_creation(component: ETJF): """An empty component has no request status.""" assert "REQUEST-STATUS" not in component assert component.REQUEST_STATUS == [] def test_add_one_request_status(component: ETJF): """Add one request status.""" component.add("REQUEST-STATUS", "2.0;Success") assert component.REQUEST_STATUS == ["2.0;Success"] def test_add_multiple_request_status(component: ETJF): """Add request status.""" component.add("REQUEST-STATUS", ["2.0;Success", "3.1;Invalid property value"]) assert component.REQUEST_STATUS == ["2.0;Success", "3.1;Invalid property value"] def test_set_request_status(component: ETJF): """Set request status.""" component.REQUEST_STATUS = ["2.0;Success", "3.1;Invalid property value"] assert component.REQUEST_STATUS == ["2.0;Success", "3.1;Invalid property value"] def test_delete_request_status(component: ETJF): """Delete request status.""" component.REQUEST_STATUS = ["2.0;Success"] del component.REQUEST_STATUS assert "REQUEST-STATUS" not in component assert component.REQUEST_STATUS == [] def test_request_status_roundtrip(component: ETJF): """The request status survives a roundtrip.""" component.REQUEST_STATUS = ["2.0;Success", "3.1;Invalid property value"] ics = component.to_ical() parsed = component.__class__.from_ical(ics) assert parsed.REQUEST_STATUS == ["2.0;Success", "3.1;Invalid property value"] def test_new_with_request_status(component: ETJF): """We can use new with request status.""" component = component.__class__.new(request_status="2.0;Success") assert component.REQUEST_STATUS == ["2.0;Success"] collective-icalendar-138c845/src/icalendar/tests/test_rfc_6868.py000066400000000000000000000061131524134033600245770ustar00rootroot00000000000000"""This implemensts RFC 6868. There are only some changes to parameters needed. """ import os from typing import TYPE_CHECKING import pytest from icalendar.cal.calendar import Calendar from icalendar.parser import dquote, rfc_6868_escape, rfc_6868_unescape if TYPE_CHECKING: from icalendar import vCalAddress, vText param_duplicate = pytest.mark.parametrize( "duplicate", [lambda x: x, lambda cal: Calendar.from_ical(cal.to_ical())] ) @param_duplicate def test_rfc_6868_example(calendars, duplicate): """Check the example from the RFC.""" cal: Calendar = duplicate(calendars.rfc_6868) attendee: vCalAddress = cal.events[0]["attendee"] assert attendee.name == 'George Herman "Babe" Ruth' @param_duplicate def test_all_parameters(calendars, duplicate): """Check that all examples get decoded correctly.""" cal: Calendar = duplicate(calendars.rfc_6868) param = cal["X-PARAM"].params["ALL"] assert param == '^"\n' @param_duplicate def test_unknown_character(calendars, duplicate): """if a ^ (U+005E) character is followed by any character other than the ones above, parsers MUST leave both the ^ and the following character in place""" cal: Calendar = duplicate(calendars.rfc_6868) param = cal["X-PARAM"].params["UNKNOWN"] assert param == "^a^ ^asd" @pytest.mark.parametrize( ("text", "expected"), [ ("^a", "^a"), ("^^", "^"), # ("^n", ), # see other test ("^'", '"'), ("^^a^b", "^a^b"), ], ) @pytest.mark.parametrize( "modify", [lambda x: x, lambda x: x * 10, lambda x: f"asd{x}aaA^AA"] ) def test_unescape(text, expected, modify): """Check unescaping.""" result = rfc_6868_unescape(modify(text)) assert result == modify(expected) @pytest.mark.parametrize("newline", ["\n", "\r\n", "\r", os.linesep]) def test_unescape_newline(newline, monkeypatch): """Unescape the newline.""" monkeypatch.setattr(os, "linesep", newline) assert rfc_6868_unescape("^n") == newline param_values = pytest.mark.parametrize( ("text", "expected"), [ ("", ""), ("^", "^^"), ("^n", "^^n"), ("This text\n has\r several\r\n lines", "This text^n has^n several^n lines"), ('Call me "Harry".', "Call me ^'Harry^'."), ], ) @param_values def test_escape_rfc_6868(text, expected): """Check that we can escape the content with special characters.""" escaped = rfc_6868_escape(text) assert escaped == expected, f"{escaped!r} == {expected!r}" assert rfc_6868_escape(rfc_6868_unescape(escaped)) == expected @param_values def test_escaping_parameters(text, expected): cal = Calendar() cal.add("X-Param", "asd") param: vText = cal["X-PARAM"] param.params["RFC6868"] = text ical = cal.to_ical().decode() print(ical) assert "X-PARAM;RFC6868=" + dquote(expected) in ical def test_encode_example_again(calendars): """The example file should yield its content again.""" cal: Calendar = calendars.rfc_6868 again = Calendar.from_ical(cal.to_ical()) assert cal == again assert cal.to_ical() == again.to_ical() collective-icalendar-138c845/src/icalendar/tests/test_rfc_7529.py000066400000000000000000000047131524134033600245760ustar00rootroot00000000000000"""This tests the compatibility with RFC 7529. See - https://github.com/collective/icalendar/issues/655 - https://www.rfc-editor.org/rfc/rfc7529.html """ import pytest from icalendar.prop import vMonth, vRecur, vSkip @pytest.mark.parametrize( ("uid", "scale"), [ ("4.3.1", "CHINESE"), ("4.3.2", "ETHIOPIC"), ("4.3.3", "HEBREW"), ("4.3.4", "GREGORIAN"), ], ) def test_rscale(calendars, uid, scale): """Check that the RSCALE is parsed correctly.""" event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == uid)[0] print(event.errors) rrule = event["RRULE"] print(rrule) assert rrule["RSCALE"] == [scale] @pytest.mark.parametrize( ("uid", "skip"), [ ("4.3.2", None), ("4.3.3", ["FORWARD"]), ], ) def test_rscale_with_skip(calendars, uid, skip): """Check that the RSCALE is parsed correctly.""" event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == uid)[0] recur = event["RRULE"] assert recur.get("SKIP") == skip def test_leap_month(calendars): """Check that we can parse the leap month.""" event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == "4.3.3")[0] recur = event["RRULE"] assert recur["BYMONTH"][0].leap is True @pytest.mark.parametrize( ("ty", "recur", "ics"), [ ( vRecur, vRecur(rscale="CHINESE", freq="YEARLY"), b"RSCALE=CHINESE;FREQ=YEARLY", ), (vRecur, vRecur(bymonth=vMonth(10)), b"BYMONTH=10"), (vRecur, vRecur(bymonth=vMonth("5L")), b"BYMONTH=5L"), (vMonth, vMonth(10), b"10"), (vMonth, vMonth("5L"), b"5L"), (vSkip, vSkip.OMIT, b"OMIT"), (vSkip, vSkip.BACKWARD, b"BACKWARD"), (vSkip, vSkip.FORWARD, b"FORWARD"), (vSkip, vSkip("OMIT"), b"OMIT"), (vSkip, vSkip("BACKWARD"), b"BACKWARD"), (vSkip, vSkip("FORWARD"), b"FORWARD"), ( vRecur, vRecur(rscale="GREGORIAN", freq="YEARLY", skip="FORWARD"), b"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD", ), ( vRecur, vRecur(rscale="GREGORIAN", freq="YEARLY", skip=vSkip.FORWARD), b"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD", ), ], ) def test_conversion(ty, recur, ics): """Test string conversion.""" assert recur.to_ical() == ics assert ty.from_ical(ics.decode()) == recur assert ty.from_ical(ics.decode()).to_ical() == ics collective-icalendar-138c845/src/icalendar/tests/test_rfc_7953.py000066400000000000000000000171731524134033600246030ustar00rootroot00000000000000"""This tests the parsing of the VAVAILABILITY component as defined in :rfc:`7953`.""" from datetime import date, datetime, timedelta, timezone import pytest from icalendar import BUSYTYPE, Availability, Available, Calendar from icalendar.error import IncompleteComponent @pytest.fixture(params=[Availability, Available]) def rfc_7953_component_class(request): """The component class.""" return request.param @pytest.fixture def rfc_7953_component(rfc_7953_component_class): """The component class.""" return rfc_7953_component_class() def test_uid(availabilities): """Test the UID property.""" availability: Availability = availabilities.rfc_7953_1 assert availability.uid == "0428C7D2-688E-4D2E-AC52-CD112E2469DF" def test_organizer(availabilities): """Test the ORGANIZER property.""" availability: Availability = availabilities.rfc_7953_1 assert availability.organizer == "mailto:bernard@example.com" assert availability.organizer.email == "bernard@example.com" def test_dtstamp(availabilities, tzp): """Test the DTSTAMP property.""" availability: Availability = availabilities.rfc_7953_1 assert availability.DTSTAMP == tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) def test_subcomponents(availabilities): """Test the subcomponents.""" availability: Availability = availabilities.rfc_7953_1 assert len(availability.subcomponents) == 1 assert availability.available == availability.subcomponents def test_busy_type_default(): assert Availability().busy_type == BUSYTYPE.BUSY_UNAVAILABLE def test_set_busy_type(): availability = Availability() availability.busy_type = BUSYTYPE.BUSY assert availability.busy_type == BUSYTYPE.BUSY assert availability["BUSYTYPE"] == "BUSY" def test_new_sets_default(test_uid): availability = Availability.new() assert availability.busy_type == BUSYTYPE.BUSY_UNAVAILABLE assert availability.uid == test_uid def test_create_new_availability_with_subsomponents(dont_validate_new): avaiable = Available.new(summary="Available1") availability = Availability.new(busy_type=BUSYTYPE.BUSY, components=[avaiable]) assert len(availability.available) == 1 assert availability.available[0].summary == "Available1" def test_avaiable_needs_start(): """Start is required and not set automatically""" with pytest.raises(ValueError) as e: Available.new() assert "Available start must be a datetime with a timezone" in str(e.value) @pytest.mark.parametrize("key", ["start", "end"]) @pytest.mark.parametrize( "value", [datetime(2011, 10, 5, 13, 32, 25), date(2011, 10, 5)] ) def test_avaiable_needs_timezone(key, value, rfc_7953_component_class): """Start is required and not set automatically""" kw = {key: value} with pytest.raises(ValueError) as e: rfc_7953_component_class.new(**kw) assert f"{key} must be a datetime with a timezone" in str(e.value) def test_create_with_start(tzp): """Create with a valid start.""" start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) available = Available.new(start=start) assert available.start == start def test_create_with_start_and_end(tzp): """Create with a valid start and end.""" start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) end = tzp.localize_utc(datetime(2011, 10, 5, 14, 32, 25)) available = Available.new(start=start, end=end) assert available.start == start assert available.end == end def test_duration_from_end(rfc_7953_component, tzp): """Check the computation of end and duration.""" rfc_7953_component.start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) rfc_7953_component.end = tzp.localize_utc(datetime(2011, 10, 5, 14, 32, 25)) assert rfc_7953_component.duration == timedelta(hours=1) def test_duration_and_end_from_duration(rfc_7953_component, tzp): """Check the computation of end and duration.""" rfc_7953_component.start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) rfc_7953_component.DURATION = timedelta(hours=1) assert rfc_7953_component.duration == timedelta(hours=1) assert rfc_7953_component.end == tzp.localize_utc(datetime(2011, 10, 5, 14, 32, 25)) def test_start_only_has_no_duration(rfc_7953_component, tzp): """Check the computation of end and duration.""" rfc_7953_component.start = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) assert rfc_7953_component.duration is None assert rfc_7953_component.end is None def test_missing_info_for_end(rfc_7953_component): """We miss information and cannot compute the end.""" rfc_7953_component.DURATION = timedelta(hours=1) with pytest.raises(IncompleteComponent) as e: rfc_7953_component.end # noqa: B018, RUF100 assert "Cannot compute end without start" in str(e.value) def test_missing_info_for_duration(rfc_7953_component, tzp): """We only have a duration.""" rfc_7953_component.end = tzp.localize_utc(datetime(2011, 10, 5, 13, 32, 25)) with pytest.raises(IncompleteComponent) as e: rfc_7953_component.duration # noqa: B018, RUF100 assert "Cannot compute duration without start" in str(e.value) def test_example_2(availabilities, tzp): """Test that we get all the right values after parsing.""" a: Availability = availabilities.rfc_7953_2 assert a.organizer == "mailto:bernard@example.com" assert a.uid == "84D0F948-7FC6-4C1D-BBF3-BA9827B424B5" assert a.stamp == datetime(2011, 10, 5, 13, 32, 25, tzinfo=timezone.utc) assert a.start == tzp.localize(datetime(2011, 10, 2), "America/Montreal") assert a.end == tzp.localize(datetime(2011, 12, 2), "America/Montreal") a1 = a.available[0] assert a1.uid == "7B33093A-7F98-4EED-B381-A5652530F04D" assert a1.summary == "Monday to Thursday from 9:00 to 17:00" assert a1.start == tzp.localize(datetime(2011, 10, 2, 9), "America/Montreal") assert a1.end == tzp.localize(datetime(2011, 10, 2, 17), "America/Montreal") assert len(a1.rrules) == 1 assert a1.rrules[0]["freq"] == ["WEEKLY"] assert a1.rrules[0]["byday"] == ["MO", "TU", "WE", "TH"] assert a1.location == "Main Office" assert a1.exdates == [] a2 = a.available[1] assert a2.uid == "DF39DC9E-D8C3-492F-9101-0434E8FC1896" assert a2.summary == "Friday from 9:00 to 12:00" assert a2.start == tzp.localize(datetime(2011, 10, 6, 9), "America/Montreal") assert a2.end == tzp.localize(datetime(2011, 10, 6, 12), "America/Montreal") assert len(a2.rrules) == 1 assert a2.rrules[0]["freq"] == ["WEEKLY"] assert a2.location == "Branch Office" assert a2.exdates == [] def test_example_3(calendars, tzp): """Test that we get all the right values after parsing.""" calendar: Calendar = calendars.rfc_7953_3 a: Availability = calendar.availabilities[0] assert a.organizer == "mailto:bernard@example.com" assert a.uid == "BE082249-7BDD-4FE0-BDBA-DE6598C32FC9" assert a.stamp == datetime(2011, 10, 5, 13, 32, 25, tzinfo=timezone.utc) assert a.start == tzp.localize(datetime(2011, 10, 2), "America/Montreal") assert a.end == tzp.localize(datetime(2011, 10, 23, 3), "America/Montreal") a1 = a.available[0] assert a1.uid == "54602321-CEDB-4620-9099-757583263981" assert a1.summary == "Monday to Friday from 9:00 to 17:00" assert a1.start == tzp.localize(datetime(2011, 10, 2, 9), "America/Montreal") assert a1.end == tzp.localize(datetime(2011, 10, 2, 17), "America/Montreal") assert len(a1.rrules) == 1 assert a1.rrules[0]["freq"] == ["WEEKLY"] assert a1.rrules[0]["byday"] == ["MO", "TU", "WE", "TH", "FR"] assert a1.location == "Montreal" assert a1.exdates == [] assert len(calendar.availabilities) == 3 collective-icalendar-138c845/src/icalendar/tests/test_rfc_7986.py000066400000000000000000000214401524134033600246010ustar00rootroot00000000000000"""This tests additional attributes from :rfc:`7986`. Some attributes are also available as ``X-*`` attributes. They are also considered. """ from __future__ import annotations from datetime import date, datetime, time, timedelta, timezone import pytest from icalendar import Calendar, Event, Journal, Todo, vText, vUri UTC = timezone.utc @pytest.fixture def calendar() -> Calendar: """Empty calendar""" return Calendar() param_name = pytest.mark.parametrize("name", ["Company Vacation Days", "Calendar Name"]) param_prop = pytest.mark.parametrize("prop", ["NAME", "X-WR-CALNAME"]) @param_prop @param_name def test_get_calendar_name(prop, name, calendar): """Get the name of the calendar.""" calendar.add(prop, name) assert calendar.calendar_name == name @param_name def test_set_calendar_name(name, calendar): """Setting the name overrides the old attributes and sets both NAME and X-WR-CALNAME.""" calendar.calendar_name = name assert calendar.calendar_name == name assert calendar["NAME"] == name assert calendar["X-WR-CALNAME"] == name @param_name @param_prop def test_replace_name(name, prop, calendar): """Setting the name overrides the old attributes.""" calendar[prop] = "Other Name" calendar.calendar_name = name assert calendar.calendar_name == name @param_name @param_prop def test_del_name(name, calendar, prop): """Delete the name.""" calendar.add(prop, name) del calendar.calendar_name assert calendar.calendar_name is None def test_default_name(calendar): """We have no name by default.""" assert calendar.calendar_name is None @param_name def test_setting_the_name_sets_both_properties(calendar, name): """Setting calendar_name sets both NAME and X-WR-CALNAME for compatibility.""" calendar.calendar_name = name assert calendar.calendar_name == name assert calendar["NAME"] == name assert calendar["X-WR-CALNAME"] == name @param_name @pytest.mark.parametrize("order", [1, 2]) def test_multiple_names_use_the_one_without_a_language(calendar, name, order): """Add several names and use the one without a language param.""" if order == 1: calendar.add("NAME", name) calendar.add("NAME", vText("Kalendername", params={"LANGUAGE": "de"})) if order == 2: calendar.add("NAME", name) assert calendar.calendar_name == name @param_name def test_name_is_preferred(calendar, name): """NAME is more important that X-WR-CALNAME""" calendar.add("NAME", name) calendar.add("X-WR-CALNAME", "asd") assert calendar.calendar_name == name # For description, we would use the same tests as name, but we also use the # same code, so it is all right. param_color = pytest.mark.parametrize("desc", ["DESCRIPTION", "X-WR-CALDESC"]) @param_color @param_name def test_description(calendar, desc, name): """Get the value""" calendar.add(desc, name) assert calendar.description == name # For color, we would use the same tests as name, but we also use the # same code, so it is all right. param_color = pytest.mark.parametrize( "color_param", ["COLOR", "X-APPLE-CALENDAR-COLOR"] ) @param_color def test_get_calendar_color(calendar, color_param, color): """Get the value""" calendar.add(color_param, color) assert calendar.color == color @param_color def test_delete_calendar_color(calendar, color_param, color): """Delete the value""" calendar.add(color_param, color) del calendar.color assert calendar.color == "" assert color_param not in calendar @param_color def test_set_calendar_color(calendar, color_param, color): """Set the color and it replaces what is there.""" calendar.add(color_param, "green") calendar.color = color assert calendar.color == color assert calendar["COLOR"] == color def test_get_COLOR_first(calendar, color): """We prefer COLOR over X-APPLE-CALENDAR-COLOR""" calendar.add("COLOR", color) calendar.add("X-APPLE-CALENDAR-COLOR", "green") assert calendar.color == color # The color of the event is a bit different # It only appears once and does not have a backup. @pytest.fixture(params=[Calendar, Event, Todo, Journal]) def color_component(request) -> Calendar | Event | Todo | Journal: """An empty component that should have a color attribute.""" return request.param() @pytest.fixture(params=["blue", "#123456"]) def color(request) -> str: """Return a color.""" return request.param def test_default_color(color_component: Calendar | Event | Todo | Journal): """There is no color by default.""" assert color_component.color == "" def test_set_the_color(color: str, color_component: Calendar | Event | Todo | Journal): """We set the value and get it.""" color_component.color = color assert color_component.color == color assert color_component["COLOR"] == color def test_replace_color(color: str, color_component: Calendar | Event | Todo | Journal): """Replace the color.""" color_component.color = "blue" color_component.color = color assert color_component.color == color assert color_component["COLOR"] == color def test_multiple_colors(color_component: Calendar | Event | Todo | Journal): """Add several colors and use the first one.""" color_component.add("COLOR", "blue") color_component.add("COLOR", "green") assert color_component.color == "blue" def test_delete_the_color(color_component: Calendar | Event | Todo | Journal): """Delete the color.""" color_component.color = "blue" del color_component.color assert "COLOR" not in color_component assert color_component.color == "" def test_set_if_multiple_colors( color: str, color_component: Calendar | Event | Todo | Journal ): """Add several colors and use the first one.""" color_component.add("COLOR", "blue") color_component.add("COLOR", "green") color_component.color = color assert color_component.color == color def test_refresh_interval_default(calendar: Calendar): """REFRESH-INTERVAL default.""" assert calendar.get("REFRESH-INTERVAL") is None assert calendar.refresh_interval is None @pytest.mark.parametrize( "invalid_value", [ datetime(2020, 1, 1), date(2020, 1, 1), "invalid", (date(2020, 1, 1), timedelta(days=1)), time(12, 0), ], ) def test_invalid_refresh_interval_type(calendar: Calendar, invalid_value): """Invalid REFRESH-INTERVAL""" calendar.refresh_interval = timedelta(hours=1) with pytest.raises(TypeError): calendar.refresh_interval = invalid_value assert calendar.refresh_interval == timedelta(hours=1) @pytest.mark.parametrize("invalid_value", [timedelta(seconds=-1), timedelta(seconds=0)]) def test_invalid_refresh_interval(calendar: Calendar, invalid_value): """Invalid REFRESH-INTERVAL. Test: - value validation - no deletion of the current value """ calendar.refresh_interval = timedelta(hours=2) with pytest.raises(ValueError): calendar.refresh_interval = invalid_value assert calendar.refresh_interval == timedelta(hours=2) def test_refresh_interval_set_to_value(calendar: Calendar): """REFRESH-INTERVAL setting.""" calendar.refresh_interval = timedelta(hours=1) assert calendar.refresh_interval == timedelta(hours=1) assert calendar["REFRESH-INTERVAL"].td == timedelta(hours=1) def test_refresh_interval_set_to_value_by_dict(calendar: Calendar): """REFRESH-INTERVAL setting.""" calendar.add("REFRESH-INTERVAL", timedelta(days=1, hours=2)) assert calendar.refresh_interval == timedelta(days=1, hours=2) assert calendar["REFRESH-INTERVAL"].dt == timedelta(days=1, hours=2) def get_rfc_7986_properties_calendar(calendars): """ICS -> obj""" return calendars.rfc_7986_properties def get_rfc_7986_properties_calendar_serialized(calendars): """ICS -> obj -> ICS -> obj""" cal = calendars.rfc_7986_properties return Calendar.from_ical(cal.to_ical()) @pytest.mark.parametrize( "get_calendar", [get_rfc_7986_properties_calendar, get_rfc_7986_properties_calendar_serialized], ) def test_attributes_from_calendar(calendars, get_calendar): """Check that the attributes have a certain value.""" calendar: Calendar = get_calendar(calendars) assert calendar.calendar_name == "RFC 7986 calendar" assert calendar.color == "black" assert calendar.refresh_interval == timedelta(hours=3) assert calendar.last_modified == datetime(2016, 10, 29, 12, 12, 29, tzinfo=UTC), ( "20161029T121229Z" ) assert calendar.uid == "5FC53010-1267-4F8E-BC28-1D7AE55A7C99" assert calendar.description == "We want a lot of RFC 7986 parameters in here!" assert ( calendar.source == "https://github.com/collective/icalendar/tree/main/src/icalendar/tests/calendars/rfc_7986_properties.ics" ) assert isinstance(calendar.source, vUri) collective-icalendar-138c845/src/icalendar/tests/test_rfc_7986_categories.py000066400000000000000000000044251524134033600270120ustar00rootroot00000000000000"""This tests the compatibility with RFC 7529. CATEGORIES property See - https://github.com/collective/icalendar/issues/655 - https://www.rfc-editor.org/rfc/rfc7529.html """ import pytest from icalendar.cal.calendar import Calendar from icalendar.cal.event import Event from icalendar.cal.journal import Journal from icalendar.cal.todo import Todo CTJE = Calendar | Event | Journal | Todo @pytest.fixture(params=[Event, Calendar, Todo, Journal]) def component(request): """An empty component with possible categories.""" return request.param() def test_no_categories_at_creation(component: CTJE): """An empty component has no categories.""" assert "CATEGORIES" not in component assert component.categories == [] def test_add_one_category(component: CTJE): """Add one category.""" component.add("categories", "Lecture") assert component.categories == ["Lecture"] def test_add_multiple_categories(component: CTJE): """Add categories.""" component.add("categories", ["Lecture", "Workshop"]) assert component.categories == ["Lecture", "Workshop"] def test_set_categories(component: CTJE): """Set categories.""" component.categories = ["Lecture", "Workshop"] assert component.categories == ["Lecture", "Workshop"] def test_modify_list(component: CTJE): """Modify the list and it still works.""" component.categories = categories = ["cat1"] categories.append("cat2") assert component.categories == ["cat1", "cat2"] def test_delete_categories(component: CTJE): """Delete categories.""" component.categories = ["Lecture", "Workshop"] del component.categories assert "CATEGORIES" not in component assert component.categories == [] def test_manage_add_append_remove_categories(component: CTJE): """Manage multiple categories by merging them, then append and remove a category from the resulting set.""" component.add("categories", ["c1", "c2"]) component.add("categories", ["c3", "c4"]) assert component.categories == ["c1", "c2", "c3", "c4"] component.categories.append("c5") assert component.categories == ["c1", "c2", "c3", "c4", "c5"] component.categories.remove("c2") assert component.categories == ["c1", "c3", "c4", "c5"] assert "c1,c3,c4,c5" in component.to_ical().decode() collective-icalendar-138c845/src/icalendar/tests/test_rfc_9074.py000066400000000000000000000027051524134033600245720ustar00rootroot00000000000000"""Test the VALARM compatibility of RFC 9074. See https://www.rfc-editor.org/rfc/rfc9074.html and also https://github.com/collective/icalendar/issues/657 """ import pytest from icalendar.prop import vDDDTypes, vText @pytest.mark.parametrize( ("prop", "cls", "file", "alarm_index"), [ ("UID", vText, "rfc_9074_example_1", 0), ("RELATED-TO", vText, "rfc_9074_example_2", 1), ("ACKNOWLEDGED", vDDDTypes, "rfc_9074_example_3", 0), ("PROXIMITY", vText, "rfc_9074_example_proximity", 0), ], ) def test_calendar_types(events, prop, cls, file, alarm_index): """Check the types of the properties.""" event = events[file] alarm = event.subcomponents[alarm_index] value = alarm[prop] assert isinstance(value, cls) def test_snooze(events): """Check values of the alarms.""" alarm_1 = events.rfc_9074_example_3.subcomponents[0] assert alarm_1["ACKNOWLEDGED"].dt == vDDDTypes.from_ical("20210302T152024Z") alarm_2 = events.rfc_9074_example_3.subcomponents[1] assert alarm_2["RELATED-TO"] == "8297C37D-BA2D-4476-91AE-C1EAA364F8E1" assert alarm_2["RELATED-TO"].params["RELTYPE"] == "SNOOZE" def test_proximity(events): """Check the proximity values.""" alarm = events.rfc_9074_example_proximity.subcomponents[0] assert alarm["PROXIMITY"] == "DEPART" assert len(alarm.subcomponents) == 1 location = alarm.subcomponents[0] assert location["UID"] == "123456-abcdef-98765432" collective-icalendar-138c845/src/icalendar/tests/test_time.py000066400000000000000000000037261524134033600242770ustar00rootroot00000000000000import datetime import os from datetime import time from icalendar import Calendar from icalendar.prop.dt.time import vTime def test_value_type_is_not_mapped(types_factory): """Usually, the value should be absent.""" assert "X-SOMETIME" not in types_factory.types_map def test_value_type_is_mapped(x_sometime, types_factory): """The value is mapped for the test.""" assert "X-SOMETIME" in types_factory.types_map def test_create_from_ical(x_sometime): directory = os.path.dirname(__file__) # noqa: PTH120 ics = open(os.path.join(directory, "calendars", "time.ics"), "rb") # noqa: PTH118, PTH123, SIM115 cal = Calendar.from_ical(ics.read()) ics.close() assert cal["X-SOMETIME"].dt == datetime.time(17, 20, 10) assert cal["X-SOMETIME"].to_ical() == "172010" def test_create_to_ical(x_sometime): cal = Calendar() cal.add("X-SOMETIME", datetime.time(17, 20, 10)) assert b"X-SOMETIME;VALUE=TIME:172010" in cal.to_ical().splitlines() def test_vtime_multiple_timezones(calendars): cal = calendars["multiple_timezones.ics"] events = list(cal.walk("VEVENT")) named = events[0]["RDATE"].dts[0].dt utc = events[1]["RDATE"].dts[0].dt floating = events[2]["RDATE"].dts[0].dt assert named.tzinfo is not None assert "America/New_York" in str(named.tzinfo) assert str(utc.tzinfo) == "UTC" assert floating.tzinfo is None def test_vtime_with_tzinfo_object(): tz = datetime.timezone.utc parsed = vTime.from_ical("083000", timezone=tz) assert parsed.tzinfo == tz def test_localize_time(tzp): t = time(8, 30) tz = tzp.timezone("America/New_York") localized = tzp.localize(t, tz) assert isinstance(localized, time) assert localized.tzinfo is not None assert "America/New_York" in str(localized.tzinfo) def test_localized_time_utc(tzp): t = time(8, 30) localized = tzp.localize_utc(t) assert isinstance(localized, time) assert str(localized.tzinfo) == "UTC" collective-icalendar-138c845/src/icalendar/tests/test_timezone_identification.py000066400000000000000000000057041524134033600302420ustar00rootroot00000000000000"""Test that we can identify all timezones. Timezones can be removed from ./timezone_ids.py if they make the tests fail: Timezone information changes over time and can be dependent on the operating system's timezone database (zoneinfo, dateutil) or the package (pytz). We want to make sure we can roughly identify most of them. """ import sys from datetime import datetime, timedelta, timezone import pytest from icalendar.timezone import is_utc, tzid_from_tzinfo, tzids_from_tzinfo from icalendar.timezone.tzp import TZP def test_can_identify_zoneinfo(tzid, zoneinfo_only, tzp: TZP): """Check that all those zoneinfo timezones can be identified.""" assert tzid in tzids_from_tzinfo(tzp.timezone(tzid)) def test_can_identify_pytz(tzid, pytz_only, tzp: TZP): """Check that all those pytz timezones can be identified.""" assert tzid in tzids_from_tzinfo(tzp.timezone(tzid)) def test_can_identify_dateutil(tzid): """Check that all those dateutil timezones can be identified.""" from dateutil.tz import gettz assert tzid in tzids_from_tzinfo(gettz(tzid)) def test_utc_is_identified(utc): """Test UTC because it is handled in a special way.""" assert "UTC" in tzids_from_tzinfo(utc) assert tzid_from_tzinfo(utc) == "UTC" assert is_utc(utc) def test_some_timezones_are_not_utc(tzp: TZP): """If we have an offset, it is not utc.""" assert not is_utc(tzp.timezone("Europe/Berlin")) def test_some_timezones_are_recognized_as_utc(): """If we are at 0 offset, it is utc.""" tzinfo = timezone(timedelta(hours=0)) assert is_utc(tzinfo) assert is_utc(datetime(2019, 1, 1, 0, 0, 0, 0, tzinfo=tzinfo)) @pytest.mark.parametrize( "tzinfo", [ timezone(timedelta(hours=1)), timezone(timedelta(hours=-1)), timezone(timedelta(hours=2)), timezone(timedelta(hours=-2)), ], ) def test_getting_a_timezone_name_for_timezones(tzinfo): """We should get a name that we can use.""" assert tzid_from_tzinfo(tzinfo) @pytest.mark.skipif(sys.platform != "win32", reason="tzwin only exists on Windows") def test_tzwin_utc_is_identified(): """tzwin('UTC') from dateutil.tz.gettz('UTC') must be recognized as UTC. On Windows, dateutil.tz.gettz('UTC') returns tzwin('UTC') whose tzname() is 'Coordinated Universal Time', not 'UTC'. This must still be identified as UTC so that datetimes use the Z suffix instead of a TZID parameter. """ from dateutil.tz import gettz tzinfo = gettz("UTC") # returns tzwin('UTC') on Windows assert "UTC" in tzids_from_tzinfo(tzinfo) assert tzid_from_tzinfo(tzinfo) == "UTC" assert is_utc(tzinfo) @pytest.mark.skipif(sys.platform != "win32", reason="tzwin only exists on Windows") def test_tzwin_non_utc_is_identified(): """tzwin for a non-UTC Windows timezone maps to its IANA equivalent.""" from dateutil.tz import gettz tzinfo = gettz("Eastern Standard Time") assert tzid_from_tzinfo(tzinfo) == "America/New_York" collective-icalendar-138c845/src/icalendar/tests/test_timezoned.py000066400000000000000000000441401524134033600253320ustar00rootroot00000000000000import datetime import dateutil.parser import icalendar import icalendar.cal.calendar import icalendar.cal.event import icalendar.cal.timezone from icalendar.timezone import tzid_from_dt def test_create_from_ical(calendars, other_tzp): """Create a calendar from a .ics file.""" cal = calendars.timezoned assert cal["prodid"].to_ical() == b"-//Plone.org//NONSGML plone.app.event//EN" timezones = cal.walk("VTIMEZONE") assert len(timezones) == 1 tz = timezones[0] assert tz["tzid"].to_ical() == b"Europe/Vienna" std = tz.walk("STANDARD")[0] assert std.decoded("TZOFFSETFROM") == datetime.timedelta(0, 7200) ev1 = cal.walk("VEVENT")[0] assert ev1.decoded("DTSTART") == other_tzp.localize( datetime.datetime(2012, 2, 13, 10, 0, 0), "Europe/Vienna" ) assert ev1.decoded("DTSTAMP") == other_tzp.localize( datetime.datetime(2010, 10, 10, 9, 10, 10), "UTC" ) def test_create_to_ical(tzp): cal = icalendar.cal.calendar.Calendar() cal.add("prodid", "-//Plone.org//NONSGML plone.app.event//EN") cal.add("version", "2.0") cal.add("x-wr-calname", "test create calendar") cal.add("x-wr-caldesc", "icalendar tests") cal.add("x-wr-relcalid", "12345") cal.add("x-wr-timezone", "Europe/Vienna") tzc = icalendar.cal.timezone.Timezone() tzc.add("tzid", "Europe/Vienna") tzc.add("x-lic-location", "Europe/Vienna") tzs = icalendar.cal.timezone.TimezoneStandard() tzs.add("tzname", "CET") tzs.add("dtstart", datetime.datetime(1970, 10, 25, 3, 0, 0)) tzs.add("rrule", {"freq": "yearly", "bymonth": 10, "byday": "-1su"}) tzs.add("TZOFFSETFROM", datetime.timedelta(hours=2)) tzs.add("TZOFFSETTO", datetime.timedelta(hours=1)) tzd = icalendar.cal.timezone.TimezoneDaylight() tzd.add("tzname", "CEST") tzd.add("dtstart", datetime.datetime(1970, 3, 29, 2, 0, 0)) tzs.add("rrule", {"freq": "yearly", "bymonth": 3, "byday": "-1su"}) tzd.add("TZOFFSETFROM", datetime.timedelta(hours=1)) tzd.add("TZOFFSETTO", datetime.timedelta(hours=2)) tzc.add_component(tzs) tzc.add_component(tzd) cal.add_component(tzc) event = icalendar.cal.event.Event() event.add( "dtstart", tzp.localize(datetime.datetime(2012, 2, 13, 10, 00, 00), "Europe/Vienna"), ) event.add( "dtend", tzp.localize(datetime.datetime(2012, 2, 17, 18, 00, 00), "Europe/Vienna"), ) event.add( "dtstamp", tzp.localize(datetime.datetime(2010, 10, 10, 10, 10, 10), "Europe/Vienna"), ) event.add( "created", tzp.localize(datetime.datetime(2010, 10, 10, 10, 10, 10), "Europe/Vienna"), ) event.add("uid", "123456") event.add( "last-modified", tzp.localize(datetime.datetime(2010, 10, 10, 10, 10, 10), "Europe/Vienna"), ) event.add("summary", "artsprint 2012") # event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10') event.add("description", "sprinting at the artsprint") event.add("location", "aka bild, wien") event.add("categories", "first subject") event.add("categories", "second subject") event.add("attendee", "häns") event.add("attendee", "franz") event.add("attendee", "sepp") event.add("contact", "Max Mustermann, 1010 Wien") event.add("url", "https://plone.org") cal.add_component(event) test_out = b"|".join(cal.to_ical().splitlines()) test_out = test_out.decode("utf-8") vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:" "Europe/Vienna|BEGIN:STANDARD|DTSTART:19701025T03" "0000|RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10|RRULE:FREQ=YEARLY;B" "YDAY=-1SU;BYMONTH=3|TZNAME:CET|TZOFFSETFROM:+0200|TZOFFSETTO:+01" "00|END:STANDARD|BEGIN:DAYLIGHT|DTSTART:19700329T" "020000|TZNAME:CEST|TZOFFSETFROM:+0100|TZOFFSETTO:+0200|END:DAYLI" "GHT|END:VTIMEZONE" assert vtimezone_lines in test_out test_str = "DTSTART;TZID=Europe/Vienna:20120213T100000" assert test_str in test_out assert "ATTENDEE:sepp" in test_out # ical standard expects DTSTAMP and CREATED in UTC assert "DTSTAMP:20101010T081010Z" in test_out assert "CREATED:20101010T081010Z" in test_out def test_tzinfo_dateutil(): """Test for issues #77, #63 references: #73,7430b66862346fe3a6a100ab25e35a8711446717 """ date = dateutil.parser.parse("2012-08-30T22:41:00Z") date2 = dateutil.parser.parse("2012-08-30T22:41:00 +02:00") assert date.tzinfo.__module__.startswith("dateutil.tz") assert date2.tzinfo.__module__.startswith("dateutil.tz") # make sure, it's parsed properly and doesn't throw an error assert icalendar.vDDDTypes(date).to_ical() == b"20120830T224100Z" assert icalendar.vDDDTypes(date2).to_ical() == b"20120830T224100" def test_create_america_new_york(calendars, tzp): """testing America/New_York, the most complex example from the RFC""" cal = calendars.america_new_york dt = cal.events[0].start assert tzid_from_dt(dt) in ("custom_America/New_York", "EDT") def test_create_america_new_york_forward_reference(calendars, tzp): """testing America/New_York variant with VTIMEZONE as a forward reference""" cal = calendars.america_new_york_forward_reference dt = cal.walk("VEVENT")[0]["DTSTART"][0].dt assert tzid_from_dt(dt) in ("custom_America/New_York_Forward_reference", "EDT") def test_america_new_york_with_pytz(calendars, tzp, pytz_only): """Create a custom timezone with pytz and test the transition times.""" print(tzp) cal = calendars.america_new_york dt = cal.events[0].start tz = dt.tzinfo tz_new_york = tzp.timezone("America/New_York") # for reasons (tm) the locally installed version of the timezone # database isn't always complete, therefore we only compare some # transition times ny_transition_times = [] ny_transition_info = [] for num, date in enumerate(tz_new_york._utc_transition_times): if ( datetime.datetime(1967, 4, 30, 7, 0) <= date <= datetime.datetime(2037, 11, 1, 6, 0) ): ny_transition_times.append(date) ny_transition_info.append(tz_new_york._transition_info[num]) assert tz._utc_transition_times[:142] == ny_transition_times assert tz._transition_info[0:142] == ny_transition_info assert ( datetime.timedelta(-1, 72000), datetime.timedelta(0, 3600), "EDT", ) in tz._tzinfos assert ( datetime.timedelta(-1, 68400), datetime.timedelta(0), "EST", ) in tz._tzinfos fiji_transition_times = [ datetime.datetime(1915, 10, 25, 12, 4), datetime.datetime(1998, 10, 31, 14, 0), datetime.datetime(1999, 2, 27, 14, 0), datetime.datetime(1999, 11, 6, 14, 0), datetime.datetime(2000, 2, 26, 14, 0), datetime.datetime(2009, 11, 28, 14, 0), datetime.datetime(2010, 3, 27, 14, 0), datetime.datetime(2010, 10, 23, 14, 0), datetime.datetime(2011, 3, 5, 14, 0), datetime.datetime(2011, 10, 22, 14, 0), datetime.datetime(2012, 1, 21, 14, 0), datetime.datetime(2012, 10, 20, 14, 0), datetime.datetime(2013, 1, 19, 14, 0), datetime.datetime(2013, 10, 26, 14, 0), datetime.datetime(2014, 1, 18, 13, 0), datetime.datetime(2014, 10, 25, 14, 0), datetime.datetime(2015, 1, 17, 13, 0), datetime.datetime(2015, 10, 24, 14, 0), datetime.datetime(2016, 1, 23, 13, 0), datetime.datetime(2016, 10, 22, 14, 0), datetime.datetime(2017, 1, 21, 13, 0), datetime.datetime(2017, 10, 21, 14, 0), datetime.datetime(2018, 1, 20, 13, 0), datetime.datetime(2018, 10, 20, 14, 0), datetime.datetime(2019, 1, 19, 13, 0), datetime.datetime(2019, 10, 26, 14, 0), datetime.datetime(2020, 1, 18, 13, 0), datetime.datetime(2020, 10, 24, 14, 0), datetime.datetime(2021, 1, 23, 13, 0), datetime.datetime(2021, 10, 23, 14, 0), datetime.datetime(2022, 1, 22, 13, 0), datetime.datetime(2022, 10, 22, 14, 0), datetime.datetime(2023, 1, 21, 13, 0), datetime.datetime(2023, 10, 21, 14, 0), datetime.datetime(2024, 1, 20, 13, 0), datetime.datetime(2024, 10, 26, 14, 0), datetime.datetime(2025, 1, 18, 13, 0), datetime.datetime(2025, 10, 25, 14, 0), datetime.datetime(2026, 1, 17, 13, 0), datetime.datetime(2026, 10, 24, 14, 0), datetime.datetime(2027, 1, 23, 13, 0), datetime.datetime(2027, 10, 23, 14, 0), datetime.datetime(2028, 1, 22, 13, 0), datetime.datetime(2028, 10, 21, 14, 0), datetime.datetime(2029, 1, 20, 13, 0), datetime.datetime(2029, 10, 20, 14, 0), datetime.datetime(2030, 1, 19, 13, 0), datetime.datetime(2030, 10, 26, 14, 0), datetime.datetime(2031, 1, 18, 13, 0), datetime.datetime(2031, 10, 25, 14, 0), datetime.datetime(2032, 1, 17, 13, 0), datetime.datetime(2032, 10, 23, 14, 0), datetime.datetime(2033, 1, 22, 13, 0), datetime.datetime(2033, 10, 22, 14, 0), datetime.datetime(2034, 1, 21, 13, 0), datetime.datetime(2034, 10, 21, 14, 0), datetime.datetime(2035, 1, 20, 13, 0), datetime.datetime(2035, 10, 20, 14, 0), datetime.datetime(2036, 1, 19, 13, 0), datetime.datetime(2036, 10, 25, 14, 0), datetime.datetime(2037, 1, 17, 13, 0), datetime.datetime(2037, 10, 24, 14, 0), datetime.datetime(2038, 1, 23, 13, 0), datetime.datetime(2038, 10, 23, 14, 0), ] fiji_transition_info = ( [ ( datetime.timedelta(0, 43200), datetime.timedelta(0), "custom_Pacific/Fiji_19151026T000000_+115544_+1200", ) ] + 3 * [ ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), "custom_Pacific/Fiji_19981101T020000_+1200_+1300", ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), "custom_Pacific/Fiji_19990228T030000_+1300_+1200", ), ] + 3 * [ ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), "custom_Pacific/Fiji_20101024T020000_+1200_+1300", ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), "custom_Pacific/Fiji_19990228T030000_+1300_+1200", ), ] + 25 * [ ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), "custom_Pacific/Fiji_20101024T020000_+1200_+1300", ), ( datetime.timedelta(0, 43200), datetime.timedelta(0), "custom_Pacific/Fiji_20140119T020000_+1300_+1200", ), ] + [ ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), "custom_Pacific/Fiji_20101024T020000_+1200_+1300", ) ] ) def test_create_pacific_fiji(calendars, pytz_only): """testing Pacific/Fiji, another pretty complex example with more than one RDATE property per subcomponent""" cal = calendars.pacific_fiji tz = cal.walk("VEVENT")[0]["DTSTART"][0].dt.tzinfo assert str(tz) == "custom_Pacific/Fiji" assert tz._utc_transition_times == fiji_transition_times assert tz._transition_info == fiji_transition_info assert ( datetime.timedelta(0, 46800), datetime.timedelta(0, 3600), "custom_Pacific/Fiji_19981101T020000_+1200_+1300", ) in tz._tzinfos assert ( datetime.timedelta(0, 43200), datetime.timedelta(0), "custom_Pacific/Fiji_19990228T030000_+1300_+1200", ) in tz._tzinfos # these are the expected offsets before and after the fiji_transition_times fiji_expected_offsets = [ (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), (datetime.timedelta(hours=13), datetime.timedelta(hours=13)), (datetime.timedelta(hours=12), datetime.timedelta(hours=12)), ] def test_transition_times_fiji(tzp, timezones): """The transition times are computed.""" tz = timezones.pacific_fiji.to_tz(tzp, lookup_tzid=False) offsets = [] # [(before, after), ...] for _i, transition_time in enumerate(fiji_transition_times): before_after_offset = [] for offset in (datetime.timedelta(hours=-1), datetime.timedelta(hours=+1)): time_in_timezone = tzp.localize(transition_time + offset, tz) utc_offset = time_in_timezone.utcoffset() before_after_offset.append(utc_offset) offsets.append(tuple(before_after_offset)) assert offsets == fiji_expected_offsets def test_same_start_date(calendars): """testing if we can handle VTIMEZONEs whose different components have the same start DTIMEs.""" cal = calendars.timezone_same_start d = cal.subcomponents[1]["DTSTART"].dt assert d.strftime("%c") == "Fri Feb 24 12:00:00 2017" def test_same_start_date_and_offset(calendars): """testing if we can handle VTIMEZONEs whose different components have the same DTSTARTs, TZOFFSETFROM, and TZOFFSETTO.""" cal = calendars.timezone_same_start_and_offset d = cal.subcomponents[1]["DTSTART"].dt assert d.strftime("%c") == "Fri Feb 24 12:00:00 2017" def test_rdate(calendars): """testing if we can handle VTIMEZONEs who only have an RDATE, not RRULE""" cal = calendars.timezone_rdate vevent = cal.walk("VEVENT")[0] assert tzid_from_dt(vevent["DTSTART"].dt) in ("posix/Europe/Vaduz", "CET") def test_rdate_pytz(calendars, pytz_only): """testing if we can handle VTIMEZONEs who only have an RDATE, not RRULE""" cal = calendars.timezone_rdate vevent = cal.walk("VEVENT")[0] tz = vevent["DTSTART"].dt.tzinfo assert tz._utc_transition_times[:6] == [ datetime.datetime(1901, 12, 13, 20, 45, 38), datetime.datetime(1941, 5, 5, 0, 0, 0), datetime.datetime(1941, 10, 6, 0, 0, 0), datetime.datetime(1942, 5, 4, 0, 0, 0), datetime.datetime(1942, 10, 5, 0, 0, 0), datetime.datetime(1981, 3, 29, 1, 0), ] assert tz._transition_info[:6] == [ (datetime.timedelta(0, 3600), datetime.timedelta(0), "CET"), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), "CEST"), (datetime.timedelta(0, 3600), datetime.timedelta(0), "CET"), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), "CEST"), (datetime.timedelta(0, 3600), datetime.timedelta(0), "CET"), (datetime.timedelta(0, 7200), datetime.timedelta(0, 3600), "CEST"), ] collective-icalendar-138c845/src/icalendar/tests/test_tools.py000066400000000000000000000004631524134033600244740ustar00rootroot00000000000000from datetime import datetime from icalendar.tools import normalize_pytz def test_normalize_pytz(pytz_only): import pytz tz = pytz.timezone("Europe/London") pytz_dt = tz.localize(datetime(2024, 1, 1, 10, 0, 0)) result = normalize_pytz(pytz_dt) assert result.tzinfo == pytz_dt.tzinfo collective-icalendar-138c845/src/icalendar/tests/test_type_hints.py000066400000000000000000000022361524134033600255220ustar00rootroot00000000000000from icalendar.caselessdict import CaselessDict, canonsort_items, canonsort_keys def test_caselessdict_basic_behavior(): d = CaselessDict() # test __setitem__ and key normalization d["a"] = 1 d["B"] = 2 assert d["A"] == 1 assert d["b"] == 2 assert set(d.keys()) == {"A", "B"} # test sorted_keys and sorted_items class MyDict(CaselessDict): canonical_order = ["A"] m = MyDict() m["b"] = 2 m["a"] = 1 assert m.sorted_keys() == ["A", "B"] assert [k for k, _ in m.sorted_items()] == ["A", "B"] def test_canonsort_keys_basic(): keys = ["C", "A", "B"] canonical = ["B", "A"] sorted_keys = canonsort_keys(keys, canonical) # canonical keys come first, in order assert sorted_keys[:2] == ["B", "A"] # remaining keys are alphabetical assert sorted_keys == ["B", "A", "C"] def test_canonsort_items_basic(): d = {"C": 3, "A": 1, "B": 2} canonical = ["A"] sorted_items = canonsort_items(d, canonical) # First item is canonical assert sorted_items[0] == ("A", 1) # Remaining keys follow alphabetical order assert sorted_items == [("A", 1), ("B", 2), ("C", 3)] collective-icalendar-138c845/src/icalendar/tests/test_unit_cal.py000066400000000000000000000421051524134033600251310ustar00rootroot00000000000000import itertools import re import sys from datetime import datetime, timedelta import pytest import icalendar from icalendar import prop from icalendar.cal.calendar import Calendar from icalendar.cal.component import Component from icalendar.cal.event import Event from icalendar.cal.journal import Journal from icalendar.timezone import tzid_from_dt def test_cal_Component(calendar_component): """A component is like a dictionary with extra methods and attributes.""" assert calendar_component assert calendar_component.is_empty() def test_nonempty_calendar_component(calendar_component): """Every key defines a property.A property can consist of either a single item. This can be set with a single value... """ calendar_component["prodid"] = "-//max m//icalendar.mxm.dk/" assert not calendar_component.is_empty() assert calendar_component == Calendar({"PRODID": "-//max m//icalendar.mxm.dk/"}) # or with a list calendar_component["ATTENDEE"] = ["Max M", "Rasmussen"] assert calendar_component == Calendar( {"ATTENDEE": ["Max M", "Rasmussen"], "PRODID": "-//max m//icalendar.mxm.dk/"} ) def test_add_multiple_values(event_component): """add multiple values to a property. If you use the add method you don't have to considder if a value is a list or not. """ # add multiple values at once event_component.add("attendee", ["test@test.com", "test2@test.com"]) # or add one per line event_component.add("attendee", "maxm@mxm.dk") event_component.add("attendee", "test@example.dk") # add again multiple values at once to very concatenaton of lists event_component.add("attendee", ["test3@test.com", "test4@test.com"]) assert event_component == Event( { "ATTENDEE": [ prop.vCalAddress("test@test.com"), prop.vCalAddress("test2@test.com"), prop.vCalAddress("maxm@mxm.dk"), prop.vCalAddress("test@example.dk"), prop.vCalAddress("test3@test.com"), prop.vCalAddress("test4@test.com"), ] } ) def test_get_content_directly(c): """You can get the values back directly ...""" c.add("prodid", "-//my product//") assert c["prodid"] == prop.vText("-//my product//") # ... or decoded to a python type assert c.decoded("prodid") == "-//my product//" def test_get_default_value(c): """With default values for non existing properties""" assert c.decoded("version", "No Version") == "No Version" def test_default_list_example(c): c.add("rdate", [datetime(2013, 3, 28), datetime(2013, 3, 27)]) assert isinstance(c.decoded("rdate"), prop.vDDDLists) def test_render_component(calendar_component): """The component can render itself in the RFC 5545 format.""" calendar_component.add("attendee", "Max M") assert ( calendar_component.to_ical() == b"BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n" ) def test_nested_component_event_ics(filled_event_component): """Check the ical string of the event component.""" assert filled_event_component.to_ical() == ( b"BEGIN:VEVENT\r\nDTEND:20000102T000000\r\n" b"DTSTART:20000101T000000\r\nSUMMARY:A brief history of time\r" b"\nEND:VEVENT\r\n" ) def test_nested_components(calendar_component, filled_event_component): """Components can be nested, so You can add a subcomponent. Eg a calendar holds events.""" assert calendar_component.subcomponents == [ Event( { "DTEND": "20000102T000000", "DTSTART": "20000101T000000", "SUMMARY": "A brief history of time", } ) ] def test_walk_filled_calendar_component(calendar_component, filled_event_component): """We can walk over nested componentes with the walk method.""" assert [i.name for i in calendar_component.walk()] == ["VCALENDAR", "VEVENT"] def test_filter_walk(calendar_component, filled_event_component): """We can also just walk over specific component types, by filtering them on their name.""" assert [i.name for i in calendar_component.walk("VEVENT")] == ["VEVENT"] assert [i["dtstart"] for i in calendar_component.walk("VEVENT")] == [ "20000101T000000" ] def test_walk_handles_deeply_nested_components(): """Deeply nested input should not exceed Python's recursion limit.""" depth = sys.getrecursionlimit() + 50 ical = ( "BEGIN:VCALENDAR\r\n" + ("BEGIN:X-DEEP\r\n" * depth) + ("END:X-DEEP\r\n" * depth) + "END:VCALENDAR\r\n" ) calendar = Calendar.from_ical(ical) walked = calendar.walk() assert len(walked) == depth + 1 assert walked[0].name == "VCALENDAR" assert all(component.name == "X-DEEP" for component in walked[1:]) def test_recursive_property_items(calendar_component, filled_event_component): """We can enumerate property items recursively with the property_items method.""" calendar_component.add("attendee", "Max M") assert calendar_component.property_items() == [ ("BEGIN", b"VCALENDAR"), ("ATTENDEE", prop.vCalAddress("Max M")), ("BEGIN", b"VEVENT"), ("DTEND", "20000102T000000"), ("DTSTART", "20000101T000000"), ("SUMMARY", "A brief history of time"), ("END", b"VEVENT"), ("END", b"VCALENDAR"), ] def test_flat_property_items(calendar_component, filled_event_component): """We can also enumerate property items just under the component.""" calendar_component.add("attendee", "Max M") assert calendar_component.property_items(recursive=False) == [ ("BEGIN", b"VCALENDAR"), ("ATTENDEE", prop.vCalAddress("Max M")), ("END", b"VCALENDAR"), ] def test_flat_property_items_2(filled_event_component): """Flat enumeration on the event.""" assert filled_event_component.property_items(recursive=False) == [ ("BEGIN", b"VEVENT"), ("DTEND", "20000102T000000"), ("DTSTART", "20000101T000000"), ("SUMMARY", "A brief history of time"), ("END", b"VEVENT"), ] def test_indent(): """Text fields which span multiple mulitple lines require proper indenting""" c = Calendar() c["description"] = "Paragraph one\n\nParagraph two" assert c.to_ical() == ( b"BEGIN:VCALENDAR\r\nDESCRIPTION:Paragraph one\\n\\nParagraph two" b"\r\nEND:VCALENDAR\r\n" ) def test_INLINE_properties(calendar_with_resources): """INLINE properties have their values on one property line. Note the double quoting of the value with a colon in it. """ assert calendar_with_resources == Calendar( {"RESOURCES": 'Chair, Table, "Room: 42"'} ) assert calendar_with_resources.to_ical() == ( b'BEGIN:VCALENDAR\r\nRESOURCES:Chair\\, Table\\, "Room: 42"\r\n' b"END:VCALENDAR\r\n" ) def test_get_inline(calendar_with_resources): """The inline values must be handled by the get_inline() and set_inline() methods. """ assert calendar_with_resources.get_inline("resources", decode=0) == [ "Chair", "Table", "Room: 42", ] def test_get_inline_decoded(calendar_with_resources): """These can also be decoded""" assert calendar_with_resources.get_inline("resources", decode=1) == [ b"Chair", b"Table", b"Room: 42", ] def test_set_inline(calendar_with_resources): """You can set them directly ...""" calendar_with_resources.set_inline( "resources", ["A", "List", "of", "some, recources"], encode=1 ) assert calendar_with_resources["resources"] == 'A,List,of,"some, recources"' assert calendar_with_resources.get_inline("resources", decode=0) == [ "A", "List", "of", "some, recources", ] def test_inline_free_busy_inline(c): c["freebusy"] = ( "19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z" ) assert c.get_inline("freebusy", decode=0) == [ "19970308T160000Z/PT3H", "19970308T200000Z/PT1H", "19970308T230000Z/19970309T000000Z", ] freebusy = c.get_inline("freebusy", decode=1) assert isinstance(freebusy[0][0], datetime) assert isinstance(freebusy[0][1], timedelta) def test_cal_Component_add(comp, tzp): """Test the for timezone correctness: dtstart should preserve it's timezone, created, dtstamp and last-modified must be in UTC. """ comp.add("dtstart", tzp.localize(datetime(2010, 10, 10, 10, 0, 0), "Europe/Vienna")) comp.add("created", datetime(2010, 10, 10, 12, 0, 0)) comp.add("dtstamp", tzp.localize(datetime(2010, 10, 10, 14, 0, 0), "Europe/Vienna")) comp.add("last-modified", tzp.localize_utc(datetime(2010, 10, 10, 16, 0, 0))) lines = comp.to_ical().splitlines() assert b"DTSTART;TZID=Europe/Vienna:20101010T100000" in lines assert b"CREATED:20101010T120000Z" in lines assert b"DTSTAMP:20101010T120000Z" in lines assert b"LAST-MODIFIED:20101010T160000Z" in lines def test_cal_Component_add_no_reencode(comp): """Already encoded values should not be re-encoded.""" comp.add("ATTACH", "me") comp.add("ATTACH", "you", encode=False) binary = prop.vBinary("us") comp.add("ATTACH", binary) assert comp["ATTACH"] == ["me", "you", binary] def test_cal_Component_add_property_parameter(comp): """Test the for timezone correctness: dtstart should preserve it's timezone, crated, dtstamp and last-modified must be in UTC. """ comp.add("X-TEST-PROP", "tryout.", parameters={"prop1": "val1", "prop2": "val2"}) lines = comp.to_ical().splitlines() assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines comp_prop = pytest.mark.parametrize( ("component_name", "property_name"), [ ("VEVENT", "DTSTART"), ("VEVENT", "DTEND"), ("VEVENT", "RECURRENCE-ID"), ("VTODO", "DUE"), ], ) @comp_prop def test_cal_Component_from_ical(component_name, property_name, tzp): """Check for proper handling of TZID parameter of datetime properties""" component_str = "BEGIN:" + component_name + "\n" component_str += property_name + ";TZID=America/Denver:" component_str += "20120404T073000\nEND:" + component_name component = Component.from_ical(component_str) assert tzid_from_dt(component[property_name].dt) == "America/Denver" @comp_prop def test_cal_Component_from_ical_2(component_name, property_name, tzp): """Check for proper handling of TZID parameter of datetime properties""" component_str = "BEGIN:" + component_name + "\n" component_str += property_name + ":" component_str += "20120404T073000\nEND:" + component_name component = Component.from_ical(component_str) assert component[property_name].dt.tzinfo is None def test_cal_Component_to_ical_property_order(): component_str = [ b"BEGIN:VEVENT", b"DTSTART:19970714T170000Z", b"DTEND:19970715T035959Z", b"SUMMARY:Bastille Day Party", b"END:VEVENT", ] component = Component.from_ical(b"\r\n".join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str != component_str assert set(sorted_str) == set(component_str) preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str def test_cal_Component_to_ical_parameter_order(): component_str = [ b"BEGIN:VEVENT", b"X-FOOBAR;C=one;A=two;B=three:helloworld.", b"END:VEVENT", ] component = Component.from_ical(b"\r\n".join(component_str)) sorted_str = component.to_ical().splitlines() assert sorted_str[0] == component_str[0] assert sorted_str[1] == b"X-FOOBAR;A=two;B=three;C=one:helloworld." assert sorted_str[2] == component_str[2] preserved_str = component.to_ical(sorted=False).splitlines() assert preserved_str == component_str @pytest.fixture def repr_example(c): class ReprExample: component = c component["key1"] = "value1" calendar = Calendar() calendar["key1"] = "value1" event = Event() event["key1"] = "value1" nested = Component(key1="VALUE1") nested.add_component(component) nested.add_component(calendar) return ReprExample def test_repr_component(repr_example): """Test correct class representation.""" assert re.match(r"Component\({u?'KEY1': u?'value1'}\)", str(repr_example.component)) def test_repr_calendar(repr_example): assert re.match(r"VCALENDAR\({u?'KEY1': u?'value1'}\)", str(repr_example.calendar)) def test_repr_event(repr_example): assert re.match(r"VEVENT\({u?'KEY1': u?'value1'}\)", str(repr_example.event)) def test_nested_components_2(repr_example): """Representation of nested Components""" repr_example.calendar.add_component(repr_example.event) print(repr_example.nested) assert re.match( r"Component\({u?'KEY1': u?'VALUE1'}, " r"Component\({u?'KEY1': u?'value1'}\), " r"VCALENDAR\({u?'KEY1': u?'value1'}, " r"VEVENT\({u?'KEY1': u?'value1'}\)\)\)", str(repr_example.nested), ) def test_component_factory_VEVENT(factory): """Check the events in the component factory""" component = factory["VEVENT"] event = component(dtstart="19700101") assert event.to_ical() == b"BEGIN:VEVENT\r\nDTSTART:19700101\r\nEND:VEVENT\r\n" def test_component_factory_VCALENDAR(factory): """Check the VCALENDAR in the factory.""" assert factory.get("VCALENDAR") == icalendar.cal.Calendar def test_minimal_calendar_component_with_one_event(): """Setting up a minimal calendar component looks like this""" cal = Calendar() # Some properties are required to be compliant cal["prodid"] = "-//My calendar product//mxm.dk//" cal["version"] = "2.0" # We also need at least one subcomponent for a calendar to be compliant event = Event() event["summary"] = "Python meeting about calendaring" event["uid"] = "42" event.add("dtstart", datetime(2005, 4, 4, 8, 0, 0)) cal.add_component(event) assert cal.subcomponents[0].to_ical() == ( b"BEGIN:VEVENT\r\nSUMMARY:Python meeting about calendaring\r\n" b"DTSTART:20050404T080000\r\nUID:42\r\n" b"END:VEVENT\r\n" ) def test_calendar_journals_property(calendars): """Calendar.journals returns all VJOURNAL components.""" journals = calendars.rfc_7986_image.journals assert len(journals) == 2 assert all(isinstance(j, Journal) for j in journals) def test_calendar_with_parsing_errors_includes_all_events(calendars): """Parsing a complete calendar from a string will silently ignore wrong events but adding the error information to the component's 'errors' attribute. The error in the following is the third EXDATE: it has an empty DATE. """ event_descriptions = [ e["DESCRIPTION"].to_ical() for e in calendars.parsing_error.walk("VEVENT") ] assert event_descriptions == [b"Perfectly OK event", b"Wrong event"] def test_calendar_with_parsing_errors_has_an_error_in_one_event(calendars): """Parsing a complete calendar from a string will silently ignore wrong events but adding the error information to the component's 'errors' attribute. The error in the following is the third EXDATE: it has an empty DATE. """ errors = [e.errors for e in calendars.parsing_error.walk("VEVENT")] assert errors == [[], [("EXDATE", "Expected datetime, date, or time. Got: ''")]] def test_cal_strict_parsing(calendars): """If components are damaged, we raise an exception.""" with pytest.raises(ValueError): calendars.parsing_error_in_UTC_offset.subcomponents def test_cal_ignore_errors_parsing(calendars, vUTCOffset_ignore_exceptions): """If we diable the errors, we should be able to put the calendar back together.""" assert ( calendars.parsing_error_in_UTC_offset.to_ical() == calendars.parsing_error_in_UTC_offset.raw_ics ) @pytest.mark.parametrize( ("calendar", "other_calendar"), itertools.product( [ "issue_156_RDATE_with_PERIOD_TZID_khal", "issue_156_RDATE_with_PERIOD_TZID_khal_2", "issue_178_custom_component_contains_other", "issue_178_custom_component_inside_other", "issue_526_calendar_with_events", "issue_526_calendar_with_different_events", "issue_526_calendar_with_event_subset", ], repeat=2, ), ) def test_comparing_calendars(calendars, calendar, other_calendar, tzp): are_calendars_equal = calendars[calendar] == calendars[other_calendar] are_calendars_actually_equal = calendar == other_calendar assert are_calendars_equal == are_calendars_actually_equal @pytest.mark.parametrize( ("calendar", "shuffeled_calendar"), [ ( "issue_526_calendar_with_events", "issue_526_calendar_with_shuffeled_events", ), ], ) def test_calendars_with_same_subcomponents_in_different_order_are_equal( calendars, calendar, shuffeled_calendar ): assert ( calendars[calendar].subcomponents != calendars[shuffeled_calendar].subcomponents ) assert calendars[calendar] == calendars[shuffeled_calendar] collective-icalendar-138c845/src/icalendar/tests/test_unit_caselessdict.py000066400000000000000000000114171524134033600270420ustar00rootroot00000000000000import unittest import icalendar class TestCaselessdict(unittest.TestCase): def test_caselessdict_canonsort_keys(self) -> None: canonsort_keys = icalendar.caselessdict.canonsort_keys keys = ["DTEND", "DTSTAMP", "DTSTART", "UID", "SUMMARY", "LOCATION"] out = canonsort_keys(keys) assert out == ["DTEND", "DTSTAMP", "DTSTART", "LOCATION", "SUMMARY", "UID"] out = canonsort_keys( keys, ( "SUMMARY", "DTSTART", "DTEND", ), ) assert out == ["SUMMARY", "DTSTART", "DTEND", "DTSTAMP", "LOCATION", "UID"] out = canonsort_keys( keys, ( "UID", "DTSTART", "DTEND", ), ) assert out == ["UID", "DTSTART", "DTEND", "DTSTAMP", "LOCATION", "SUMMARY"] out = canonsort_keys(keys, ("UID", "DTSTART", "DTEND", "RRULE", "EXDATE")) assert out == ["UID", "DTSTART", "DTEND", "DTSTAMP", "LOCATION", "SUMMARY"] def test_caselessdict_canonsort_items(self) -> None: canonsort_items = icalendar.caselessdict.canonsort_items d = { "i": 7, "c": "at", "a": 3.5, "l": (2, 3), "e": [4, 5], "n": 13, "d": {"x": "y"}, "r": 1.0, } out = canonsort_items(d) assert out == [ ("a", 3.5), ("c", "at"), ("d", {"x": "y"}), ("e", [4, 5]), ("i", 7), ("l", (2, 3)), ("n", 13), ("r", 1.0), ] out = canonsort_items(d, ("i", "c", "a")) assert out, [ ("i", 7), ("c", "at"), ("a", 3.5), ("d", {"x": "y"}), ("e", [4, 5]), ("l", (2, 3)), ("n", 13), ("r", 1.0), ] def test_caselessdict_copy(self) -> None: CaselessDict = icalendar.caselessdict.CaselessDict original_dict = CaselessDict(key1="val1", key2="val2") copied_dict = original_dict.copy() assert original_dict == copied_dict def test_caselessdict_popitem_returns_last_item(self) -> None: CaselessDict = icalendar.caselessdict.CaselessDict dictionary = CaselessDict(first=1, second=2) assert dictionary.popitem() == ("SECOND", 2) assert dictionary == {"FIRST": 1} def test_caselessdict_repr_includes_class_name_and_items(self) -> None: CaselessDict = icalendar.caselessdict.CaselessDict assert repr(CaselessDict(summary="Meeting")) == ( "CaselessDict({'SUMMARY': 'Meeting'})" ) def test_CaselessDict(self) -> None: CaselessDict = icalendar.caselessdict.CaselessDict ncd = CaselessDict(key1="val1", key2="val2") assert ncd == CaselessDict({"KEY2": "val2", "KEY1": "val1"}) assert ncd["key1"] == "val1" assert ncd["KEY1"] == "val1" assert ncd.has_key("key1") ncd["KEY3"] = "val3" assert ncd["key3"] == "val3" assert ncd.setdefault("key3", "FOUND") == "val3" assert ncd.setdefault("key4", "NOT FOUND") == "NOT FOUND" assert ncd["key4"] == "NOT FOUND" assert ncd.get("key1") == "val1" assert ncd.get("key3", "NOT FOUND") == "val3" assert ncd.get("key4", "NOT FOUND") == "NOT FOUND" assert "key4" in ncd del ncd["key4"] assert "key4" not in ncd ncd.update({"key5": "val5", "KEY6": "val6", "KEY5": "val7"}) assert ncd["key6"] == "val6" keys = sorted(ncd.keys()) assert keys == ["KEY1", "KEY2", "KEY3", "KEY5", "KEY6"] def test_eq_with_non_dict_types(self) -> None: """Test that CaselessDict.__eq__ handles non-dict comparisons correctly.""" CaselessDict = icalendar.caselessdict.CaselessDict d = CaselessDict() d["test"] = 1 # Should return False for non-dict types, not crash assert d != "string" assert d != 123 assert d is not None assert d != [] assert d != () assert d != set() # Should still work correctly with dicts assert d == {"TEST": 1} assert d != {"TEST": 2} assert d != {"OTHER": 1} def test_ne_with_non_dict_types(self) -> None: """Test that CaselessDict.__ne__ handles non-dict comparisons correctly.""" CaselessDict = icalendar.caselessdict.CaselessDict d = CaselessDict() d["test"] = 1 # Should return True for non-dict types assert d != "string" assert d != 123 assert d is not None assert d != [] # Should still work correctly with dicts assert d == {"TEST": 1} assert d != {"TEST": 2} collective-icalendar-138c845/src/icalendar/tests/test_unit_parser_tools.py000066400000000000000000000022771524134033600271140ustar00rootroot00000000000000import unittest from icalendar.parser_tools import data_encode, from_unicode, to_unicode class TestParserTools(unittest.TestCase): def test_parser_tools_to_unicode(self): assert to_unicode(b"spam") == "spam" assert to_unicode("spam") == "spam" assert to_unicode(b"spam") == "spam" assert to_unicode(b"\xc6\xb5") == "Ƶ" assert to_unicode(b"\xc6\xb5") == "Ƶ" assert to_unicode(b"\xc6\xb5", encoding="ascii") == "Ƶ" assert to_unicode(1) == 1 assert to_unicode(None) is None def test_parser_tools_from_unicode(self): assert from_unicode("Ƶ", encoding="ascii") == b"\xc6\xb5" assert from_unicode(b"spam") == b"spam" assert from_unicode(1) == 1 assert from_unicode(None) is None def test_parser_tools_data_encode(self): data1 = { "k1": "v1", "k2": "v2", "k3": "v3", "li1": ["it1", "it2", {"k4": "v4", "k5": "v5"}, 123], } res = { b"k3": b"v3", b"k2": b"v2", b"k1": b"v1", b"li1": [b"it1", b"it2", {b"k5": b"v5", b"k4": b"v4"}, 123], } assert data_encode(data1) == res collective-icalendar-138c845/src/icalendar/tests/test_windows_to_olson_version.py000066400000000000000000000007571524134033600305150ustar00rootroot00000000000000import re from icalendar.timezone import windows_to_olson def test_windows_to_olson_records_cldr_source_version(): """The generated mapping identifies the exact CLDR source commit.""" assert re.fullmatch(r"[0-9a-f]{40}", windows_to_olson.version) assert re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", windows_to_olson.version_date) documentation = windows_to_olson.__doc__ assert documentation is not None assert f"cldr/blob/{windows_to_olson.version}/" in documentation collective-icalendar-138c845/src/icalendar/tests/test_with_doctest.py000066400000000000000000000052401524134033600260320ustar00rootroot00000000000000"""This file tests the source code provided by the documentation. See - doctest documentation: https://docs.python.org/3/library/doctest.html - :issue:`443` This file should be tests, too: >>> print("Hello World!") Hello World! """ import doctest import importlib import sys from pathlib import Path import pytest HERE = Path(__file__ or ".").absolute().parent ICALENDAR_PATH = HERE / ".." PYTHON_FILES = [ file for file in ICALENDAR_PATH.glob("**/*.py") if "fuzzing" not in file.relative_to(ICALENDAR_PATH).parts ] MODULE_NAMES = [ "icalendar" + str(python_file)[len(str(ICALENDAR_PATH)) : -3] .replace("\\", "/") .replace("/", ".") for python_file in PYTHON_FILES ] def test_this_module_is_among_them() -> None: assert __name__ in MODULE_NAMES @pytest.mark.parametrize("module_name", MODULE_NAMES) def test_docstring_of_python_file(module_name: str, env_for_doctest) -> None: """This test runs doctest on the Python module.""" try: module = importlib.import_module(module_name) except ModuleNotFoundError as e: if e.name == "pytz": pytest.skip("pytz is not installed, skipping this module.") raise test_result = doctest.testmod(module, name=module_name, globs=env_for_doctest) assert test_result.failed == 0, f"{test_result.failed} errors in {module_name}" # This collection includes only the root and docs subdirectory REPOSITORY = HERE.parent.parent.parent DOCS_PATHS = list(REPOSITORY.glob("docs/**/*.rst")) ROOT_DOCS_PATHS = list(REPOSITORY.glob("*.rst")) DOCUMENT_PATHS = DOCS_PATHS + ROOT_DOCS_PATHS @pytest.mark.parametrize( "filename", [ "README.rst", "index.rst", ], ) def test_files_is_included(filename: str) -> None: assert any(path.name == filename for path in DOCUMENT_PATHS) @pytest.mark.parametrize("document", DOCUMENT_PATHS) def test_documentation_file(document, zoneinfo_only, env_for_doctest, tzp): """This test runs doctest on a documentation file. functions are also replaced to work. """ try: import pytz # noqa: F401 except ImportError: pytest.skip("pytz not installed, skipping this file.") try: # set raise_on_error to False if you wand to see the error for debug test_result = doctest.testfile( document, module_relative=False, globs=env_for_doctest, raise_on_error=False ) finally: tzp.use_zoneinfo() assert test_result.failed == 0, ( f"{test_result.failed} errors in {Path(document).name}" ) def test_can_import_zoneinfo(env_for_doctest) -> None: """Allow importing zoneinfo for tests.""" assert "zoneinfo" in sys.modules collective-icalendar-138c845/src/icalendar/tests/timezone_ids.py000066400000000000000000000317741524134033600247770ustar00rootroot00000000000000"""This file contains all the timezone ids that should be tested.""" TZIDS = [ "Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", "Africa/Algiers", "Africa/Asmara", "Africa/Asmera", "Africa/Bamako", "Africa/Bangui", "Africa/Banjul", "Africa/Bissau", "Africa/Blantyre", "Africa/Brazzaville", "Africa/Bujumbura", "Africa/Cairo", "Africa/Casablanca", "Africa/Ceuta", "Africa/Conakry", "Africa/Dakar", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Douala", "Africa/El_Aaiun", "Africa/Freetown", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Juba", "Africa/Kampala", "Africa/Khartoum", "Africa/Kigali", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Lome", "Africa/Luanda", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Malabo", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Mogadishu", "Africa/Monrovia", "Africa/Nairobi", "Africa/Ndjamena", "Africa/Niamey", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Porto-Novo", "Africa/Sao_Tome", "Africa/Timbuktu", "Africa/Tripoli", "Africa/Tunis", "Africa/Windhoek", "America/Adak", "America/Anchorage", "America/Anguilla", "America/Antigua", "America/Araguaina", "America/Argentina/Buenos_Aires", "America/Argentina/Catamarca", "America/Argentina/ComodRivadavia", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La_Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio_Gallegos", "America/Argentina/Salta", "America/Argentina/San_Juan", "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Aruba", "America/Asuncion", "America/Atikokan", "America/Atka", "America/Bahia", "America/Bahia_Banderas", "America/Barbados", "America/Belem", "America/Belize", "America/Blanc-Sablon", "America/Boa_Vista", "America/Bogota", "America/Boise", "America/Buenos_Aires", "America/Cambridge_Bay", "America/Campo_Grande", "America/Cancun", "America/Caracas", "America/Catamarca", "America/Cayenne", "America/Cayman", "America/Chicago", "America/Chihuahua", "America/Ciudad_Juarez", "America/Coral_Harbour", "America/Cordoba", "America/Costa_Rica", "America/Creston", "America/Cuiaba", "America/Curacao", "America/Danmarkshavn", "America/Dawson", "America/Dawson_Creek", "America/Denver", "America/Detroit", "America/Dominica", "America/Edmonton", "America/Eirunepe", "America/El_Salvador", "America/Ensenada", "America/Fortaleza", "America/Fort_Nelson", "America/Fort_Wayne", "America/Glace_Bay", "America/Godthab", "America/Goose_Bay", "America/Grand_Turk", "America/Grenada", "America/Guadeloupe", "America/Guatemala", "America/Guayaquil", "America/Guyana", "America/Halifax", "America/Havana", "America/Hermosillo", "America/Indiana/Indianapolis", "America/Indiana/Knox", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indianapolis", "America/Indiana/Tell_City", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Inuvik", "America/Iqaluit", "America/Jamaica", "America/Jujuy", "America/Juneau", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Knox_IN", "America/Kralendijk", "America/La_Paz", "America/Lima", "America/Los_Angeles", "America/Louisville", "America/Lower_Princes", "America/Maceio", "America/Managua", "America/Manaus", "America/Marigot", "America/Martinique", "America/Matamoros", "America/Mazatlan", "America/Mendoza", "America/Menominee", "America/Metlakatla", "America/Mexico_City", "America/Miquelon", "America/Moncton", "America/Monterrey", "America/Montevideo", "America/Montreal", "America/Montserrat", "America/Nassau", "America/New_York", "America/Nipigon", "America/Nome", "America/Noronha", "America/North_Dakota/Beulah", "America/North_Dakota/Center", "America/North_Dakota/New_Salem", "America/Nuuk", "America/Ojinaga", "America/Panama", "America/Pangnirtung", "America/Paramaribo", "America/Phoenix", "America/Port-au-Prince", "America/Porto_Acre", "America/Port_of_Spain", "America/Porto_Velho", "America/Puerto_Rico", "America/Punta_Arenas", "America/Rainy_River", "America/Rankin_Inlet", "America/Recife", "America/Regina", "America/Resolute", "America/Rio_Branco", "America/Rosario", "America/Santa_Isabel", "America/Santarem", "America/Santiago", "America/Santo_Domingo", "America/Sao_Paulo", "America/Scoresbysund", "America/Shiprock", "America/Sitka", "America/St_Barthelemy", "America/St_Johns", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Swift_Current", "America/Tegucigalpa", "America/Thule", "America/Thunder_Bay", "America/Tijuana", "America/Toronto", "America/Tortola", "America/Vancouver", "America/Virgin", "America/Whitehorse", "America/Winnipeg", "America/Yakutat", "America/Yellowknife", "Antarctica/Casey", "Antarctica/Davis", "Antarctica/DumontDUrville", "Antarctica/Macquarie", "Antarctica/Mawson", "Antarctica/McMurdo", "Antarctica/Palmer", "Antarctica/Rothera", "Antarctica/South_Pole", "Antarctica/Syowa", "Antarctica/Troll", "Antarctica/Vostok", "Arctic/Longyearbyen", "Asia/Aden", "Asia/Almaty", "Asia/Amman", "Asia/Anadyr", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Ashkhabad", "Asia/Atyrau", "Asia/Baghdad", "Asia/Bahrain", "Asia/Baku", "Asia/Bangkok", "Asia/Barnaul", "Asia/Beirut", "Asia/Bishkek", "Asia/Brunei", "Asia/Calcutta", "Asia/Chita", "Asia/Choibalsan", "Asia/Chongqing", "Asia/Chungking", "Asia/Colombo", "Asia/Dacca", "Asia/Damascus", "Asia/Dhaka", "Asia/Dili", "Asia/Dubai", "Asia/Dushanbe", "Asia/Famagusta", "Asia/Gaza", "Asia/Harbin", "Asia/Hebron", "Asia/Ho_Chi_Minh", "Asia/Hong_Kong", "Asia/Hovd", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jayapura", "Asia/Jerusalem", "Asia/Kabul", "Asia/Kamchatka", "Asia/Karachi", "Asia/Kashgar", "Asia/Kathmandu", "Asia/Katmandu", "Asia/Khandyga", "Asia/Kolkata", "Asia/Krasnoyarsk", "Asia/Kuala_Lumpur", "Asia/Kuching", "Asia/Kuwait", "Asia/Macao", "Asia/Macau", "Asia/Magadan", "Asia/Makassar", "Asia/Manila", "Asia/Muscat", "Asia/Nicosia", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Omsk", "Asia/Oral", "Asia/Phnom_Penh", "Asia/Pontianak", "Asia/Pyongyang", "Asia/Qatar", "Asia/Qostanay", "Asia/Qyzylorda", "Asia/Rangoon", "Asia/Riyadh", "Asia/Saigon", "Asia/Sakhalin", "Asia/Samarkand", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Srednekolymsk", "Asia/Taipei", "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tel_Aviv", "Asia/Thimbu", "Asia/Thimphu", "Asia/Tokyo", "Asia/Tomsk", "Asia/Ujung_Pandang", "Asia/Ulaanbaatar", "Asia/Ulan_Bator", "Asia/Urumqi", "Asia/Ust-Nera", "Asia/Vientiane", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yangon", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Bermuda", "Atlantic/Canary", "Atlantic/Cape_Verde", "Atlantic/Faeroe", "Atlantic/Faroe", "Atlantic/Jan_Mayen", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/South_Georgia", "Atlantic/Stanley", "Atlantic/St_Helena", "Australia/ACT", "Australia/Adelaide", "Australia/Brisbane", "Australia/Broken_Hill", "Australia/Canberra", "Australia/Currie", "Australia/Darwin", "Australia/Eucla", "Australia/Hobart", "Australia/LHI", "Australia/Lindeman", "Australia/Lord_Howe", "Australia/Melbourne", "Australia/North", "Australia/NSW", "Australia/Perth", "Australia/Queensland", "Australia/South", "Australia/Sydney", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Australia/Yancowinna", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "Canada/Atlantic", "Canada/Central", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "CET", "Chile/Continental", "Chile/EasterIsland", "CST6CDT", "Cuba", "EET", "Egypt", "Eire", "EST", "EST5EDT", "Etc/GMT", "Etc/GMT+0", "Etc/GMT-0", "Etc/GMT0", "Etc/GMT+1", "Etc/GMT-1", "Etc/GMT+10", "Etc/GMT-10", "Etc/GMT+11", "Etc/GMT-11", "Etc/GMT+12", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT+2", "Etc/GMT-2", "Etc/GMT+3", "Etc/GMT-3", "Etc/GMT+4", "Etc/GMT-4", "Etc/GMT+5", "Etc/GMT-5", "Etc/GMT+6", "Etc/GMT-6", "Etc/GMT+7", "Etc/GMT-7", "Etc/GMT+8", "Etc/GMT-8", "Etc/GMT+9", "Etc/GMT-9", "Etc/Greenwich", "Etc/UCT", "Etc/Universal", "Etc/UTC", "Etc/Zulu", "Europe/Amsterdam", "Europe/Andorra", "Europe/Astrakhan", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", "Europe/Busingen", "Europe/Chisinau", "Europe/Copenhagen", "Europe/Dublin", "Europe/Gibraltar", "Europe/Guernsey", "Europe/Helsinki", "Europe/Isle_of_Man", "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Kirov", "Europe/Kyiv", "Europe/Lisbon", "Europe/Ljubljana", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Mariehamn", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Nicosia", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Riga", "Europe/Rome", "Europe/Samara", "Europe/San_Marino", "Europe/Sarajevo", "Europe/Saratov", "Europe/Simferopol", "Europe/Skopje", "Europe/Sofia", "Europe/Stockholm", "Europe/Tallinn", "Europe/Tirane", "Europe/Tiraspol", "Europe/Ulyanovsk", "Europe/Uzhgorod", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Vilnius", "Europe/Volgograd", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zaporozhye", "Europe/Zurich", "GB", "GB-Eire", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "Hongkong", "HST", "Iceland", "Indian/Antananarivo", "Indian/Chagos", "Indian/Christmas", "Indian/Cocos", "Indian/Comoro", "Indian/Kerguelen", "Indian/Mahe", "Indian/Maldives", "Indian/Mauritius", "Indian/Mayotte", "Indian/Reunion", "Iran", "Asia/Tehran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "MST", "MST7MDT", "Navajo", "NZ", "NZ-CHAT", "Pacific/Chatham", "Pacific/Apia", "Pacific/Auckland", "Pacific/Bougainville", "Pacific/Chuuk", "Pacific/Easter", "Pacific/Efate", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Galapagos", "Pacific/Gambier", "Pacific/Guadalcanal", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Kanton", "Pacific/Kiritimati", "Pacific/Kosrae", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Marquesas", "Pacific/Midway", "Pacific/Nauru", "Pacific/Niue", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pago_Pago", "Pacific/Palau", "Pacific/Pitcairn", "Pacific/Pohnpei", "Pacific/Ponape", "Pacific/Port_Moresby", "Pacific/Rarotonga", "Pacific/Saipan", "Pacific/Samoa", "Pacific/Tahiti", "Pacific/Tarawa", "Pacific/Tongatapu", "Pacific/Truk", "Pacific/Wake", "Pacific/Wallis", "Pacific/Yap", "Poland", "Portugal", "PRC", "PST8PDT", "ROC", "ROK", "Singapore", "Turkey", "UCT", "Universal", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/Eastern", "US/East-Indiana", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa", "UTC", "WET", "W-SU", "Zulu", ] collective-icalendar-138c845/src/icalendar/tests/timezones/000077500000000000000000000000001524134033600237355ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/timezones/issue_237_brazilia_standard.ics000066400000000000000000000006201524134033600317130ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:(UTC-03:00) Brasília BEGIN:STANDARD TZNAME:Brasília standard DTSTART:16010101T235959 TZOFFSETFROM:-0200 TZOFFSETTO:-0300 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 END:STANDARD BEGIN:DAYLIGHT TZNAME:Brasília daylight DTSTART:16010101T235959 TZOFFSETFROM:-0300 TZOFFSETTO:-0200 RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 END:DAYLIGHT END:VTIMEZONE collective-icalendar-138c845/src/icalendar/tests/timezones/issue_53_tzid_parsed_properly.ics000066400000000000000000000006621524134033600324240ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:America/New_York TZURL:http://tzurl.org/zoneinfo-outlook/America/New_York X-LIC-LOCATION:America/New_York BEGIN:DAYLIGHT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:EDT DTSTART:19700308T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:EST DTSTART:19701101T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU END:STANDARD END:VTIMEZONE issue_55_parse_error_on_utc_offset_with_seconds.ics000066400000000000000000000002741524134033600361130ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/timezonesBEGIN:VTIMEZONE TZID:America/Los Angeles BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE collective-icalendar-138c845/src/icalendar/tests/timezones/pacific_fiji.ics000066400000000000000000000016411524134033600270360ustar00rootroot00000000000000BEGIN:VTIMEZONE TZID:custom_Pacific/Fiji TZURL:http://tzurl.org/zoneinfo/Pacific/Fiji X-LIC-LOCATION:Pacific/Fiji BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:20101024T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=21,22,23,24,25,26,27;BYDAY=SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:20140119T020000 RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=18,19,20,21,22,23,24;BYDAY=SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+115544 TZOFFSETTO:+1200 DTSTART:19151026T000000 RDATE:19151026T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+1200 TZOFFSETTO:+1300 DTSTART:19981101T020000 RDATE:19981101T020000 RDATE:19991107T020000 RDATE:20091129T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+1300 TZOFFSETTO:+1200 DTSTART:19990228T030000 RDATE:19990228T030000 RDATE:20000227T030000 RDATE:20100328T030000 RDATE:20110306T030000 RDATE:20120122T030000 RDATE:20130120T030000 END:STANDARD END:VTIMEZONE collective-icalendar-138c845/src/icalendar/tests/todos/000077500000000000000000000000001524134033600230505ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/tests/todos/example.ics000066400000000000000000000003401524134033600252000ustar00rootroot00000000000000BEGIN:VTODO UID:20070313T123432Z-456553@example.com DTSTAMP:20070313T123432Z DUE;VALUE=DATE:20070501 SUMMARY:Submit Quebec Income Tax Return for 2006 CLASS:CONFIDENTIAL CATEGORIES:FAMILY,FINANCE STATUS:NEEDS-ACTION END:VTODOcollective-icalendar-138c845/src/icalendar/timezone/000077500000000000000000000000001524134033600224105ustar00rootroot00000000000000collective-icalendar-138c845/src/icalendar/timezone/__init__.py000066400000000000000000000010731524134033600245220ustar00rootroot00000000000000"""This package contains all functionality for timezones.""" from .tzid import is_utc, tzid_from_dt, tzid_from_tzinfo, tzids_from_tzinfo from .tzp import TZP tzp = TZP() def use_pytz(): """Use pytz as the implementation that looks up and creates timezones.""" tzp.use_pytz() def use_zoneinfo(): """Use zoneinfo as the implementation that looks up and creates timezones.""" tzp.use_zoneinfo() __all__ = [ "TZP", "is_utc", "tzid_from_dt", "tzid_from_tzinfo", "tzids_from_tzinfo", "tzp", "use_pytz", "use_zoneinfo", ] collective-icalendar-138c845/src/icalendar/timezone/equivalent_timezone_ids.py000066400000000000000000000105551524134033600277160ustar00rootroot00000000000000"""This module helps identifying the timezone ids and where they differ. The algorithm: We use the tzname and the utcoffset for each hour from 1970 - 2030. We make a big map. If they are equivalent, they are equivalent within the time that is mostly used. You can regenerate the information from this module. See also: - https://stackoverflow.com/questions/79171631/how-do-i-determine-whether-a-zoneinfo-is-an-alias/79171734#79171734 Run this module: python -m icalendar.timezone.equivalent_timezone_ids """ from __future__ import annotations from collections import defaultdict from datetime import datetime, timedelta, tzinfo from pathlib import Path from pprint import pprint from typing import TYPE_CHECKING, NamedTuple from zoneinfo import ZoneInfo, available_timezones from pytz import AmbiguousTimeError, NonExistentTimeError if TYPE_CHECKING: from collections.abc import Callable START = datetime(1970, 1, 1) # noqa: DTZ001 END = datetime(2020, 1, 1) # noqa: DTZ001 DISTANCE_FROM_TIMEZONE_CHANGE = timedelta(hours=12) DTS = [] dt = START while dt <= END: DTS.append(dt) # This must be big enough to be fast and small enough to identify the timeszones # before it is the present year dt += timedelta(hours=25) del dt def main( create_timezones: list[Callable[[str], tzinfo]], name: str, ): """Generate a lookup table for timezone information if unknown timezones. We cannot create one lookup for all because they seem to be all equivalent if we mix timezone implementations. """ unsorted_tzids = available_timezones() unsorted_tzids.remove("localtime") unsorted_tzids.remove("Factory") class TZ(NamedTuple): tz: tzinfo id: str tzs = [ TZ(create_timezone(tzid), tzid) for create_timezone in create_timezones for tzid in unsorted_tzids ] def generate_tree( tzs: list[TZ], step: timedelta = timedelta(hours=1), start: datetime = START, end: datetime = END, todo: set[str] | None = None, ) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive """Generate a lookup tree.""" if todo is None: todo = [tz.id for tz in tzs] if len(tzs) == 0: raise ValueError("tzs cannot be empty") if len(tzs) == 1: todo.remove(tzs[0].id) return {tzs[0].id} while start < end: offsets: dict[timedelta, list[TZ]] = defaultdict(list) try: # if we are around a timezone change, we must move on # see https://github.com/collective/icalendar/issues/776 around_tz_change = not all( tz.tz.utcoffset(start) == tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE) == tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE) for tz in tzs ) except (NonExistentTimeError, AmbiguousTimeError): around_tz_change = True if around_tz_change: start += DISTANCE_FROM_TIMEZONE_CHANGE continue for tz in tzs: offsets[tz.tz.utcoffset(start)].append(tz) if len(offsets) == 1: start += step continue lookup = {} for offset, tz2 in offsets.items(): lookup[offset] = generate_tree( tzs=tz2, step=step, start=start + step, end=end, todo=todo ) return start, lookup result = set() for tz in tzs: result.add(tz.id) todo.remove(tz.id) return result lookup = generate_tree(tzs, step=timedelta(hours=33)) file = Path(__file__).parent / f"equivalent_timezone_ids_{name}.py" with file.open("w") as f: f.write( f"'''This file is automatically generated by {Path(__file__).name}'''\n" ) f.write("import datetime\n\n") f.write("\nlookup = ") pprint(lookup, stream=f) f.write("\n\n__all__ = ['lookup']\n") return lookup __all__ = ["main"] if __name__ == "__main__": from zoneinfo import ZoneInfo from dateutil.tz import gettz from pytz import timezone # add more timezone implementations if you like main( [ZoneInfo, timezone, gettz], "result", ) collective-icalendar-138c845/src/icalendar/timezone/equivalent_timezone_ids_result.py000066400000000000000000006544111524134033600313210ustar00rootroot00000000000000"""This file is automatically generated by equivalent_timezone_ids.py""" import datetime lookup = ( datetime.datetime(1970, 1, 2, 0, 0), { datetime.timedelta(days=-1, seconds=43200): ( datetime.datetime(1979, 10, 2, 0, 0), { datetime.timedelta(days=-1, seconds=43200): ( datetime.datetime(1993, 8, 22, 21, 0), { datetime.timedelta(days=-1, seconds=43200): {"Etc/GMT+12"}, datetime.timedelta(seconds=43200): { "Kwajalein", "Pacific/Kwajalein", }, }, ), datetime.timedelta(days=-1, seconds=46800): { "Pacific/Enderbury", "Pacific/Kanton", }, }, ), datetime.timedelta(days=-1, seconds=46800): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=46800): ( datetime.datetime(2010, 9, 26, 15, 0), { datetime.timedelta(days=-1, seconds=46800): ( datetime.datetime(2011, 12, 31, 21, 0), { datetime.timedelta(days=-1, seconds=46800): { "Etc/GMT+11", "Pacific/Midway", "Pacific/Niue", "Pacific/Pago_Pago", "Pacific/Samoa", "US/Samoa", }, datetime.timedelta(seconds=46800): {"Pacific/Fakaofo"}, }, ), datetime.timedelta(days=-1, seconds=50400): {"Pacific/Apia"}, }, ), datetime.timedelta(days=-1, seconds=50400): ( datetime.datetime(1983, 10, 30, 21, 0), { datetime.timedelta(days=-1, seconds=50400): { "America/Adak", "America/Atka", "US/Aleutian", }, datetime.timedelta(days=-1, seconds=54000): {"America/Nome"}, }, ), }, ), datetime.timedelta(days=-1, seconds=48000): {"Pacific/Kiritimati"}, datetime.timedelta(days=-1, seconds=48600): {"Pacific/Rarotonga"}, datetime.timedelta(days=-1, seconds=50400): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=50400): { "Etc/GMT+10", "HST", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Tahiti", "US/Hawaii", }, datetime.timedelta(days=-1, seconds=54000): { "America/Anchorage", "US/Alaska", }, }, ), datetime.timedelta(days=-1, seconds=52200): {"Pacific/Marquesas"}, datetime.timedelta(days=-1, seconds=54000): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=54000): ( datetime.datetime(1973, 10, 28, 18, 0), { datetime.timedelta(days=-1, seconds=54000): { "Etc/GMT+9", "Pacific/Gambier", }, datetime.timedelta(days=-1, seconds=57600): {"America/Dawson"}, }, ), datetime.timedelta(days=-1, seconds=57600): {"America/Yakutat"}, }, ), datetime.timedelta(days=-1, seconds=55800): {"Pacific/Pitcairn"}, datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(1972, 4, 30, 21, 0), { datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(1976, 4, 25, 15, 0), { datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(1980, 4, 27, 15, 0), { datetime.timedelta(days=-1, seconds=57600): { "Etc/GMT+8" }, datetime.timedelta(days=-1, seconds=61200): { "America/Whitehorse", "Canada/Yukon", }, }, ), datetime.timedelta(days=-1, seconds=61200): { "America/Ensenada", "America/Santa_Isabel", "America/Tijuana", "Mexico/BajaNorte", }, }, ), datetime.timedelta(days=-1, seconds=61200): {"America/Inuvik"}, }, ), datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1972, 10, 30, 0, 0), { datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(1974, 1, 6, 18, 0), { datetime.timedelta(days=-1, seconds=57600): ( datetime.datetime(2015, 11, 1, 21, 0), { datetime.timedelta(days=-1, seconds=57600): { "America/Vancouver", "Canada/Pacific", }, datetime.timedelta(days=-1, seconds=61200): { "America/Fort_Nelson" }, }, ), datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1980, 4, 28, 0, 0), { datetime.timedelta(days=-1, seconds=57600): { "America/Juneau" }, datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1983, 10, 30, 21, 0), { datetime.timedelta( days=-1, seconds=54000 ): {"America/Sitka"}, datetime.timedelta( days=-1, seconds=57600 ): ( datetime.datetime( 1984, 4, 29, 21, 0 ), { datetime.timedelta( days=-1, seconds=57600 ): {"America/Metlakatla"}, datetime.timedelta( days=-1, seconds=61200 ): { "America/Los_Angeles", "PST8PDT", "US/Pacific", }, }, ), }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=61200): { "America/Dawson_Creek" }, }, ), }, ), datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1972, 4, 30, 21, 0), { datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1996, 4, 7, 15, 0), { datetime.timedelta(days=-1, seconds=61200): { "America/Creston", "America/Phoenix", "Etc/GMT+7", "MST", "US/Arizona", }, datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1999, 4, 4, 15, 0), { datetime.timedelta(days=-1, seconds=61200): { "America/Hermosillo" }, datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(2010, 4, 5, 0, 0), { datetime.timedelta( days=-1, seconds=64800 ): { "America/Mazatlan", "Mexico/BajaSur", }, datetime.timedelta( days=-1, seconds=68400 ): {"America/Bahia_Banderas"}, }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1972, 10, 29, 21, 0), { datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(1999, 11, 1, 0, 0), { datetime.timedelta(days=-1, seconds=61200): { "America/Edmonton", "America/Yellowknife", "Canada/Mountain", }, datetime.timedelta(days=-1, seconds=64800): { "America/Cambridge_Bay" }, }, ), datetime.timedelta(days=-1, seconds=64800): { "America/Swift_Current" }, }, ), }, ), datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1974, 1, 6, 18, 0), { datetime.timedelta(days=-1, seconds=61200): {"America/Boise"}, datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1992, 10, 25, 18, 0), { datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(2003, 10, 26, 15, 0), { datetime.timedelta(days=-1, seconds=61200): ( datetime.datetime(2010, 11, 7, 21, 0), { datetime.timedelta( days=-1, seconds=61200 ): { "America/Denver", "America/Shiprock", "MST7MDT", "Navajo", "US/Mountain", }, datetime.timedelta( days=-1, seconds=64800 ): {"America/North_Dakota/Beulah"}, }, ), datetime.timedelta(days=-1, seconds=64800): { "America/North_Dakota/New_Salem" }, }, ), datetime.timedelta(days=-1, seconds=64800): { "America/North_Dakota/Center" }, }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1970, 3, 29, 15, 0), { datetime.timedelta(days=-1, seconds=61200): { "Chile/EasterIsland", "Pacific/Easter", }, datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1972, 4, 30, 21, 0), { datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1973, 5, 1, 15, 0), { datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1973, 11, 25, 18, 0), { datetime.timedelta( days=-1, seconds=64800 ): ( datetime.datetime( 1973, 12, 5, 21, 0 ), { datetime.timedelta( days=-1, seconds=64800 ): ( datetime.datetime( 1979, 2, 26, 0, 0 ), { datetime.timedelta( days=-1, seconds=64800, ): ( datetime.datetime( 1981, 12, 27, 0, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): ( datetime.datetime( 1987, 5, 4, 0, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): ( datetime.datetime( 1988, 4, 4, 0, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): ( datetime.datetime( 1996, 4, 7, 15, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): { "America/Regina", "Canada/Saskatchewan", "Etc/GMT+6", }, datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1998, 4, 5, 15, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): ( datetime.datetime( 2010, 3, 14, 21, 0, ), { datetime.timedelta( days=-1, seconds=61200, ): { "America/Chihuahua" }, datetime.timedelta( days=-1, seconds=64800, ): { "America/Ciudad_Juarez", "America/Ojinaga", }, }, ), datetime.timedelta( days=-1, seconds=68400, ): { "America/Mexico_City", "Mexico/General", }, }, ), }, ), datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 2010, 3, 14, 21, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): { "America/Monterrey" }, datetime.timedelta( days=-1, seconds=68400, ): { "America/Matamoros" }, }, ), }, ), datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 2006, 5, 8, 0, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): { "America/El_Salvador" }, datetime.timedelta( days=-1, seconds=68400, ): { "America/Tegucigalpa" }, }, ), }, ), datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1982, 11, 2, 18, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): { "America/Merida" }, datetime.timedelta( days=-1, seconds=68400, ): { "America/Cancun" }, }, ), }, ), datetime.timedelta( days=-1, seconds=68400, ): { "America/Costa_Rica" }, }, ), datetime.timedelta( days=-1, seconds=68400 ): {"America/Belize"}, }, ), datetime.timedelta( days=-1, seconds=68400 ): {"America/Guatemala"}, }, ), datetime.timedelta(days=-1, seconds=68400): { "America/Managua" }, }, ), datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(2006, 10, 29, 21, 0), { datetime.timedelta(days=-1, seconds=64800): { "America/Rankin_Inlet" }, datetime.timedelta(days=-1, seconds=68400): { "America/Resolute" }, }, ), }, ), datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1974, 1, 6, 18, 0), { datetime.timedelta(days=-1, seconds=64800): { "America/Rainy_River", "America/Winnipeg", "Canada/Central", }, datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1977, 10, 31, 0, 0), { datetime.timedelta(days=-1, seconds=64800): ( datetime.datetime(1991, 10, 27, 18, 0), { datetime.timedelta( days=-1, seconds=64800 ): ( datetime.datetime( 2000, 10, 29, 15, 0 ), { datetime.timedelta( days=-1, seconds=64800 ): { "America/Chicago", "CST6CDT", "US/Central", }, datetime.timedelta( days=-1, seconds=68400 ): { "America/Kentucky/Monticello" }, }, ), datetime.timedelta( days=-1, seconds=68400 ): { "America/Indiana/Knox", "America/Knox_IN", "US/Indiana-Starke", }, }, ), datetime.timedelta(days=-1, seconds=68400): { "America/Indiana/Petersburg" }, }, ), }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1972, 4, 30, 21, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1973, 4, 29, 21, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1973, 10, 28, 21, 0), { datetime.timedelta(days=-1, seconds=64800): { "America/Menominee" }, datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(1974, 1, 6, 15, 0), { datetime.timedelta( days=-1, seconds=68400 ): ( datetime.datetime( 1979, 4, 29, 21, 0 ), { datetime.timedelta( days=-1, seconds=68400 ): ( datetime.datetime( 1983, 5, 8, 18, 0 ), { datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1985, 11, 2, 15, 0, ), { datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1986, 1, 1, 18, 0, ), { datetime.timedelta( days=-1, seconds=64800, ): { "Pacific/Galapagos" }, datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1992, 5, 4, 0, 0, ), { datetime.timedelta( days=-1, seconds=68400, ): ( datetime.datetime( 1992, 11, 28, 15, 0, ), { datetime.timedelta( days=-1, seconds=68400, ): { "America/Atikokan", "America/Cayman", "America/Coral_Harbour", "America/Panama", "EST", "Etc/GMT+5", }, datetime.timedelta( days=-1, seconds=72000, ): { "America/Guayaquil" }, }, ), datetime.timedelta( days=-1, seconds=72000, ): { "America/Bogota" }, }, ), datetime.timedelta( days=-1, seconds=72000, ): { "America/Lima" }, }, ), datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1993, 10, 17, 15, 0, ), { datetime.timedelta( days=-1, seconds=68400, ): { "America/Porto_Acre", "America/Rio_Branco", "Brazil/Acre", }, datetime.timedelta( days=-1, seconds=72000, ): { "America/Eirunepe" }, }, ), }, ), datetime.timedelta( days=-1, seconds=72000, ): { "America/Port-au-Prince" }, }, ), datetime.timedelta( days=-1, seconds=72000 ): {"America/Grand_Turk"}, }, ), datetime.timedelta( days=-1, seconds=72000 ): {"America/Jamaica", "Jamaica"}, }, ), }, ), datetime.timedelta(days=-1, seconds=72000): { "America/Detroit", "US/Michigan", }, }, ), datetime.timedelta(days=-1, seconds=72000): { "America/Iqaluit", "America/Pangnirtung", }, }, ), datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1971, 4, 25, 15, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(2006, 4, 2, 15, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(2007, 3, 12, 3, 0), { datetime.timedelta(days=-1, seconds=68400): ( datetime.datetime(2007, 11, 4, 15, 0), { datetime.timedelta( days=-1, seconds=64800 ): {"America/Indiana/Tell_City"}, datetime.timedelta( days=-1, seconds=68400 ): {"America/Indiana/Vincennes"}, }, ), datetime.timedelta(days=-1, seconds=72000): { "America/Indiana/Winamac" }, }, ), datetime.timedelta(days=-1, seconds=72000): { "America/Fort_Wayne", "America/Indiana/Indianapolis", "America/Indianapolis", "US/East-Indiana", }, }, ), datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1972, 10, 8, 12, 0), { datetime.timedelta(days=-1, seconds=68400): { "America/Havana", "Cuba", }, datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1973, 4, 29, 15, 0), { datetime.timedelta(days=-1, seconds=68400): { "America/Indiana/Vevay" }, datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1974, 1, 6, 18, 0), { datetime.timedelta( days=-1, seconds=68400 ): ( datetime.datetime( 1974, 4, 28, 15, 0 ), { datetime.timedelta( days=-1, seconds=68400 ): ( datetime.datetime( 1976, 4, 26, 0, 0 ), { datetime.timedelta( days=-1, seconds=68400, ): { "America/Indiana/Marengo" }, datetime.timedelta( days=-1, seconds=72000, ): { "America/Kentucky/Louisville", "America/Louisville", }, }, ), datetime.timedelta( days=-1, seconds=72000 ): { "America/Montreal", "America/Nassau", "America/Nipigon", "America/Thunder_Bay", "America/Toronto", "Canada/Eastern", }, }, ), datetime.timedelta( days=-1, seconds=72000 ): { "America/New_York", "EST5EDT", "US/Eastern", }, }, ), }, ), }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=70200): {"America/Santo_Domingo"}, datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1972, 4, 30, 21, 0), { datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1972, 10, 2, 0, 0), { datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1974, 4, 28, 21, 0), { datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(1977, 6, 12, 18, 0), { datetime.timedelta( days=-1, seconds=72000 ): ( datetime.datetime( 1980, 4, 6, 18, 0 ), { datetime.timedelta( days=-1, seconds=72000 ): ( datetime.datetime( 1980, 5, 2, 0, 0 ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1983, 5, 2, 0, 0 ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1985, 11, 2, 18, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1991, 3, 31, 18, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 2007, 12, 10, 0, 0, ), { datetime.timedelta( days=-1, seconds=70200, ): { "America/Caracas" }, datetime.timedelta( days=-1, seconds=72000, ): { "America/Anguilla", "America/Antigua", "America/Aruba", "America/Blanc-Sablon", "America/Curacao", "America/Dominica", "America/Grenada", "America/Guadeloupe", "America/Kralendijk", "America/La_Paz", "America/Lower_Princes", "America/Marigot", "America/Montserrat", "America/Port_of_Spain", "America/Puerto_Rico", "America/St_Barthelemy", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Tortola", "America/Virgin", "Etc/GMT+4", }, }, ), datetime.timedelta( days=-1, seconds=75600, ): { "America/Thule" }, }, ), datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 1988, 10, 16, 15, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1993, 10, 17, 15, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 1999, 10, 3, 18, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 2008, 6, 24, 15, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Porto_Velho" }, datetime.timedelta( days=-1, seconds=75600, ): { "America/Santarem" }, }, ), datetime.timedelta( days=-1, seconds=75600, ): { "America/Boa_Vista" }, }, ), datetime.timedelta( days=-1, seconds=75600, ): { "America/Manaus", "Brazil/West", }, }, ), datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 2003, 10, 19, 21, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Cuiaba" }, datetime.timedelta( days=-1, seconds=75600, ): { "America/Campo_Grande" }, }, ), }, ), }, ), datetime.timedelta( days=-1, seconds=75600, ): { "Atlantic/Stanley" }, }, ), datetime.timedelta( days=-1, seconds=75600, ): {"America/Miquelon"}, }, ), datetime.timedelta( days=-1, seconds=75600 ): {"America/Martinique"}, }, ), datetime.timedelta( days=-1, seconds=75600 ): {"America/Barbados"}, }, ), datetime.timedelta(days=-1, seconds=75600): { "Atlantic/Bermuda" }, }, ), datetime.timedelta(days=-1, seconds=75600): { "America/Asuncion" }, }, ), datetime.timedelta(days=-1, seconds=75600): { "America/Glace_Bay" }, }, ), datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1973, 4, 30, 0, 0), { datetime.timedelta(days=-1, seconds=72000): {"America/Moncton"}, datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1988, 4, 3, 15, 0), { datetime.timedelta(days=-1, seconds=75600): { "America/Halifax", "Canada/Atlantic", }, datetime.timedelta(days=-1, seconds=79200): { "America/Goose_Bay" }, }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=72900): {"America/Guyana"}, datetime.timedelta(days=-1, seconds=73800): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(days=-1, seconds=73800): {"America/Paramaribo"}, datetime.timedelta(days=-1, seconds=77400): { "America/St_Johns", "Canada/Newfoundland", }, }, ), datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1970, 3, 29, 15, 0), { datetime.timedelta(days=-1, seconds=72000): ( datetime.datetime(2017, 5, 14, 18, 0), { datetime.timedelta(days=-1, seconds=72000): { "America/Santiago", "Chile/Continental", }, datetime.timedelta(days=-1, seconds=75600): { "America/Punta_Arenas" }, }, ), datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1970, 4, 25, 18, 0), { datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1974, 1, 23, 21, 0), { datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1980, 4, 7, 0, 0), { datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1985, 11, 3, 0, 0), { datetime.timedelta( days=-1, seconds=75600 ): {"America/Cayenne", "Etc/GMT+3"}, datetime.timedelta( days=-1, seconds=79200 ): ( datetime.datetime( 1988, 10, 16, 15, 0 ), { datetime.timedelta( days=-1, seconds=75600 ): {"America/Belem"}, datetime.timedelta( days=-1, seconds=79200 ): ( datetime.datetime( 1990, 10, 22, 0, 0 ), { datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 1995, 10, 15, 15, 0, ), { datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 2000, 10, 15, 12, 0, ), { datetime.timedelta( days=-1, seconds=75600, ): { "America/Recife" }, datetime.timedelta( days=-1, seconds=79200, ): { "America/Fortaleza" }, }, ), datetime.timedelta( days=-1, seconds=79200, ): ( datetime.datetime( 1996, 10, 6, 18, 0, ), { datetime.timedelta( days=-1, seconds=75600, ): { "America/Maceio" }, datetime.timedelta( days=-1, seconds=79200, ): { "America/Araguaina" }, }, ), }, ), datetime.timedelta( days=-1, seconds=79200, ): ( datetime.datetime( 2003, 10, 19, 21, 0, ), { datetime.timedelta( days=-1, seconds=75600, ): { "America/Bahia" }, datetime.timedelta( days=-1, seconds=79200, ): { "America/Sao_Paulo", "Brazil/East", }, }, ), }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=79200): ( datetime.datetime(1996, 1, 2, 0, 0), { datetime.timedelta( days=-1, seconds=75600 ): {"America/Godthab", "America/Nuuk"}, datetime.timedelta(0): { "America/Danmarkshavn" }, }, ), }, ), datetime.timedelta(days=-1, seconds=79200): ( datetime.datetime(1982, 5, 1, 21, 0), { datetime.timedelta(days=-1, seconds=72000): { "Antarctica/Palmer" }, datetime.timedelta(days=-1, seconds=75600): ( datetime.datetime(1990, 3, 4, 21, 0), { datetime.timedelta( days=-1, seconds=72000 ): ( datetime.datetime( 1990, 10, 16, 0, 0 ), { datetime.timedelta( days=-1, seconds=72000 ): { "America/Argentina/Jujuy", "America/Jujuy", }, datetime.timedelta( days=-1, seconds=75600 ): { "America/Argentina/Mendoza", "America/Mendoza", }, }, ), datetime.timedelta( days=-1, seconds=75600 ): ( datetime.datetime( 1991, 3, 1, 15, 0 ), { datetime.timedelta( days=-1, seconds=72000 ): ( datetime.datetime( 2004, 6, 20, 18, 0 ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Argentina/San_Juan" }, datetime.timedelta( days=-1, seconds=75600, ): { "America/Argentina/La_Rioja" }, }, ), datetime.timedelta( days=-1, seconds=79200 ): ( datetime.datetime( 1991, 3, 4, 0, 0 ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 2004, 6, 1, 12, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): ( datetime.datetime( 2004, 6, 14, 0, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Argentina/Catamarca", "America/Argentina/ComodRivadavia", "America/Catamarca", }, datetime.timedelta( days=-1, seconds=75600, ): { "America/Argentina/Tucuman" }, }, ), datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 2008, 10, 19, 15, 0, ), { datetime.timedelta( days=-1, seconds=75600, ): { "America/Argentina/Salta" }, datetime.timedelta( days=-1, seconds=79200, ): { "America/Argentina/Cordoba", "America/Cordoba", "America/Rosario", }, }, ), }, ), datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 2004, 5, 30, 15, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Argentina/Ushuaia" }, datetime.timedelta( days=-1, seconds=75600, ): ( datetime.datetime( 2004, 6, 1, 12, 0, ), { datetime.timedelta( days=-1, seconds=72000, ): { "America/Argentina/Rio_Gallegos" }, datetime.timedelta( days=-1, seconds=75600, ): { "America/Argentina/Buenos_Aires", "America/Buenos_Aires", }, }, ), }, ), }, ), }, ), datetime.timedelta( days=-1, seconds=79200 ): {"America/Argentina/San_Luis"}, }, ), }, ), }, ), datetime.timedelta(days=-1, seconds=79200): { "America/Montevideo" }, }, ), }, ), datetime.timedelta(days=-1, seconds=79200): ( datetime.datetime(1975, 11, 25, 18, 0), { datetime.timedelta(days=-1, seconds=79200): ( datetime.datetime(1980, 4, 6, 21, 0), { datetime.timedelta(days=-1, seconds=79200): ( datetime.datetime(1985, 11, 2, 21, 0), { datetime.timedelta(days=-1, seconds=79200): { "Atlantic/South_Georgia", "Etc/GMT+2", }, datetime.timedelta(days=-1, seconds=82800): { "America/Noronha", "Brazil/DeNoronha", }, }, ), datetime.timedelta(days=-1, seconds=82800): { "America/Scoresbysund" }, }, ), datetime.timedelta(days=-1, seconds=82800): {"Atlantic/Cape_Verde"}, }, ), datetime.timedelta(days=-1, seconds=82800): ( datetime.datetime(1975, 1, 1, 15, 0), { datetime.timedelta(days=-1, seconds=82800): ( datetime.datetime(1976, 4, 15, 0, 0), { datetime.timedelta(days=-1, seconds=82800): ( datetime.datetime(1982, 3, 29, 0, 0), { datetime.timedelta(days=-1, seconds=82800): { "Etc/GMT+1" }, datetime.timedelta(0): {"Atlantic/Azores"}, }, ), datetime.timedelta(0): {"Africa/El_Aaiun"}, }, ), datetime.timedelta(0): {"Africa/Bissau"}, }, ), datetime.timedelta(days=-1, seconds=83730): {"Africa/Monrovia"}, datetime.timedelta(days=-1, seconds=83760): {"Africa/Monrovia"}, datetime.timedelta(0): ( datetime.datetime(1971, 4, 26, 12, 0), { datetime.timedelta(0): ( datetime.datetime(1974, 6, 25, 0, 0), { datetime.timedelta(0): ( datetime.datetime(1976, 12, 1, 15, 0), { datetime.timedelta(days=-1, seconds=75600): { "Antarctica/Rothera" }, datetime.timedelta(0): ( datetime.datetime(1977, 4, 4, 0, 0), { datetime.timedelta(0): ( datetime.datetime(1980, 4, 6, 15, 0), { datetime.timedelta(0): ( datetime.datetime( 1981, 3, 29, 18, 0 ), { datetime.timedelta(0): ( datetime.datetime( 1982, 4, 4, 15, 0 ), { datetime.timedelta(0): ( datetime.datetime( 2005, 3, 27, 15, 0, ), { datetime.timedelta( 0 ): ( datetime.datetime( 2018, 1, 1, 18, 0, ), { datetime.timedelta( 0 ): { "Africa/Abidjan", "Africa/Accra", "Africa/Bamako", "Africa/Banjul", "Africa/Conakry", "Africa/Dakar", "Africa/Freetown", "Africa/Lome", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Timbuktu", "Atlantic/Reykjavik", "Atlantic/St_Helena", "Etc/GMT", "Etc/GMT+0", "Etc/GMT-0", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/UTC", "Etc/Universal", "Etc/Zulu", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "Iceland", "UCT", "UTC", "Universal", "Zulu", }, datetime.timedelta( seconds=3600 ): { "Africa/Sao_Tome" }, }, ), datetime.timedelta( seconds=7200 ): { "Antarctica/Troll" }, }, ), datetime.timedelta( seconds=3600 ): {"Atlantic/Madeira"}, }, ), datetime.timedelta( seconds=3600 ): { "Atlantic/Faeroe", "Atlantic/Faroe", }, }, ), datetime.timedelta(seconds=3600): { "Atlantic/Canary" }, }, ), datetime.timedelta(seconds=3600): {"WET"}, }, ), }, ), datetime.timedelta(seconds=3600): ( datetime.datetime(1986, 1, 1, 12, 0), { datetime.timedelta(0): {"Africa/Casablanca"}, datetime.timedelta(seconds=3600): {"Africa/Ceuta"}, }, ), }, ), datetime.timedelta(seconds=3600): {"Africa/Algiers"}, }, ), datetime.timedelta(seconds=3600): ( datetime.datetime(1970, 6, 1, 0, 0), { datetime.timedelta(seconds=3600): ( datetime.datetime(1971, 11, 1, 0, 0), { datetime.timedelta(0): { "Eire", "Europe/Belfast", "Europe/Dublin", "Europe/Guernsey", "Europe/Isle_of_Man", "Europe/Jersey", "Europe/London", "GB", "GB-Eire", }, datetime.timedelta(seconds=3600): ( datetime.datetime(1974, 4, 14, 15, 0), { datetime.timedelta(seconds=3600): ( datetime.datetime(1974, 5, 4, 21, 0), { datetime.timedelta(seconds=3600): ( datetime.datetime(1976, 3, 28, 21, 0), { datetime.timedelta(seconds=3600): ( datetime.datetime( 1976, 9, 26, 21, 0 ), { datetime.timedelta(0): ( datetime.datetime( 1980, 3, 30, 18, 0 ), { datetime.timedelta(0): { "Europe/Lisbon", "Portugal", "WET", }, datetime.timedelta( seconds=3600 ): {"Portugal"}, }, ), datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1977, 4, 3, 18, 0 ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1977, 4, 30, 21, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1979, 4, 1, 18, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1979, 10, 15, 0, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1980, 4, 6, 15, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1981, 3, 29, 18, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1982, 3, 28, 18, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1983, 3, 27, 18, 0, ), { datetime.timedelta( seconds=3600 ): ( datetime.datetime( 1985, 4, 1, 0, 0, ), { datetime.timedelta( seconds=3600 ): { "Africa/Bangui", "Africa/Brazzaville", "Africa/Douala", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Luanda", "Africa/Malabo", "Africa/Niamey", "Africa/Porto-Novo", "Etc/GMT-1", }, datetime.timedelta( seconds=7200 ): { "Europe/Andorra" }, }, ), datetime.timedelta( seconds=7200 ): { "Europe/Belgrade", "Europe/Ljubljana", "Europe/Podgorica", "Europe/Sarajevo", "Europe/Skopje", "Europe/Zagreb", }, }, ), datetime.timedelta( seconds=7200 ): { "Europe/Gibraltar" }, }, ), datetime.timedelta( seconds=7200 ): { "Europe/Busingen", "Europe/Vaduz", "Europe/Zurich", }, }, ), datetime.timedelta( seconds=7200 ): { "Arctic/Longyearbyen", "Atlantic/Jan_Mayen", "Europe/Berlin", "Europe/Budapest", "Europe/Copenhagen", "Europe/Oslo", "Europe/Stockholm", "Europe/Vienna", }, }, ), datetime.timedelta( seconds=7200 ): { "Africa/Ndjamena" }, }, ), datetime.timedelta( seconds=7200 ): { "Europe/Bratislava", "Europe/Prague", }, }, ), datetime.timedelta( seconds=7200 ): { "Africa/Tunis" }, }, ), datetime.timedelta( seconds=7200 ): { "CET", "Europe/Amsterdam", "Europe/Brussels", "Europe/Luxembourg", "Europe/Warsaw", "MET", "Poland", }, }, ), }, ), datetime.timedelta(seconds=7200): { "Europe/Monaco", "Europe/Paris", }, }, ), datetime.timedelta(seconds=7200): { "Europe/Tirane" }, }, ), datetime.timedelta(seconds=7200): {"Europe/Madrid"}, }, ), }, ), datetime.timedelta(seconds=7200): ( datetime.datetime(1973, 3, 31, 18, 0), { datetime.timedelta(seconds=3600): { "Europe/Rome", "Europe/San_Marino", "Europe/Vatican", }, datetime.timedelta(seconds=7200): {"Europe/Malta"}, }, ), }, ), datetime.timedelta(seconds=7200): ( datetime.datetime(1970, 5, 1, 15, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime(1972, 6, 23, 0, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime(1973, 6, 3, 15, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime(1973, 6, 6, 21, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime(1974, 7, 8, 0, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime( 1975, 4, 13, 18, 0 ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1977, 4, 3, 18, 0 ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1979, 4, 1, 15, 0, ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1979, 5, 27, 15, 0, ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1981, 3, 29, 15, 0, ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1982, 1, 1, 21, 0, ), { datetime.timedelta( seconds=3600 ): { "Africa/Tripoli", "Libya", }, datetime.timedelta( seconds=7200 ): ( datetime.datetime( 1994, 3, 21, 18, 0, ), { datetime.timedelta( seconds=3600 ): { "Africa/Windhoek" }, datetime.timedelta( seconds=7200 ): { "Africa/Blantyre", "Africa/Bujumbura", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Kigali", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Etc/GMT-2", }, }, ), }, ), datetime.timedelta( seconds=10800 ): { "Europe/Helsinki", "Europe/Mariehamn", }, }, ), datetime.timedelta( seconds=10800 ): { "Europe/Bucharest" }, }, ), datetime.timedelta( seconds=10800 ): { "Europe/Sofia" }, }, ), datetime.timedelta( seconds=10800 ): {"EET"}, }, ), datetime.timedelta( seconds=10800 ): ( datetime.datetime( 1975, 10, 12, 18, 0 ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 2016, 10, 31, 3, 0, ), { datetime.timedelta( seconds=7200 ): { "Asia/Nicosia", "Europe/Nicosia", }, datetime.timedelta( seconds=10800 ): { "Asia/Famagusta" }, }, ), datetime.timedelta( seconds=10800 ): { "EET", "Europe/Athens", }, }, ), }, ), datetime.timedelta(seconds=10800): ( datetime.datetime( 1996, 3, 15, 21, 0 ), { datetime.timedelta( seconds=7200 ): ( datetime.datetime( 2008, 8, 29, 18, 0 ), { datetime.timedelta( seconds=7200 ): {"Asia/Gaza"}, datetime.timedelta( seconds=10800 ): {"Asia/Hebron"}, }, ), datetime.timedelta( seconds=10800 ): { "Asia/Jerusalem", "Asia/Tel_Aviv", "Israel", }, }, ), }, ), datetime.timedelta(seconds=10800): { "Asia/Amman" }, }, ), datetime.timedelta(seconds=10800): { "Asia/Istanbul", "Europe/Istanbul", "Turkey", }, }, ), datetime.timedelta(seconds=10800): {"Asia/Beirut"}, }, ), datetime.timedelta(seconds=10800): ( datetime.datetime(1970, 10, 1, 18, 0), { datetime.timedelta(seconds=7200): ( datetime.datetime(1977, 9, 2, 0, 0), { datetime.timedelta(seconds=7200): {"Asia/Damascus"}, datetime.timedelta(seconds=10800): { "Africa/Cairo", "Egypt", }, }, ), datetime.timedelta(seconds=10800): ( datetime.datetime(2017, 11, 1, 18, 0), { datetime.timedelta(seconds=7200): {"Africa/Khartoum"}, datetime.timedelta(seconds=10800): {"Africa/Juba"}, }, ), }, ), }, ), datetime.timedelta(seconds=10800): ( datetime.datetime(1981, 4, 1, 15, 0), { datetime.timedelta(seconds=10800): ( datetime.datetime(1982, 5, 1, 18, 0), { datetime.timedelta(seconds=10800): { "Africa/Addis_Ababa", "Africa/Asmara", "Africa/Asmera", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Kampala", "Africa/Mogadishu", "Africa/Nairobi", "Antarctica/Syowa", "Asia/Aden", "Asia/Kuwait", "Asia/Riyadh", "Etc/GMT-3", "Indian/Antananarivo", "Indian/Comoro", "Indian/Mayotte", }, datetime.timedelta(seconds=14400): {"Asia/Baghdad"}, }, ), datetime.timedelta(seconds=14400): ( datetime.datetime(1989, 3, 26, 21, 0), { datetime.timedelta(seconds=10800): ( datetime.datetime(1996, 9, 29, 15, 0), { datetime.timedelta(seconds=7200): {"Europe/Riga"}, datetime.timedelta(seconds=10800): ( datetime.datetime(1998, 3, 29, 15, 0), { datetime.timedelta(seconds=7200): { "Europe/Vilnius" }, datetime.timedelta(seconds=10800): ( datetime.datetime(2000, 3, 26, 21, 0), { datetime.timedelta(seconds=7200): { "Europe/Tallinn" }, datetime.timedelta(seconds=10800): { "Europe/Kaliningrad" }, }, ), }, ), }, ), datetime.timedelta(seconds=14400): ( datetime.datetime(1990, 3, 25, 21, 0), { datetime.timedelta(seconds=10800): ( datetime.datetime(1990, 7, 2, 0, 0), { datetime.timedelta(seconds=7200): { "Europe/Simferopol" }, datetime.timedelta(seconds=10800): { "Europe/Minsk" }, }, ), datetime.timedelta(seconds=14400): ( datetime.datetime(1990, 5, 6, 15, 0), { datetime.timedelta(seconds=10800): { "Europe/Chisinau", "Europe/Tiraspol", }, datetime.timedelta(seconds=14400): ( datetime.datetime(1990, 7, 1, 15, 0), { datetime.timedelta(seconds=10800): { "Europe/Kiev", "Europe/Kyiv", "Europe/Uzhgorod", "Europe/Zaporozhye", }, datetime.timedelta(seconds=14400): { "Europe/Moscow", "W-SU", }, }, ), }, ), }, ), }, ), }, ), datetime.timedelta(seconds=12600): {"Iran", "Asia/Tehran"}, datetime.timedelta(seconds=14400): ( datetime.datetime(1972, 6, 1, 21, 0), { datetime.timedelta(seconds=10800): {"Asia/Bahrain", "Asia/Qatar"}, datetime.timedelta(seconds=14400): ( datetime.datetime(1981, 4, 1, 15, 0), { datetime.timedelta(seconds=14400): ( datetime.datetime(1982, 10, 11, 0, 0), { datetime.timedelta(seconds=14400): { "Asia/Dubai", "Asia/Muscat", "Etc/GMT-4", "Indian/Mahe", "Indian/Reunion", }, datetime.timedelta(seconds=18000): {"Indian/Mauritius"}, }, ), datetime.timedelta(seconds=18000): ( datetime.datetime(1988, 3, 27, 21, 0), { datetime.timedelta(seconds=14400): ( datetime.datetime(2016, 12, 4, 18, 0), { datetime.timedelta(seconds=10800): { "Europe/Volgograd" }, datetime.timedelta(seconds=14400): { "Europe/Saratov" }, }, ), datetime.timedelta(seconds=18000): ( datetime.datetime(1989, 3, 26, 21, 0), { datetime.timedelta(seconds=14400): ( datetime.datetime(1991, 3, 31, 18, 0), { datetime.timedelta(seconds=10800): ( datetime.datetime( 1991, 9, 29, 18, 0 ), { datetime.timedelta( seconds=7200 ): {"Europe/Ulyanovsk"}, datetime.timedelta( seconds=10800 ): {"Europe/Samara"}, }, ), datetime.timedelta(seconds=14400): ( datetime.datetime( 2016, 3, 27, 18, 0 ), { datetime.timedelta( seconds=10800 ): {"Europe/Kirov"}, datetime.timedelta( seconds=14400 ): {"Europe/Astrakhan"}, }, ), }, ), datetime.timedelta(seconds=18000): ( datetime.datetime(1992, 9, 27, 18, 0), { datetime.timedelta(seconds=10800): ( datetime.datetime( 1994, 9, 25, 18, 0 ), { datetime.timedelta( seconds=10800 ): {"Asia/Yerevan"}, datetime.timedelta( seconds=14400 ): {"Asia/Tbilisi"}, }, ), datetime.timedelta(seconds=14400): { "Asia/Baku" }, }, ), }, ), }, ), }, ), }, ), datetime.timedelta(seconds=16200): {"Asia/Kabul"}, datetime.timedelta(seconds=18000): ( datetime.datetime(1981, 4, 1, 15, 0), { datetime.timedelta(seconds=18000): ( datetime.datetime(1981, 10, 2, 0, 0), { datetime.timedelta(seconds=18000): ( datetime.datetime(1996, 1, 1, 21, 0), { datetime.timedelta(seconds=18000): ( datetime.datetime(2002, 4, 8, 0, 0), { datetime.timedelta(seconds=18000): { "Etc/GMT-5", "Indian/Kerguelen", "Indian/Maldives", }, datetime.timedelta(seconds=21600): { "Asia/Karachi" }, }, ), datetime.timedelta(seconds=21600): {"Indian/Chagos"}, }, ), datetime.timedelta(seconds=21600): ( datetime.datetime(1994, 9, 26, 3, 0), { datetime.timedelta(seconds=14400): {"Asia/Aqtau"}, datetime.timedelta(seconds=18000): {"Asia/Atyrau"}, }, ), }, ), datetime.timedelta(seconds=21600): ( datetime.datetime(1981, 10, 1, 12, 0), { datetime.timedelta(seconds=18000): ( datetime.datetime(1992, 3, 29, 18, 0), { datetime.timedelta(seconds=18000): { "Asia/Ashgabat", "Asia/Ashkhabad", }, datetime.timedelta(seconds=21600): { "Asia/Yekaterinburg" }, }, ), datetime.timedelta(seconds=21600): ( datetime.datetime(1989, 3, 26, 15, 0), { datetime.timedelta(seconds=18000): {"Asia/Oral"}, datetime.timedelta(seconds=21600): ( datetime.datetime(1991, 4, 1, 0, 0), { datetime.timedelta(seconds=18000): ( datetime.datetime(1991, 9, 30, 0, 0), { datetime.timedelta(seconds=14400): ( datetime.datetime( 2004, 10, 31, 18, 0 ), { datetime.timedelta( seconds=18000 ): {"Asia/Aqtobe"}, datetime.timedelta( seconds=21600 ): {"Asia/Qostanay"}, }, ), datetime.timedelta(seconds=18000): { "Asia/Qyzylorda" }, }, ), datetime.timedelta(seconds=21600): { "Asia/Samarkand" }, }, ), }, ), }, ), }, ), datetime.timedelta(seconds=19800): ( datetime.datetime(1986, 1, 1, 18, 0), { datetime.timedelta(seconds=19800): ( datetime.datetime(1987, 10, 1, 18, 0), { datetime.timedelta(seconds=19800): ( datetime.datetime(1996, 5, 25, 15, 0), { datetime.timedelta(seconds=19800): { "Asia/Calcutta", "Asia/Kolkata", }, datetime.timedelta(seconds=23400): {"Asia/Colombo"}, }, ), datetime.timedelta(seconds=21600): { "Asia/Thimbu", "Asia/Thimphu", }, }, ), datetime.timedelta(seconds=20700): {"Asia/Kathmandu", "Asia/Katmandu"}, }, ), datetime.timedelta(seconds=21600): ( datetime.datetime(1978, 1, 2, 0, 0), { datetime.timedelta(seconds=21600): ( datetime.datetime(1981, 4, 1, 18, 0), { datetime.timedelta(seconds=21600): ( datetime.datetime(2009, 6, 20, 18, 0), { datetime.timedelta(seconds=21600): ( datetime.datetime(2009, 10, 18, 21, 0), { datetime.timedelta(seconds=18000): { "Antarctica/Mawson" }, datetime.timedelta(seconds=21600): { "Asia/Kashgar", "Asia/Urumqi", "Etc/GMT-6", }, }, ), datetime.timedelta(seconds=25200): { "Asia/Dacca", "Asia/Dhaka", }, }, ), datetime.timedelta(seconds=25200): ( datetime.datetime(1991, 8, 31, 21, 0), { datetime.timedelta(seconds=18000): {"Asia/Bishkek"}, datetime.timedelta(seconds=21600): ( datetime.datetime(1991, 9, 9, 15, 0), { datetime.timedelta(seconds=18000): { "Asia/Dushanbe" }, datetime.timedelta(seconds=21600): ( datetime.datetime(1992, 1, 19, 18, 0), { datetime.timedelta(seconds=18000): { "Asia/Tashkent" }, datetime.timedelta(seconds=21600): ( datetime.datetime( 2005, 3, 27, 15, 0 ), { datetime.timedelta( seconds=21600 ): {"Asia/Almaty"}, datetime.timedelta( seconds=25200 ): {"Asia/Omsk"}, }, ), }, ), }, ), }, ), }, ), datetime.timedelta(seconds=25200): {"Asia/Hovd"}, }, ), datetime.timedelta(seconds=23400): { "Asia/Rangoon", "Asia/Yangon", "Indian/Cocos", }, datetime.timedelta(seconds=25200): ( datetime.datetime(1978, 1, 2, 0, 0), { datetime.timedelta(seconds=25200): ( datetime.datetime(1981, 4, 1, 18, 0), { datetime.timedelta(seconds=25200): ( datetime.datetime(1994, 2, 1, 12, 0), { datetime.timedelta(0): {"Antarctica/Vostok"}, datetime.timedelta(seconds=25200): ( datetime.datetime(2009, 10, 18, 21, 0), { datetime.timedelta(seconds=18000): { "Antarctica/Davis" }, datetime.timedelta(seconds=25200): { "Asia/Bangkok", "Asia/Jakarta", "Asia/Phnom_Penh", "Asia/Vientiane", "Etc/GMT-7", "Indian/Christmas", }, }, ), }, ), datetime.timedelta(seconds=28800): ( datetime.datetime(1993, 5, 23, 18, 0), { datetime.timedelta(seconds=25200): {"Asia/Novosibirsk"}, datetime.timedelta(seconds=28800): ( datetime.datetime(1995, 5, 28, 18, 0), { datetime.timedelta(seconds=25200): { "Asia/Barnaul" }, datetime.timedelta(seconds=28800): ( datetime.datetime(2002, 5, 2, 0, 0), { datetime.timedelta(seconds=25200): { "Asia/Tomsk" }, datetime.timedelta(seconds=28800): ( datetime.datetime( 2010, 3, 28, 15, 0 ), { datetime.timedelta( seconds=25200 ): {"Asia/Novokuznetsk"}, datetime.timedelta( seconds=28800 ): {"Asia/Krasnoyarsk"}, }, ), }, ), }, ), }, ), }, ), datetime.timedelta(seconds=28800): { "Asia/Choibalsan", "Asia/Ulaanbaatar", "Asia/Ulan_Bator", }, }, ), datetime.timedelta(seconds=27000): { "Asia/Kuala_Lumpur", "Asia/Singapore", "Singapore", }, datetime.timedelta(seconds=28800): ( datetime.datetime(1970, 4, 19, 18, 0), { datetime.timedelta(seconds=28800): ( datetime.datetime(1974, 4, 1, 15, 0), { datetime.timedelta(seconds=28800): ( datetime.datetime(1974, 10, 27, 15, 0), { datetime.timedelta(seconds=28800): ( datetime.datetime(1975, 6, 13, 21, 0), { datetime.timedelta(seconds=25200): { "Asia/Ho_Chi_Minh", "Asia/Saigon", }, datetime.timedelta(seconds=28800): ( datetime.datetime(1978, 3, 22, 21, 0), { datetime.timedelta(seconds=28800): ( datetime.datetime( 1981, 4, 1, 21, 0 ), { datetime.timedelta( seconds=28800 ): ( datetime.datetime( 1986, 5, 4, 21, 0 ), { datetime.timedelta( seconds=28800 ): ( datetime.datetime( 1988, 1, 1, 18, 0, ), { datetime.timedelta( seconds=25200 ): { "Asia/Pontianak" }, datetime.timedelta( seconds=28800 ): ( datetime.datetime( 2009, 10, 19, 3, 0, ), { datetime.timedelta( seconds=28800 ): { "Asia/Brunei", "Asia/Kuching", "Asia/Makassar", "Asia/Ujung_Pandang", "Etc/GMT-8", }, datetime.timedelta( seconds=39600 ): { "Antarctica/Casey" }, }, ), }, ), datetime.timedelta( seconds=32400 ): { "Asia/Chongqing", "Asia/Chungking", "Asia/Harbin", "Asia/Shanghai", "PRC", }, }, ), datetime.timedelta( seconds=32400 ): {"Asia/Irkutsk"}, }, ), datetime.timedelta(seconds=32400): { "Asia/Manila" }, }, ), }, ), datetime.timedelta(seconds=32400): { "Australia/Perth", "Australia/West", }, }, ), datetime.timedelta(seconds=32400): {"Asia/Taipei", "ROC"}, }, ), datetime.timedelta(seconds=32400): { "Asia/Hong_Kong", "Asia/Macao", "Asia/Macau", "Hongkong", }, }, ), datetime.timedelta(seconds=31500): {"Australia/Eucla"}, datetime.timedelta(seconds=32400): ( datetime.datetime(1976, 5, 3, 18, 0), { datetime.timedelta(seconds=28800): {"Asia/Dili"}, datetime.timedelta(seconds=32400): ( datetime.datetime(1981, 4, 1, 18, 0), { datetime.timedelta(seconds=32400): ( datetime.datetime(1987, 5, 10, 15, 0), { datetime.timedelta(seconds=32400): ( datetime.datetime(2015, 8, 15, 15, 0), { datetime.timedelta(seconds=30600): { "Asia/Pyongyang" }, datetime.timedelta(seconds=32400): { "Asia/Jayapura", "Asia/Tokyo", "Etc/GMT-9", "Japan", "Pacific/Palau", }, }, ), datetime.timedelta(seconds=36000): { "Asia/Seoul", "ROK", }, }, ), datetime.timedelta(seconds=36000): ( datetime.datetime(2004, 1, 1, 21, 0), { datetime.timedelta(seconds=32400): ( datetime.datetime(2014, 10, 27, 0, 0), { datetime.timedelta(seconds=28800): { "Asia/Chita" }, datetime.timedelta(seconds=32400): { "Asia/Yakutsk" }, }, ), datetime.timedelta(seconds=36000): {"Asia/Khandyga"}, }, ), datetime.timedelta(seconds=43200): {"Asia/Ust-Nera"}, }, ), }, ), datetime.timedelta(seconds=34200): ( datetime.datetime(1971, 10, 31, 21, 0), { datetime.timedelta(seconds=34200): { "Australia/Darwin", "Australia/North", }, datetime.timedelta(seconds=37800): ( datetime.datetime(1982, 3, 7, 18, 0), { datetime.timedelta(seconds=34200): { "Australia/Adelaide", "Australia/South", }, datetime.timedelta(seconds=37800): { "Australia/Broken_Hill", "Australia/Yancowinna", }, }, ), }, ), datetime.timedelta(seconds=36000): ( datetime.datetime(1970, 4, 26, 15, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(1971, 10, 31, 21, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(1981, 3, 2, 0, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(1981, 4, 1, 18, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(2014, 12, 28, 21, 0), { datetime.timedelta(seconds=36000): { "Antarctica/DumontDUrville", "Etc/GMT-10", "Pacific/Chuuk", "Pacific/Port_Moresby", "Pacific/Truk", "Pacific/Yap", }, datetime.timedelta(seconds=39600): { "Pacific/Bougainville" }, }, ), datetime.timedelta(seconds=39600): { "Asia/Vladivostok" }, }, ), datetime.timedelta(seconds=37800): { "Australia/LHI", "Australia/Lord_Howe", }, }, ), datetime.timedelta(seconds=39600): ( datetime.datetime(1972, 10, 29, 21, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(1992, 10, 26, 0, 0), { datetime.timedelta(seconds=36000): { "Australia/Brisbane", "Australia/Queensland", }, datetime.timedelta(seconds=39600): { "Australia/Lindeman" }, }, ), datetime.timedelta(seconds=39600): ( datetime.datetime(1982, 3, 7, 18, 0), { datetime.timedelta(seconds=36000): { "Australia/Melbourne", "Australia/Victoria", }, datetime.timedelta(seconds=39600): { "Australia/ACT", "Australia/Canberra", "Australia/NSW", "Australia/Sydney", }, }, ), }, ), }, ), datetime.timedelta(seconds=39600): {"Pacific/Guam", "Pacific/Saipan"}, }, ), datetime.timedelta(seconds=39600): ( datetime.datetime(1970, 3, 8, 15, 0), { datetime.timedelta(seconds=36000): ( datetime.datetime(2010, 4, 4, 21, 0), { datetime.timedelta(seconds=36000): { "Australia/Currie", "Australia/Hobart", "Australia/Tasmania", }, datetime.timedelta(seconds=39600): {"Antarctica/Macquarie"}, }, ), datetime.timedelta(seconds=39600): ( datetime.datetime(1973, 12, 23, 15, 0), { datetime.timedelta(seconds=39600): ( datetime.datetime(1977, 12, 4, 15, 0), { datetime.timedelta(seconds=39600): ( datetime.datetime(1981, 4, 1, 18, 0), { datetime.timedelta(seconds=39600): { "Etc/GMT-11", "Pacific/Guadalcanal", "Pacific/Pohnpei", "Pacific/Ponape", }, datetime.timedelta(seconds=43200): ( datetime.datetime(1997, 3, 30, 15, 0), { datetime.timedelta(seconds=39600): { "Asia/Sakhalin" }, datetime.timedelta(seconds=43200): ( datetime.datetime( 2014, 10, 26, 18, 0 ), { datetime.timedelta( seconds=36000 ): {"Asia/Magadan"}, datetime.timedelta( seconds=39600 ): {"Asia/Srednekolymsk"}, }, ), }, ), }, ), datetime.timedelta(seconds=43200): {"Pacific/Noumea"}, }, ), datetime.timedelta(seconds=43200): {"Pacific/Efate"}, }, ), }, ), datetime.timedelta(seconds=41400): ( datetime.datetime(1974, 10, 27, 15, 0), { datetime.timedelta(seconds=41400): {"Pacific/Nauru"}, datetime.timedelta(seconds=45000): {"Pacific/Norfolk"}, }, ), datetime.timedelta(seconds=43200): ( datetime.datetime(1974, 11, 3, 21, 0), { datetime.timedelta(seconds=43200): ( datetime.datetime(1981, 4, 1, 15, 0), { datetime.timedelta(seconds=43200): ( datetime.datetime(1998, 11, 1, 18, 0), { datetime.timedelta(seconds=43200): ( datetime.datetime(1999, 1, 1, 18, 0), { datetime.timedelta(seconds=39600): { "Pacific/Kosrae" }, datetime.timedelta(seconds=43200): { "Etc/GMT-12", "Pacific/Funafuti", "Pacific/Majuro", "Pacific/Tarawa", "Pacific/Wake", "Pacific/Wallis", }, }, ), datetime.timedelta(seconds=46800): {"Pacific/Fiji"}, }, ), datetime.timedelta(seconds=46800): {"Asia/Kamchatka"}, }, ), datetime.timedelta(seconds=46800): { "Antarctica/McMurdo", "Antarctica/South_Pole", "NZ", "Pacific/Auckland", }, }, ), datetime.timedelta(seconds=45900): {"NZ-CHAT", "Pacific/Chatham"}, datetime.timedelta(seconds=46800): ( datetime.datetime(1981, 4, 1, 15, 0), { datetime.timedelta(seconds=46800): ( datetime.datetime(1999, 10, 7, 21, 0), { datetime.timedelta(seconds=46800): {"Etc/GMT-13"}, datetime.timedelta(seconds=50400): {"Pacific/Tongatapu"}, }, ), datetime.timedelta(seconds=50400): {"Asia/Anadyr"}, }, ), datetime.timedelta(seconds=50400): {"Etc/GMT-14"}, }, ) __all__ = ["lookup"] collective-icalendar-138c845/src/icalendar/timezone/provider.py000066400000000000000000000030601524134033600246130ustar00rootroot00000000000000"""The interface for timezone implementations.""" from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING if TYPE_CHECKING: from datetime import datetime, tzinfo from dateutil.rrule import rrule from icalendar import prop class TZProvider(ABC): """Interface for timezone implementations.""" @property @abstractmethod def name(self) -> str: """The name of the implementation.""" @abstractmethod def localize_utc(self, dt: datetime) -> datetime: """Return the datetime in UTC.""" @abstractmethod def localize(self, dt: datetime, tz: tzinfo) -> datetime: """Localize a datetime to a timezone.""" @abstractmethod def knows_timezone_id(self, tzid: str) -> bool: """Whether the timezone is already cached by the implementation.""" @abstractmethod def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: """Make sure the until value works for rrules generated from the ical_rrule.""" @abstractmethod def create_timezone(self, name: str, transition_times, transition_info) -> tzinfo: """Create a pytz timezone file given information.""" @abstractmethod def timezone(self, name: str) -> tzinfo | None: """Return a timezone with a name or None if we cannot find it.""" @abstractmethod def uses_pytz(self) -> bool: """Whether we use pytz.""" @abstractmethod def uses_zoneinfo(self) -> bool: """Whether we use zoneinfo.""" __all__ = ["TZProvider"] collective-icalendar-138c845/src/icalendar/timezone/pytz.py000066400000000000000000000045301524134033600237720ustar00rootroot00000000000000"""Use pytz timezones.""" from __future__ import annotations from datetime import datetime, tzinfo from typing import TYPE_CHECKING import pytz from pytz.tzinfo import DstTzInfo from .provider import TZProvider if TYPE_CHECKING: from dateutil.rrule import rrule from icalendar import prop from icalendar.cal import Timezone class PYTZ(TZProvider): """Provide icalendar with timezones from pytz.""" name = "pytz" def localize_utc(self, dt: datetime) -> datetime: """Return the datetime in UTC.""" if getattr(dt, "tzinfo", False) and dt.tzinfo is not None: return dt.astimezone(pytz.utc) # assume UTC for naive datetime instances return pytz.utc.localize(dt) def localize(self, dt: datetime, tz: tzinfo) -> datetime: """Localize a datetime to a timezone.""" return tz.localize(dt) def knows_timezone_id(self, tzid: str) -> bool: """Whether the timezone is already cached by the implementation.""" return tzid in pytz.all_timezones def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: """Make sure the until value works for rrules generated from the ical_rrule.""" if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()): # pytz.timezones don't know any transition dates after 2038 # either rrule._until = datetime(2038, 12, 31, tzinfo=pytz.UTC) # noqa: SLF001 def create_timezone(self, tz: Timezone.Timezone) -> tzinfo: """Create a pytz timezone from the given information.""" transition_times, transition_info = tz.get_transitions() name = tz.tz_name cls = type( name, (DstTzInfo,), { "zone": name, "_utc_transition_times": transition_times, "_transition_info": transition_info, }, ) return cls() def timezone(self, name: str) -> tzinfo | None: """Return a timezone with a name or None if we cannot find it.""" try: return pytz.timezone(name) except pytz.UnknownTimeZoneError: pass def uses_pytz(self) -> bool: """Whether we use pytz.""" return True def uses_zoneinfo(self) -> bool: """Whether we use zoneinfo.""" return False __all__ = ["PYTZ"] collective-icalendar-138c845/src/icalendar/timezone/tzid.py000066400000000000000000000116321524134033600237370ustar00rootroot00000000000000"""This module identifies timezones. Normally, timezones have ids. This is a way to access the ids if you have a datetime.tzinfo object. """ from __future__ import annotations from collections import defaultdict from datetime import date, time, timezone from pathlib import Path from typing import TYPE_CHECKING from dateutil.tz import tz from icalendar.timezone import equivalent_timezone_ids_result from icalendar.timezone.windows_to_olson import WINDOWS_TO_OLSON from icalendar.tools import is_date try: from dateutil.tz.win import tzwin as _tzwin except ImportError: _tzwin = None # not on Windows if TYPE_CHECKING: from datetime import datetime, tzinfo DATEUTIL_UTC = tz.gettz("UTC") DATEUTIL_UTC_PATH: str | None = getattr(DATEUTIL_UTC, "_filename", None) DATEUTIL_ZONEINFO_PATH = ( None if DATEUTIL_UTC_PATH is None else Path(DATEUTIL_UTC_PATH).parent ) def tzids_from_tzinfo(tzinfo: tzinfo | None) -> tuple[str]: """Get several timezone ids if we can identify the timezone. >>> import zoneinfo >>> from icalendar.timezone.tzid import tzids_from_tzinfo >>> tzids_from_tzinfo(zoneinfo.ZoneInfo("Arctic/Longyearbyen")) ('Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Berlin', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna') >>> from dateutil.tz import gettz >>> tzids_from_tzinfo(gettz("Europe/Berlin")) ('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna') """ # The example might need to change if you recreate the lookup tree # noqa: E501 if tzinfo is None: return () if isinstance(tzinfo, timezone): # fixed offset timezone with name return get_equivalent_tzids(tzinfo.tzname(None)) if hasattr(tzinfo, "zone"): return get_equivalent_tzids(tzinfo.zone) # pytz implementation if hasattr(tzinfo, "key"): return get_equivalent_tzids(tzinfo.key) # ZoneInfo implementation if isinstance(tzinfo, tz._tzicalvtz): # noqa: SLF001 return get_equivalent_tzids(tzinfo._tzid) # noqa: SLF001 if isinstance(tzinfo, tz.tzstr): return get_equivalent_tzids(tzinfo._s) # noqa: SLF001 if _tzwin is not None and isinstance(tzinfo, _tzwin): olson = WINDOWS_TO_OLSON.get(tzinfo._name) # noqa: SLF001 if olson is not None: return get_equivalent_tzids(olson) return get_equivalent_tzids(tzinfo._name) # noqa: SLF001 if hasattr(tzinfo, "_filename"): # dateutil.tz.tzfile # noqa: SIM102 if DATEUTIL_ZONEINFO_PATH is not None: # tzfile('/usr/share/zoneinfo/Europe/Berlin') path = tzinfo._filename # noqa: SLF001 if path.startswith(str(DATEUTIL_ZONEINFO_PATH)): tzid = str(Path(path).relative_to(DATEUTIL_ZONEINFO_PATH)) return get_equivalent_tzids(tzid) return get_equivalent_tzids(path) if isinstance(tzinfo, tz.tzutc): return get_equivalent_tzids("UTC") return () def tzid_from_tzinfo(tzinfo: tzinfo | None) -> str | None: """Retrieve the timezone id from the tzinfo object. Some timezones are equivalent. Thus, we might return one ID that is equivalent to others. """ tzids = tzids_from_tzinfo(tzinfo) if "UTC" in tzids: return "UTC" if not tzids: return None return tzids[0] def tzid_from_dt(dt: datetime) -> str | None: """Retrieve the timezone id from the datetime object.""" tzid = tzid_from_tzinfo(dt.tzinfo) if tzid is None: return dt.tzname() return tzid _EQUIVALENT_IDS: dict[str, set[str]] = defaultdict(set) def _add_equivalent_ids(value: tuple | dict | set): """This adds equivalent ids/ As soon as one timezone implementation used claims their equivalence, they are considered equivalent. Have a look at icalendar.timezone.equivalent_timezone_ids. """ if isinstance(value, set): for tzid in value: _EQUIVALENT_IDS[tzid].update(value) elif isinstance(value, tuple): _add_equivalent_ids(value[1]) elif isinstance(value, dict): for value2 in value.values(): _add_equivalent_ids(value2) else: raise TypeError( f"Expected tuple, dict or set, not {value.__class__.__name__}: {value!r}" ) _add_equivalent_ids(equivalent_timezone_ids_result.lookup) def get_equivalent_tzids(tzid: str) -> tuple[str]: """This returns the tzids which are equivalent to this one.""" ids = _EQUIVALENT_IDS.get(tzid, set()) return (tzid,) + tuple(sorted(ids - {tzid})) def is_utc(t: datetime | time | date | tzinfo) -> bool: """Whether this date is in UTC.""" if is_date(t): return False tzid = tzid_from_dt(t) if hasattr(t, "tzinfo") else tzid_from_tzinfo(t) return tzid == "UTC" __all__ = ["is_utc", "tzid_from_dt", "tzid_from_tzinfo", "tzids_from_tzinfo"] collective-icalendar-138c845/src/icalendar/timezone/tzp.py000066400000000000000000000173671524134033600236150ustar00rootroot00000000000000from __future__ import annotations import warnings from datetime import datetime, time from typing import TYPE_CHECKING, overload from icalendar.tools import to_datetime from .windows_to_olson import WINDOWS_TO_OLSON if TYPE_CHECKING: from collections.abc import Iterator from dateutil.rrule import rrule from icalendar import prop from icalendar.cal import Timezone from .provider import TZProvider DEFAULT_TIMEZONE_PROVIDER = "zoneinfo" class TZP: """This is the timezone provider proxy. If you would like to have another timezone implementation, you can create a new one and pass it to this proxy. All of icalendar will then use this timezone implementation. """ def __init__(self, provider: str | TZProvider = DEFAULT_TIMEZONE_PROVIDER) -> None: """Create a new timezone implementation proxy.""" self.use(provider) def use_pytz(self) -> None: """Use pytz as the timezone provider.""" from .pytz import PYTZ # noqa: PLC0415, RUF100 self._use(PYTZ()) def use_zoneinfo(self) -> None: """Use zoneinfo as the timezone provider.""" from .zoneinfo import ZONEINFO # noqa: PLC0415, RUF100 self._use(ZONEINFO()) def _use(self, provider: TZProvider) -> None: """Use a timezone implementation.""" self.__tz_cache = {} self.__provider = provider def use(self, provider: str | TZProvider): """Switch to a different timezone provider.""" if isinstance(provider, str): use_provider = getattr(self, f"use_{provider}", None) if use_provider is None: raise ValueError( f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'." ) use_provider() else: self._use(provider) def use_default(self) -> None: """Use the default timezone provider.""" self.use(DEFAULT_TIMEZONE_PROVIDER) def localize_utc(self, dt: datetime.date) -> datetime.datetime: """Return the datetime in UTC. If the datetime has no timezone, set UTC as its timezone. """ return self.__provider.localize_utc(to_datetime(dt)) @overload def localize( self, dt: datetime.datetime, tz: datetime.tzinfo | str | None ) -> datetime.datetime: ... @overload def localize( self, dt: datetime.time, tz: datetime.tzinfo | str | None ) -> datetime.time: ... def localize( self, dt: datetime.date | datetime.time, tz: datetime.tzinfo | str | None ) -> datetime.datetime | datetime.time: """Localize a datetime or time to a timezone. Returns: - A localized :class:`datetime.datetime` when a :class:`datetime.datetime` is given. - A localized :class:`datetime.time` when a :class:`datetime.time` is given. """ if isinstance(tz, str): tz = self.timezone(tz) if tz is None: return dt.replace(tzinfo=None) if isinstance(dt, time): dt_full = datetime.combine(datetime(2020, 1, 1), dt) # noqa: DTZ001 localized = self.__provider.localize(dt_full, tz) return localized.timetz() return self.__provider.localize(to_datetime(dt), tz) def cache_timezone_component(self, timezone_component: Timezone.Timezone) -> None: """Cache the timezone that is created from a timezone component if it is not already known. This can influence the result from timezone(): Once cached, the custom timezone is returned from timezone(). """ _unclean_id = timezone_component["TZID"] _id = self.clean_timezone_id(_unclean_id) if ( not self.__provider.knows_timezone_id(_id) and not self.__provider.knows_timezone_id(_unclean_id) and _id not in self.__tz_cache ): self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False) def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: """Make sure the until value works.""" self.__provider.fix_rrule_until(rrule, ical_rrule) def create_timezone(self, timezone_component: Timezone.Timezone) -> datetime.tzinfo: """Create a timezone from a timezone component. This component will not be cached. """ return self.__provider.create_timezone(timezone_component) def clean_timezone_id(self, tzid: str) -> str: """Return a clean version of the timezone id. Timezone ids can be a bit unclean, starting with a / for example. Internally, we should use this to identify timezones. """ return tzid.strip("/") def timezone(self, tz_id: str) -> datetime.tzinfo | None: """Return a timezone with an ID or ``None`` if we can't find it. ``tz_id`` may be a plain Olson name (``Europe/Berlin``), a Windows timezone name, or a "globally unique" identifier (:rfc:`5545#section-3.2.19`) such as ``/freeassociation.sourceforge.net/Europe/Berlin``. We try the candidate IDs from :meth:`_lookup_ids` in order, checking the cache before the provider for each one, and cache the first match under the primary ID so the next lookup is fast. """ primary = None for lookup_id, is_global_guess in self._lookup_ids(tz_id): if primary is None: primary = lookup_id tz = self.__tz_cache.get(lookup_id) or self.__provider.timezone(lookup_id) if tz is not None: if is_global_guess: from icalendar.error import GloballyUniqueTZIDGuessed warnings.warn( f"Timezone {tz_id!r} is a globally unique TZID; " f"guessing it means {lookup_id!r} by stripping the vendor " "prefix. This may be wrong. See RFC 5545 section 3.2.19.", GloballyUniqueTZIDGuessed, stacklevel=3, ) self.__tz_cache[primary] = tz return tz return None def _lookup_ids(self, tz_id: str) -> Iterator[tuple[str, bool]]: """Yield ``(id, is_global_guess)`` tuples to try, best match first. 1. The cleaned ID, without any surrounding ``/``. 2. The Olson name of a Windows timezone (for example, ``W. Europe Standard Time`` -> ``Europe/Berlin``). 3. For a "globally unique" TZID (:rfc:`5545#section-3.2.19`) of the form ``//``—emitted by clients such as libical, Evolution and Mozilla Lightning—the trailing Olson identifier, dropping vendor path components from the front. The longest suffix is tried first, so multi-part names such as ``America/Argentina/Buenos_Aires`` still match. 4. The original, unmodified ID. """ cleaned = self.clean_timezone_id(tz_id) yield cleaned, False if cleaned in WINDOWS_TO_OLSON: yield WINDOWS_TO_OLSON[cleaned], False if tz_id.startswith("/"): parts = cleaned.split("/") for start in range(1, len(parts)): yield "/".join(parts[start:]), True yield tz_id, False def uses_pytz(self) -> bool: """Whether we use pytz at all.""" return self.__provider.uses_pytz() def uses_zoneinfo(self) -> bool: """Whether we use zoneinfo.""" return self.__provider.uses_zoneinfo() @property def name(self) -> str: """The name of the timezone component used.""" return self.__provider.name def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r})" __all__ = ["TZP"] collective-icalendar-138c845/src/icalendar/timezone/windows_to_olson.py000066400000000000000000000165021524134033600263740ustar00rootroot00000000000000""" This module contains mappings from Windows timezone identifiers to Olson timezone identifiers. This file is automatically generated by generate_windows_to_olson_mapping.py. Do not edit manually. The data is taken from the Unicode Consortium [0] at commit ``c33a1f0a23e3b676b406345fe0b42c130defc51d``, dated 2025-04-10. The proposal and rationale for this mapping is also available at the Unicode Consortium [1]. [0] https://github.com/unicode-org/cldr/blob/c33a1f0a23e3b676b406345fe0b42c130defc51d/common/supplemental/windowsZones.xml [1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping """ version = "c33a1f0a23e3b676b406345fe0b42c130defc51d" version_date = "2025-04-10" WINDOWS_TO_OLSON = { "AUS Central Standard Time": "Australia/Darwin", "AUS Eastern Standard Time": "Australia/Sydney", "Afghanistan Standard Time": "Asia/Kabul", "Alaskan Standard Time": "America/Anchorage", "Aleutian Standard Time": "America/Adak", "Altai Standard Time": "Asia/Barnaul", "Arab Standard Time": "Asia/Riyadh", "Arabian Standard Time": "Asia/Dubai", "Arabic Standard Time": "Asia/Baghdad", "Argentina Standard Time": "America/Buenos_Aires", "Astrakhan Standard Time": "Europe/Astrakhan", "Atlantic Standard Time": "America/Halifax", "Aus Central W. Standard Time": "Australia/Eucla", "Azerbaijan Standard Time": "Asia/Baku", "Azores Standard Time": "Atlantic/Azores", "Bahia Standard Time": "America/Bahia", "Bangladesh Standard Time": "Asia/Dhaka", "Belarus Standard Time": "Europe/Minsk", "Bougainville Standard Time": "Pacific/Bougainville", "Canada Central Standard Time": "America/Regina", "Cape Verde Standard Time": "Atlantic/Cape_Verde", "Caucasus Standard Time": "Asia/Yerevan", "Cen. Australia Standard Time": "Australia/Adelaide", "Central America Standard Time": "America/Guatemala", "Central Asia Standard Time": "Asia/Bishkek", "Central Brazilian Standard Time": "America/Cuiaba", "Central Europe Standard Time": "Europe/Budapest", "Central European Standard Time": "Europe/Warsaw", "Central Pacific Standard Time": "Pacific/Guadalcanal", "Central Standard Time": "America/Chicago", "Central Standard Time (Mexico)": "America/Mexico_City", "Chatham Islands Standard Time": "Pacific/Chatham", "China Standard Time": "Asia/Shanghai", "Cuba Standard Time": "America/Havana", "Dateline Standard Time": "Etc/GMT+12", "E. Africa Standard Time": "Africa/Nairobi", "E. Australia Standard Time": "Australia/Brisbane", "E. Europe Standard Time": "Europe/Chisinau", "E. South America Standard Time": "America/Sao_Paulo", "Easter Island Standard Time": "Pacific/Easter", "Eastern Standard Time": "America/New_York", "Eastern Standard Time (Mexico)": "America/Cancun", "Egypt Standard Time": "Africa/Cairo", "Ekaterinburg Standard Time": "Asia/Yekaterinburg", "FLE Standard Time": "Europe/Kiev", "Fiji Standard Time": "Pacific/Fiji", "GMT Standard Time": "Europe/London", "GTB Standard Time": "Europe/Bucharest", "Georgian Standard Time": "Asia/Tbilisi", "Greenland Standard Time": "America/Godthab", "Greenwich Standard Time": "Atlantic/Reykjavik", "Haiti Standard Time": "America/Port-au-Prince", "Hawaiian Standard Time": "Pacific/Honolulu", "India Standard Time": "Asia/Calcutta", "Iran Standard Time": "Asia/Tehran", "Israel Standard Time": "Asia/Jerusalem", "Jordan Standard Time": "Asia/Amman", "Kaliningrad Standard Time": "Europe/Kaliningrad", "Korea Standard Time": "Asia/Seoul", "Libya Standard Time": "Africa/Tripoli", "Line Islands Standard Time": "Pacific/Kiritimati", "Lord Howe Standard Time": "Australia/Lord_Howe", "Magadan Standard Time": "Asia/Magadan", "Magallanes Standard Time": "America/Punta_Arenas", "Marquesas Standard Time": "Pacific/Marquesas", "Mauritius Standard Time": "Indian/Mauritius", "Middle East Standard Time": "Asia/Beirut", "Montevideo Standard Time": "America/Montevideo", "Morocco Standard Time": "Africa/Casablanca", "Mountain Standard Time": "America/Denver", "Mountain Standard Time (Mexico)": "America/Mazatlan", "Myanmar Standard Time": "Asia/Rangoon", "N. Central Asia Standard Time": "Asia/Novosibirsk", "Namibia Standard Time": "Africa/Windhoek", "Nepal Standard Time": "Asia/Katmandu", "New Zealand Standard Time": "Pacific/Auckland", "Newfoundland Standard Time": "America/St_Johns", "Norfolk Standard Time": "Pacific/Norfolk", "North Asia East Standard Time": "Asia/Irkutsk", "North Asia Standard Time": "Asia/Krasnoyarsk", "North Korea Standard Time": "Asia/Pyongyang", "Omsk Standard Time": "Asia/Omsk", "Pacific SA Standard Time": "America/Santiago", "Pacific Standard Time": "America/Los_Angeles", "Pacific Standard Time (Mexico)": "America/Tijuana", "Pakistan Standard Time": "Asia/Karachi", "Paraguay Standard Time": "America/Asuncion", "Qyzylorda Standard Time": "Asia/Qyzylorda", "Romance Standard Time": "Europe/Paris", "Russia Time Zone 10": "Asia/Srednekolymsk", "Russia Time Zone 11": "Asia/Kamchatka", "Russia Time Zone 3": "Europe/Samara", "Russian Standard Time": "Europe/Moscow", "SA Eastern Standard Time": "America/Cayenne", "SA Pacific Standard Time": "America/Bogota", "SA Western Standard Time": "America/La_Paz", "SE Asia Standard Time": "Asia/Bangkok", "Saint Pierre Standard Time": "America/Miquelon", "Sakhalin Standard Time": "Asia/Sakhalin", "Samoa Standard Time": "Pacific/Apia", "Sao Tome Standard Time": "Africa/Sao_Tome", "Saratov Standard Time": "Europe/Saratov", "Singapore Standard Time": "Asia/Singapore", "South Africa Standard Time": "Africa/Johannesburg", "South Sudan Standard Time": "Africa/Juba", "Sri Lanka Standard Time": "Asia/Colombo", "Sudan Standard Time": "Africa/Khartoum", "Syria Standard Time": "Asia/Damascus", "Taipei Standard Time": "Asia/Taipei", "Tasmania Standard Time": "Australia/Hobart", "Tocantins Standard Time": "America/Araguaina", "Tokyo Standard Time": "Asia/Tokyo", "Tomsk Standard Time": "Asia/Tomsk", "Tonga Standard Time": "Pacific/Tongatapu", "Transbaikal Standard Time": "Asia/Chita", "Turkey Standard Time": "Europe/Istanbul", "Turks And Caicos Standard Time": "America/Grand_Turk", "US Eastern Standard Time": "America/Indianapolis", "US Mountain Standard Time": "America/Phoenix", "UTC": "Etc/UTC", "UTC+12": "Etc/GMT-12", "UTC+13": "Etc/GMT-13", "UTC-02": "Etc/GMT+2", "UTC-08": "Etc/GMT+8", "UTC-09": "Etc/GMT+9", "UTC-11": "Etc/GMT+11", "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", "Venezuela Standard Time": "America/Caracas", "Vladivostok Standard Time": "Asia/Vladivostok", "Volgograd Standard Time": "Europe/Volgograd", "W. Australia Standard Time": "Australia/Perth", "W. Central Africa Standard Time": "Africa/Lagos", "W. Europe Standard Time": "Europe/Berlin", "W. Mongolia Standard Time": "Asia/Hovd", "West Asia Standard Time": "Asia/Tashkent", "West Bank Standard Time": "Asia/Hebron", "West Pacific Standard Time": "Pacific/Port_Moresby", "Yakutsk Standard Time": "Asia/Yakutsk", "Yukon Standard Time": "America/Whitehorse", } collective-icalendar-138c845/src/icalendar/timezone/zoneinfo.py000066400000000000000000000141301524134033600246100ustar00rootroot00000000000000"""Use zoneinfo timezones""" from __future__ import annotations import copy import copyreg import functools import threading import zoneinfo from datetime import datetime, tzinfo from io import StringIO from typing import TYPE_CHECKING from dateutil.rrule import rrule, rruleset from dateutil.tz import tzical from dateutil.tz.tz import _tzicalvtz from icalendar.tools import is_date, to_datetime from .provider import TZProvider if TYPE_CHECKING: from icalendar import prop from icalendar.cal import Timezone from icalendar.prop import vDDDTypes class ZONEINFO(TZProvider): """Provide icalendar with timezones from zoneinfo.""" name = "zoneinfo" # Use lazy initialization via properties to avoid import-time failures # in environments without timezone data (e.g., Pyodide/WebAssembly). # See https://github.com/collective/icalendar/issues/1073 _utc: zoneinfo.ZoneInfo | None = None _available_timezones_cache: set | None = None # Class-level lock is intentional: caches are shared by all instances. _init_lock = threading.Lock() @property def utc(self) -> zoneinfo.ZoneInfo: """Return the UTC timezone, initializing lazily on first access.""" if self._utc is None: with self._init_lock: # Double-check after acquiring lock if self._utc is None: ZONEINFO._utc = zoneinfo.ZoneInfo("UTC") return self._utc # type: ignore[return-value] @property def _available_timezones(self) -> set: """Return available timezones, initializing lazily on first access.""" if self._available_timezones_cache is None: with self._init_lock: # Double-check after acquiring lock if self._available_timezones_cache is None: ZONEINFO._available_timezones_cache = zoneinfo.available_timezones() return self._available_timezones_cache # type: ignore[return-value] def localize(self, dt: datetime, tz: zoneinfo.ZoneInfo) -> datetime: """Localize a datetime to a timezone.""" return dt.replace(tzinfo=tz) def localize_utc(self, dt: datetime) -> datetime: """Return the datetime in UTC.""" if getattr(dt, "tzinfo", False) and dt.tzinfo is not None: return dt.astimezone(self.utc) return self.localize(dt, self.utc) def timezone(self, name: str) -> tzinfo | None: """Return a timezone with a name or None if we cannot find it.""" try: return zoneinfo.ZoneInfo(name) except zoneinfo.ZoneInfoNotFoundError: pass except ValueError: # ValueError: ZoneInfo keys may not be absolute paths, got: /Europe/CUSTOM pass def knows_timezone_id(self, tzid: str) -> bool: """Whether the timezone is already cached by the implementation.""" return tzid in self._available_timezones def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: """Make sure the until value works for the rrule generated from the ical_rrule.""" # noqa: E501 if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()): # zoninfo does not know any transition dates after 2038 rrule._until = datetime(2038, 12, 31, tzinfo=self.utc) # noqa: SLF001 def create_timezone(self, tz: Timezone.Timezone) -> tzinfo: """Create a timezone from the given information.""" try: return self._create_timezone(tz) except ValueError: # We might have a custom component in there. # see https://github.com/python/cpython/issues/120217 tz = copy.deepcopy(tz) for sub in tz.walk(): for attr in list(sub.keys()): if attr.lower().startswith("x-"): sub.pop(attr) for sub in tz.subcomponents: start: vDDDTypes = sub.get("DTSTART") if start and hasattr(start, "dt") and is_date(start.dt): # ValueError: Unsupported DTSTART param in VTIMEZONE: VALUE=DATE sub.DTSTART = to_datetime(start.dt) return self._create_timezone(tz) def _create_timezone(self, tz: Timezone.Timezone) -> tzinfo: """Create a timezone and maybe fail""" file = StringIO(tz.to_ical().decode("UTF-8", "replace")) return tzical(file).get() def uses_pytz(self) -> bool: """Whether we use pytz.""" return False def uses_zoneinfo(self) -> bool: """Whether we use zoneinfo.""" return True def pickle_tzicalvtz(tzicalvtz: _tzicalvtz): """Because we use dateutil.tzical, we need to make it pickle-able.""" return _tzicalvtz, (tzicalvtz._tzid, tzicalvtz._comps) # noqa: SLF001 copyreg.pickle(_tzicalvtz, pickle_tzicalvtz) def pickle_rrule_with_cache(self: rrule): """Make sure we can also pickle rrules that cache. This is mainly copied from rrule.replace. """ new_kwargs = { "interval": self._interval, "count": self._count, "dtstart": self._dtstart, "freq": self._freq, "until": self._until, "wkst": self._wkst, "cache": self._cache is not None, } new_kwargs.update(self._original_rule) # from https://stackoverflow.com/a/64915638/1320237 return functools.partial(rrule, new_kwargs.pop("freq"), **new_kwargs), () copyreg.pickle(rrule, pickle_rrule_with_cache) def pickle_rruleset_with_cache(rs: rruleset): """Pickle an rruleset.""" return unpickle_rruleset_with_cache, ( rs._rrule, # noqa: SLF001 rs._rdate, # noqa: SLF001 rs._exrule, # noqa: SLF001 rs._exdate, # noqa: SLF001 rs._cache is not None, # noqa: SLF001 ) def unpickle_rruleset_with_cache(rrule, rdate, exrule, exdate, cache): """unpickling the rruleset.""" rs = rruleset(cache) for o in rrule: rs.rrule(o) for o in rdate: rs.rdate(o) for o in exrule: rs.exrule(o) for o in exdate: rs.exdate(o) return rs copyreg.pickle(rruleset, pickle_rruleset_with_cache) __all__ = ["ZONEINFO"] collective-icalendar-138c845/src/icalendar/tools.py000066400000000000000000000102031524134033600222640ustar00rootroot00000000000000"""Utility functions for icalendar.""" from __future__ import annotations from datetime import date, datetime, tzinfo from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from icalendar.compatibility import TypeGuard, TypeIs def is_date(dt: date | datetime) -> bool: """Check if a value is a date but not a datetime. This function distinguishes between ``date`` and ``datetime`` objects, returning ``True`` only for pure ``date`` instances. Parameters: dt: The date or datetime object to check. Returns: ``True`` if the value is a ``date`` but not a ``datetime``, ``False`` otherwise. Example: .. code-block:: pycon >>> from datetime import date, datetime >>> from icalendar.tools import is_date >>> is_date(date(2024, 1, 15)) True >>> is_date(datetime(2024, 1, 15, 10, 30)) False """ return isinstance(dt, date) and not isinstance(dt, datetime) def is_datetime(dt: date | datetime) -> TypeIs[datetime]: """Check if a value is a datetime. Parameters: dt: The date or datetime object to check. Returns: ``True`` if the value is a ``datetime``, ``False`` if it is only a ``date``. Example: .. code-block:: pycon >>> from datetime import date, datetime >>> from icalendar.tools import is_datetime >>> is_datetime(datetime(2024, 1, 15, 10, 30)) True >>> is_datetime(date(2024, 1, 15)) False """ return isinstance(dt, datetime) def to_datetime(dt: date | datetime) -> datetime: """Convert a date to a datetime. If the input is already a ``datetime``, it is returned unchanged. If the input is a ``date``, it is converted to a ``datetime`` at midnight. Parameters: dt: The date or datetime to convert. Returns: A ``datetime`` object. If the input was a ``date``, the time component will be set to midnight (00:00:00). Example: .. code-block:: pycon >>> from datetime import date, datetime >>> from icalendar.tools import to_datetime >>> to_datetime(date(2024, 1, 15)) datetime.datetime(2024, 1, 15, 0, 0) >>> to_datetime(datetime(2024, 1, 15, 10, 30)) datetime.datetime(2024, 1, 15, 10, 30) """ if is_date(dt): return datetime(dt.year, dt.month, dt.day) # noqa: DTZ001 return cast("datetime", dt) def is_pytz(tz: tzinfo) -> bool: """Check if a timezone is a pytz timezone. pytz timezones require special handling with ``localize()`` and ``normalize()`` methods for correct timezone calculations. Parameters: tz: The timezone info object to check. Returns: ``True`` if the timezone is a pytz timezone (has a ``localize`` attribute), ``False`` otherwise. """ return hasattr(tz, "localize") def is_pytz_dt(dt: date | datetime) -> TypeGuard[datetime]: """Check if a datetime uses a pytz timezone. This function checks whether the datetime has a timezone attached and whether that timezone is a pytz timezone requiring special handling. Parameters: dt: The date or datetime object to check. Returns: ``True`` if the value is a ``datetime`` with a pytz timezone, ``False`` otherwise. """ return is_datetime(dt) and (tzinfo := dt.tzinfo) is not None and is_pytz(tzinfo) def normalize_pytz(dt: date | datetime) -> date | datetime: """Normalize a datetime after calculations when using pytz. pytz requires the ``normalize()`` function to be called after arithmetic operations to correctly adjust the timezone offset, especially around daylight saving time transitions. Parameters: dt: The date or datetime to normalize. Returns: The normalized datetime if it uses pytz, otherwise the input unchanged. """ if is_pytz_dt(dt): return dt.tzinfo.normalize(dt) # type: ignore[attr-defined] return dt __all__ = [ "is_date", "is_datetime", "is_pytz", "is_pytz_dt", "normalize_pytz", "to_datetime", ] collective-icalendar-138c845/src/icalendar/version.py000066400000000000000000000013031524134033600226120ustar00rootroot00000000000000"""Version information on icalendar. This module provides a stable interface to the generated :file:`_version.py` file. Version as a string: .. code-block:: pycon >>> from icalendar import version >>> version # doctest: +SKIP '7.0.0' Version as a tuple: .. code-block:: pycon >>> from icalendar import version_tuple >>> version_tuple # doctest: +SKIP (7, 0, 0) """ try: from ._version import __version__, __version_tuple__, version, version_tuple except ModuleNotFoundError: __version__ = version = "0.0.0dev0" __version_tuple__ = version_tuple = (0, 0, 0, "dev0") __all__ = [ "__version__", "__version_tuple__", "version", "version_tuple", ] collective-icalendar-138c845/tox.ini000066400000000000000000000023041524134033600173570ustar00rootroot00000000000000# to run for a specific environment, use ``tox -e ENVNAME`` [tox] envlist = py310,py311,py312,py313,py314,pypy3,docs,nopytz # Note: the 'docs' env creates a 'build' directory which may interfere in strange ways # with the other environments. You might see this when you run the tests in parallel. # See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269 [testenv] description = test environment runner = uv-venv-lock-runner dependency_groups = test commands = coverage run --branch --source=src/icalendar --omit=*/tests/hypothesis/* --omit=*/tests/fuzzed/* --module pytest [] coverage report coverage html coverage xml [testenv:nopytz] # install with dependencies usedevelop = False # use lowest version basepython = python3.10 commands = coverage run --branch --source=src/icalendar --omit=*/tests/hypothesis/* --omit=*/tests/fuzzed/* --module pytest [] coverage report coverage html coverage xml [testenv:docs] dependency_groups = docs allowlist_externals = make commands = make docs-all [testenv:ruff] # use tox -e ruff to format the code base deps = skip_install = True dependency_groups = formatting commands = ruff format ruff check --fix