├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── pre-commit.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yaml ├── ACKNOWLEDGEMENTS.txt ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE-2.0.txt ├── README.md ├── ROADMAP.md ├── programmers-guide ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── pyproject.toml ├── specs ├── rfc2425-mime-directory.txt ├── rfc2426-vcard-3.0.txt ├── rfc2445-icalendar-2.0.txt ├── rfc2739-calendar-attrs.txt ├── rfc4770-im-attrs.txt ├── rfc5545-icalendar-2.0-bis.txt ├── rfc6321-xcal.txt ├── rfc6350-vcard-4.0.txt ├── rfc6868-param-value-encoding.txt ├── rfc7095-jcard.txt ├── rfc7265-jcal.txt ├── rfc7529-non-greg-rrule.txt ├── rfc7953-availability.txt ├── rfc7986-new-ical-props.txt ├── rfc9074-valarm-ext.txt ├── rfc9253-ical-relationships.txt ├── vcalendar-10.pdf └── vcard-21.pdf ├── test_files ├── more_tests.txt ├── ms_tzid.ics ├── radicale-0816.ics ├── radicale-0827.ics ├── radicale-1587.vcf ├── ruby_rrule.ics ├── simple_test.ics ├── timezones.ics ├── tz_us_eastern.ics ├── tzid_8bit.ics ├── vobject_0050.ics └── vtodo.ics ├── tests ├── test_behaviors.py ├── test_calendar_serialization.py ├── test_change_tz.py ├── test_cli.py ├── test_compatibility.py ├── test_icalendar.py ├── test_vcards.py ├── test_vobject.py ├── test_vobject_parsing.py └── test_vtodo.py └── vobject ├── __init__.py ├── base.py ├── behavior.py ├── change_tz.py ├── hcalendar.py ├── icalendar.py ├── ics_diff.py └── vcard.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | *.pdf binary 4 | *.bat eol=crlf 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Quick Summary** 11 | The TL;DR of what went wrong. eg. "exception thrown parsing ics file" 12 | 13 | **Context** 14 | - vObject version 15 | - Python version 16 | - Operating system and version 17 | 18 | **Description** 19 | A clear, concise, and detailed description of the issue. 20 | 21 | **To Reproduce** 22 | A description of how to reproduce the issue, ideally including 23 | - a short Python script that demonstrates the problem 24 | - the iCalendar / vCard that triggers the issue 25 | 26 | **Further Notes** 27 | Anything else useful 28 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Code Lint Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | pre-commit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-python@v5.1.1 13 | with: 14 | python-version: 3.9 15 | cache: 'pip' 16 | 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pre-commit pylint 21 | 22 | - name: Run pre-commit 23 | env: 24 | SKIP: mypy,no-commit-to-branch 25 | run: pre-commit run --all-files 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | run-tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup Python ${{ matrix.python-version }} 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - name: Install Requirements 16 | run: | 17 | python -m pip install --upgrade flit pip wheel pytest 18 | pip install -e .['dev'] 19 | - name: Run Tests 20 | run: | 21 | pytest 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.log 3 | *.pyc 4 | .DS_Store 5 | build/**/* 6 | dist/**/* 7 | vobject.egg-info/**/* 8 | venv/**/* 9 | .idea/* 10 | *.sublime-* 11 | .coverage 12 | venv* 13 | programmers-guide/build 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/isort 3 | rev: 5.13.2 4 | hooks: 5 | - id: isort 6 | 7 | - repo: https://github.com/psf/black 8 | rev: 24.10.0 9 | hooks: 10 | - id: black 11 | 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v5.0.0 14 | hooks: 15 | - id: check-yaml 16 | - id: check-toml 17 | - id: end-of-file-fixer 18 | - id: no-commit-to-branch 19 | args: [ -b, main, -b, master ] 20 | 21 | - repo: https://github.com/PyCQA/flake8 22 | rev: 7.1.1 23 | hooks: 24 | - id: flake8 25 | additional_dependencies: [ flake8-pyproject ] 26 | 27 | - repo: local 28 | hooks: 29 | - id: pylint 30 | name: pylint 31 | entry: pylint 32 | language: system 33 | types: [ python ] 34 | args: 35 | [ 36 | "-rn", # Only display messages 37 | "-sn", # Don't display the score 38 | ] 39 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | jobs=2 3 | ignore-paths=(?!(vobject|tests))/* 4 | 5 | [FORMAT] 6 | max-line-length = 130 7 | 8 | [REFACTORING] 9 | max-args = 8 10 | max-attributes = 9 11 | max-bool-expr = 6 12 | max-branches = 32 13 | max-locals = 31 14 | max-module-lines = 1200 15 | max-nested-blocks = 6 16 | max-positional-arguments = 8 17 | max-statements = 95 18 | 19 | [MESSAGES CONTROL] 20 | disable= 21 | I, 22 | import-error, 23 | # W 24 | fixme, 25 | keyword-arg-before-vararg, 26 | logging-format-interpolation, 27 | protected-access, 28 | raise-missing-from, 29 | unspecified-encoding, 30 | unused-argument, 31 | # C 32 | import-outside-toplevel, 33 | invalid-name, 34 | missing-docstring, 35 | unidiomatic-typecheck, 36 | # R 37 | duplicate-code, 38 | no-else-return, 39 | too-few-public-methods, 40 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-lts-latest 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: programmers-guide/source/conf.py 16 | 17 | # declare the Python requirements required to build your documentation 18 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 19 | python: 20 | install: 21 | - method: pip 22 | path: . 23 | extra_requirements: 24 | - dev 25 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.txt: -------------------------------------------------------------------------------- 1 | Enormous thanks to: 2 | Jeffrey Harris, for his incredible work on the original package 3 | Tim Baxter, for all his work maintaining vobject over the past few years 4 | Adieu, for keeping things alive on github 5 | Kristian Glass, for his enormous help with testing and Python3 matters 6 | Gustavo Niemeyer, for all his work on dateutil 7 | Dave Cridland, for helping talk about vobject and working on vcard 8 | TJ Gabbour, for putting his heart into parsing 9 | Sameen Karim and Will Percival, for maintaining the package at Eventable. 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Python vObject Release Notes 2 | ============================ 3 | 4 | vobject 0.9.9 released 5 | -- 6 | 16 December 2024 7 | 8 | To install, use `pip install vobject`, or download the archive and 9 | untar, run `python setup.py install`. Tests can be run via `python 10 | setup.py test`. 11 | _dateutil_ and _six_ are required. 12 | Python 2.7 or higher is required. 13 | 14 | * Added product version to PRODID tag in iCalendar headers 15 | * Added support for GEO tags in vCards 16 | 17 | vobject 0.9.8 released 18 | -- 19 | 2 October 2024 20 | 21 | To install, use `pip install vobject`, or download the archive and 22 | untar, run `python setup.py install`. Tests can be run via `python 23 | setup.py test`. 24 | _dateutil_ and _six_ are required. 25 | Python 2.7 or higher is required. 26 | 27 | * Accumulated bug fixes 28 | 29 | vobject 0.9.7 released 30 | -- 31 | 24 February 2024 32 | 33 | To install, use `pip install vobject`, or download the archive and 34 | untar, run `python setup.py install`. Tests can be run via `python 35 | setup.py test`. 36 | _dateutil_ and _six_ are required. 37 | Python 2.7 or higher is required. 38 | 39 | * New repository: https://github.com/py-vobject/vobject 40 | * New website: https://py-object.github.io 41 | * New maintainers 42 | * Cosmetic release, switching to new maintenance organization. 43 | * This release is functionally identical to 0.9.6.1. 44 | 45 | vobject 0.9.6 released 46 | -- 47 | 7 July 2018 48 | 49 | To install, use `pip install vobject`, or download the archive and 50 | untar, run `python setup.py install`. Tests can be run via `python 51 | setup.py test`. 52 | _dateutil_ and _six_ are required. 53 | Python 2.7 or higher is required. 54 | 55 | * Correctly order calendar properties before calendar components 56 | * Correctly serialize timestamp values (i.e. `REV`) 57 | * Pass correct formatting string to logger 58 | * RRULE: Fix floating UNTIL with dateutil > 2.6.1 59 | * Encode params if necessary in serialization 60 | * Ignore escaped semi-colons in UNTIL value 61 | * RRULE: Fix VTODO without DTSTART 62 | * Fixed regexp for VCF Version 2.1 63 | * repr() changed for datetime.timedelta in python 3.7 64 | 65 | vobject 0.9.5 released 66 | -- 67 | 29 June 2017 68 | 69 | To install, use `pip install vobject`, or download the archive and 70 | untar, run `python setup.py install`. Tests can be run via `python 71 | setup.py test`. 72 | _dateutil_ and _six_ are required. 73 | Python 2.7 or higher is required. 74 | 75 | * Make `ics_diff.py` work with Python 3 76 | * Huge changes to text encoding for Python 2/3 compatibility 77 | * Autogenerate DTSTAMP if not provided 78 | * Fix `getrruleset()` for Python 3 and in the case that `addRDate=True` 79 | * Update vCard property validation to match specifications 80 | * Handle offset-naive and offset-aware datetimes in recurrence rules 81 | * Improved documentation for multi-value properties 82 | 83 | vobject 0.9.4.1 released 84 | -- 85 | 22 January 2017 86 | 87 | To install, use `pip install vobject`, or download the archive and 88 | untar, run `python setup.py install`. Tests can be run via `python 89 | setup.py test`. 90 | _dateutil_ and _six_ are required. 91 | Python 2.7 or higher is required. 92 | 93 | * Pickling/deepcopy hotfix 94 | 95 | vobject 0.9.4 released 96 | -- 97 | 20 January 2017 98 | 99 | To install, use `pip install vobject`, or download the archive and 100 | untar, run `python setup.py install`. Tests can be run via `python 101 | setup.py test`. 102 | _dateutil_ and _six_ are required. 103 | Python 2.7 or higher is required. 104 | 105 | * Improved PEP8 compliance 106 | * Improved Python 3 compatibility 107 | * Improved encoding/decoding 108 | * Correct handling of _pytz_ timezones 109 | 110 | vobject 0.9.3 released 111 | -- 112 | 26 August 2016 113 | 114 | To install, use `pip install vobject`, or download the archive and 115 | untar, run `python setup.py install`. Tests can be run via `python 116 | setup.py test`. 117 | _dateutil_ and _six_ are required. 118 | Python 2.7 or higher is required. 119 | 120 | * Fixed use of doc in `setup.py` for -OO mode 121 | * Added python3 compatibility for base64 encoding 122 | * Fixed ORG fields with multiple components 123 | * Handle _pytz_ timezones in iCalendar serialization 124 | * Use logging instead of printing to stdout 125 | 126 | vobject 0.9.2 released 127 | -- 128 | 13 March 2016 129 | 130 | To install, use `pip install vobject`, or download the archive and 131 | untar, run `python setup.py install`. Tests can be run via `python 132 | setup.py test`. 133 | _dateutil_ and _six_ are required. 134 | Python 2.7 or higher is required. 135 | 136 | * Better line folding for UTF-8 strings 137 | * Convert unicode to UTF-8 to be _StringIO_ compatible 138 | 139 | vobject 0.9.1 released 140 | -- 141 | 16 February 2016 142 | 143 | To install, use `pip install vobject`, or download the archive and 144 | untar, run `python setup.py install`. Tests can be run via `python 145 | setup.py test`. 146 | _dateutil_ and _six_ are required. 147 | Python 2.7 or higher is now required. 148 | 149 | * Removed lock on _dateutil_ version (>=2.4.0 now works) 150 | 151 | vobject 0.9.0 released 152 | -- 153 | 3 February 2016 154 | 155 | To install, use `pip install vobject`, or download the archive and 156 | untar, run `python setup.py install`. Tests can be run via `python 157 | setup.py test`. 158 | _dateutil 2.4.0_ and _six_ are required. 159 | Python 2.7 or higher is now required. 160 | 161 | * Python 3 compatible 162 | * Requires Python 2.7 or later (was Python 2.4) 163 | * New dependency on _six_ for Python 2/Python 3 compatibility 164 | * Updated version of _dateutil_ (2.4.0) 165 | * More comprehensive unit tests available in `tests.py` 166 | * Performance improvements in iteration 167 | * Test files are included in PyPI download package 168 | 169 | vobject 0.8.2 released 170 | -- 171 | 28 January 2016 172 | 173 | To install, use `pip install vobject`, or download the archive and 174 | untar, run `python setup.py install`. Tests can be run via `python 175 | setup.py test`. 176 | _dateutil 1.1_ or later is required. 177 | Python 2.4 is also required. 178 | 179 | * Removed unnecessary `ez_setup` call from `setup.py` 180 | * Moved source code repository to GitHub 181 | * New maintainer Sameen Karim 182 | 183 | vobject 0.8.1c released (SVN revision 217) 184 | -- 185 | 27 February 2009 186 | 187 | To install, use _easy_install_, or download the archive and untar, run 188 | `python setup.py install`. Tests can be run via `python setup.py test`. 189 | _dateutil 1.1_ or later is required. 190 | Python 2.4 or later is required. 191 | 192 | * Tweaked `change_tz.py` to keep it 2.4 compatible 193 | 194 | vobject 0.8.1b released (SVN revision 216) 195 | -- 196 | 12 January 2009 197 | 198 | To install, use _easy_install_, or download the archive and untar, run 199 | `python setup.py install`. Tests can be run via `python setup.py test`. 200 | _dateutil 1.1_ or later is required. 201 | Python 2.4 is also required. 202 | 203 | * Change behavior when import a VCALENDAR or VCARD with an older or 204 | absent VERSION line, now the most recent behavior (i.e., VCARD 3.0 205 | and iCalendar, VCALENDAR 2.0) is used 206 | 207 | vobject 0.8.0 released (SVN revision 213) 208 | -- 209 | 29 December 2008 210 | 211 | To install, use _easy_install_, or download the archive and untar, run 212 | `python setup.py install`. Tests can be run via `python setup.py test`. 213 | _dateutil 1.1_ or later is required. 214 | Python 2.4 is also required. 215 | 216 | * Changed license to Apache 2.0 from Apache 1.1 217 | * Fixed a major performance bug in backslash decoding large text bodies 218 | * Added workaround for strange Apple Address Book parsing of vcard PHOTO, 219 | don't wrap PHOTO by default. To disable this behavior, set 220 | `vobject.vcard.wacky_apple_photo_serialize` to `False`. 221 | 222 | vobject 0.7.1 released (SVN revision 208) 223 | -- 224 | 25 July 2008 225 | 226 | To install, use _easy_install_, or download the archive and untar, run 227 | `python setup.py install`. Tests can be run via `python setup.py test`. 228 | `_dateutil 1.1_` or later is required. Python 2.4 is also required. 229 | 230 | * Add `change_tz` script for converting timezones in iCalendar files 231 | 232 | vobject 0.7.0 released (SVN revision 206) 233 | -- 234 | 16 July 2008 235 | 236 | To install, use _easy_install_, or download the archive and untar, run 237 | `python setup.py install`. Tests can be run via `python setup.py test`. 238 | _dateutil 1.1_ or later is required. 239 | Python 2.4 is also required. 240 | 241 | * Allow Outlook's technically illegal use of commas in TZIDs 242 | * Added introspection help for IPython so tab completion works with 243 | vobject's custom __getattr__ 244 | * Made vobjects pickle-able 245 | * Added tolerance for the escaped semi-colons in RRULEs a Ruby iCalendar 246 | library generates 247 | * Fixed Bug 12245, setting an rrule from a dateutil instance missed 248 | BYMONTHDAY when the number used is negative 249 | 250 | vobject 0.6.6 released (SVN revision 201) 251 | -- 252 | 30 May 2008 253 | 254 | To install, use _easy_install_, or download the archive and untar, run 255 | `python setup.py install`. Tests can be run via `python setup.py test`. 256 | _dateutil 1.1_ or later is required. 257 | Python 2.4 is also required. 258 | 259 | * Fixed bug 12120, unicode TZIDs were failing to parse. 260 | 261 | vobject 0.6.5 released (SVN revision 200) 262 | -- 263 | 28 May 2008 264 | 265 | To install, use _easy_install_, or download the archive and untar, run 266 | `python setup.py install`. Tests can be run via `python setup.py test`. 267 | _dateutil 1.1_ or later is required. 268 | Python 2.4 is also required. 269 | 270 | * Fixed bug 9814, quoted-printable data wasn't being decoded into unicode, 271 | thanks to Ilpo Nyyssönen for the fix. 272 | * Fixed bug 12008, silently translate buggy Lotus Notes names with 273 | underscores into dashes. 274 | 275 | vobject 0.6.0 released (SVN revision 193) 276 | -- 277 | 21 February 2008 278 | 279 | To install, use _easy_install_, or download the archive and untar, run 280 | `python setup.py install`. 281 | _dateutil 1.1_ or later is required. 282 | Python 2.4 is also required. 283 | 284 | * Added VAVAILABILITY support, thanks to the Calendar Server team. 285 | * Improved unicode line folding. 286 | 287 | vobject 0.5.0 released (SVN revision 189) 288 | -- 289 | 14 January 2008 290 | 291 | To install, use _easy_install_, or download the archive and untar, run 292 | `python setup.py install`. 293 | _dateutil 1.1_ or later is required. 294 | Python 2.4 is also required. 295 | 296 | * Updated to more recent `ez_setup`, vobject wasn't successfully installing. 297 | 298 | vobject 0.4.9 released (SVN revision 187) 299 | -- 300 | 19 November 2007 301 | 302 | To install, use _easy_install_, or download the archive and untar, run 303 | `python setup.py install`. 304 | _dateutil 1.1_ or later is required. 305 | Python 2.4 is also required. 306 | 307 | * Tolerate invalid UNTIL values for recurring events 308 | * Minor improvements to logging and tracebacks 309 | * Fix serialization of zero-delta durations 310 | * Treat different tzinfo classes that represent UTC as equal 311 | * Added ORG behavior to vCard handling, native value for ORG is now a list. 312 | 313 | vobject 0.4.8 released (SVN revision 180) 314 | -- 315 | 7 January 2007 316 | 317 | To install, use _easy_install_, or download the archive and untar, run 318 | `python setup.py install`. 319 | _dateutil 1.1_ or later is required. 320 | Python 2.4 is also required. 321 | 322 | * Fixed problem with the UNTIL time used when creating a dateutil rruleset. 323 | 324 | vobject 0.4.7 released (SVN revision 172), hot on the heals of yesterday's 0.4.6 325 | -- 326 | 21 December 2006 327 | 328 | To install, use _easy_install_, or download the archive and untar, run 329 | `python setup.py install`. 330 | _dateutil 1.1_ or later is required. 331 | Python 2.4 is also required. 332 | 333 | * Fixed a problem causing DATE valued RDATEs and EXDATEs to be ignored 334 | when interpreting recurrence rules 335 | * And, from the short lived vobject 0.4.6, added an `ics_diff` module 336 | and an `ics_diff` command line script for comparing similar iCalendar 337 | files 338 | 339 | vobject 0.4.6 released (SVN revision 171) 340 | -- 341 | 20 December 2006 342 | 343 | To install, use _easy_install_, or download the archive and untar, run 344 | `python setup.py install`. 345 | _dateutil 1.1_ or later is required. 346 | Python 2.4 is also required. 347 | 348 | * Added an ics_diff module and an ics_diff command line script for 349 | comparing similar iCalendar files 350 | 351 | vobject 0.4.5 released (SVN revision 168) 352 | -- 353 | 8 December 2006 354 | 355 | To install, use _easy_install_, or download the archive and untar, run 356 | `python setup.py install`. 357 | _dateutil 1.1_ or later is required. 358 | Python 2.4 is also required. 359 | 360 | * Added ignoreUnreadable flag to readOne and readComponents 361 | * Tolerate date-time or date fields incorrectly failing to set VALUE=DATE 362 | for date values 363 | * Cause unrecognized lines to default to use a text behavior, so commas, 364 | carriage returns, and semi-colons are escaped properly in unrecognized 365 | lines 366 | 367 | vobject 0.4.4 released (SVN revision 159) 368 | -- 369 | 9 October 2006 370 | 371 | To install, use _easy_install_, or download the archive and untar, run 372 | `python setup.py install`. 373 | _dateutil 1.1_ or later is required. 374 | Python 2.4 is also required. 375 | 376 | * Merged in Apple CalendarServer patches as of CalendarServer-r191 377 | * Added copy and duplicate code to base module 378 | * Improved recurring VTODO handling 379 | * Save TZIDs when parsed and use them as back up TZIDs when serializing 380 | 381 | vobject 0.4.3 released (SVN revision 157) 382 | -- 383 | 22 September 2006 384 | 385 | To install, use _easy_install_, or download the archive and untar, run 386 | `python setup.py install`. 387 | _dateutil 0.9_ or later is required. 388 | Python 2.4 is also required. 389 | 390 | * Added support for PyTZ `tzinfo` classes. 391 | 392 | vobject 0.4.2 released (SVN revision 153) 393 | -- 394 | 29 August 2006 395 | 396 | To install, use _easy_install_, or download the archive and untar, run 397 | `python setup.py install`. 398 | _dateutil 0.9_ or later is required. 399 | Python 2.4 is also required. 400 | 401 | * Updated `ez_setup.py` to use the latest _setuptools_. 402 | 403 | vobject 0.4.1 released (SVN revision 152) 404 | -- 405 | 4 August 2006 406 | 407 | To install, use _easy_install_, or download the archive and untar, run 408 | `python setup.py install`. 409 | _dateutil 0.9_ or later is required. 410 | Python 2.4 is also required. 411 | 412 | * When vobject encounters ASCII, it now tries UTF-8, then UTF-16 with 413 | either LE or BE byte orders, searching for BEGIN in the decoded string 414 | to determine if it's found an encoding match. `readOne` and 415 | `readComponents` will no longer work on arbitrary Versit style ASCII 416 | streams unless the optional `findBegin` flag is set to `False` 417 | 418 | vobject 0.4.0 released (SVN revision 151) 419 | -- 420 | 2 August 2006 421 | 422 | To install, use _easy_install_, or download the archive and untar, run 423 | `python setup.py install`. 424 | _dateutil 0.9_ or later is required. 425 | Python 2.4 is also required. 426 | 427 | * Workarounds for common invalid files produced by Apple's iCal and AddressBook 428 | * Added getChildValue convenience method 429 | * Added experimental hCalendar serialization 430 | * Handle DATE valued EXDATE and RRULEs better 431 | 432 | vobject 0.3.0 released (SVN revision 129) 433 | -- 434 | 17 February 2006 435 | 436 | To install, untar the archive, run `python setup.py install`. 437 | _dateutil 0.9_ or later is required. 438 | Python 2.4 is also required. 439 | 440 | * Changed API for accessing children and parameters, attributes now 441 | return the first child or parameter, not a list. See usage for examples 442 | * Added support for groups, a vcard feature 443 | * Added behavior for FREEBUSY lines 444 | * Worked around problem with dateutil's treatment of experimental 445 | properties (bug 4978) 446 | * Fixed bug 4992, problem with rruleset when addRDate is set 447 | 448 | vobject 0.2.3 released (SVN revision 104) 449 | -- 450 | 9 January 2006 451 | 452 | To install, untar the archive, run `python setup.py install`. 453 | _dateutil 0.9_ or later is required. 454 | Python 2.4 is also required. 455 | 456 | * 457 | * Added VERSION line back into native iCalendar objects 458 | * Added a first stab at a vcard module, parsing of vCard 3.0 files now 459 | gives structured values for N and ADR properties 460 | * Fix bug in regular expression causing the '^' character to not parse 461 | 462 | vobject 0.2.2 released (SVN revision 101) 463 | -- 464 | 4 November 2005 465 | 466 | To install, untar the archive, run `python setup.py install`. 467 | _dateutil 0.9_ or later is required. 468 | Python 2.4 is also required. 469 | 470 | * Fixed problem with add('duration') 471 | * Fixed serialization of EXDATEs which are dates or have floating timezone 472 | * Fixed problem serializing timezones with no daylight savings time 473 | 474 | vobject 0.2.0 released (SVN revision 97) 475 | -- 476 | 10 October 2005 477 | 478 | To install, untar the archive, run `python setup.py install`. 479 | _dateutil 0.9_ or later is required. 480 | Python 2.4 is also required. 481 | 482 | * Added serialization of arbitrary tzinfo classes as VTIMEZONEs 483 | * Removed unused methods 484 | * Changed getLogicalLines to use regular expressions, dramatically 485 | speeding it up 486 | * Changed rruleset behavior to use a property for rruleset 487 | 488 | vobject 0.1.4 released (SVN revision 93) 489 | -- 490 | 30 September 2005 491 | 492 | To install, untar the archive, run `python setup.py install`. 493 | _dateutil 0.9_ or later is required. 494 | Python 2.4 is also required. 495 | 496 | * Changed parseLine to use regular expression instead of a state 497 | machine, reducing parse time dramatically 498 | 499 | vobject 0.1.3 released (SVN revision 88) 500 | -- 501 | 1 July 2005 502 | 503 | To install, untar the archive, run `python setup.py install`. 504 | _dateutil 0.9_ or later is required. 505 | 506 | * As of this release, Python 2.4 is required. 507 | * Added license and acknowledgements. 508 | * Fixed the fact that defaultSerialize wasn't escaping linefeeds 509 | * Updated backslashEscape to encode CRLF's and bare CR's as linefeeds, 510 | which seems to be what RFC2445 requires 511 | 512 | vobject 0.1.2 released (SVN revision 83) 513 | -- 514 | 24 March 2005 515 | 516 | To install, untar the archive, run `python setup.py install`. 517 | _dateutil_ is required. 518 | 519 | * You'll need to apply this patch to be able to read certain VTIMEZONEs 520 | exported by Apple iCal, or if you happen to be in Europe! 521 | 522 | patch -R $PYTHONLIB/site-packages/dateutil/tz.py dateutil-0.5-tzoffset-bug.patch 523 | 524 | * Fixed printing of non-ascii unicode. 525 | * Fixed bug preventing content lines with empty contents from parsing. 526 | 527 | vobject 0.1.1 released (SVN revision 82) 528 | -- 529 | 25 January 2005 530 | 531 | To install, untar the archive, run `python setup.py install`. 532 | `_dateutil_` is required. 533 | 534 | * You'll need to apply this patch to be able to read certain VTIMEZONEs 535 | exported by Apple iCal, or if you happen to be in Europe! 536 | 537 | patch -R $PYTHONLIB/site-packages/dateutil/tz.py dateutil-0.5-tzoffset-bug.patch 538 | 539 | * Various bug fixes involving recurrence. 540 | * TRIGGER and VALARM behaviors set up. 541 | 542 | vobject 0.1 released (SVN revision 70) 543 | -- 544 | 13 December 2004 545 | 546 | * Parsing all iCalendar files should be working, please file a bug if 547 | you can't read one! 548 | * Timezones can be set for datetimes, but currently they'll be converted 549 | to UTC for serializing, because VTIMEZONE serialization isn't yet 550 | working. 551 | * RRULEs can be parsed, but when they're serialized, they'll be 552 | converted to a maximum of 500 RDATEs, because RRULE serialization 553 | isn't yet working. 554 | * To parse unicode, see issue 4. 555 | * Much more testing is needed, of course! 556 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to vObject 2 | ======================= 3 | 4 | Welcome, and thanks for considering contributing to vObject! 5 | 6 | **_This document is an incomplete draft_** 7 | 8 | Contributions can take many forms, from coding major features, through 9 | triaging issues, writing documentation, assisting users, etc, etc. You 10 | can, of course, just dive right in, but it's generally a good idea to 11 | open an issue (if the contribution addresses a problem) or a discussion 12 | to discuss your plans first. This avoids duplicate effort, and builds 13 | the community of contributors. 14 | 15 | In all interactions, contributors should be polite, kind, and respectful 16 | of others. Remember that not everyone lives in the same country, speaks 17 | English as their native language, or has the same level of experience and 18 | confidence. 19 | 20 | Python Code 21 | ----------- 22 | vObject is licensed under the Apache 2.0 License, and any code or 23 | documentation can only be accepted under those terms. You do _not_ need 24 | a formal statement of origin, and are not required to sign over your 25 | copyright. 26 | 27 | All new code should adhere to the PEP-8 conventions, with some exceptions 28 | possible when extending older APIs for consistency with what's already 29 | there. 30 | 31 | The supported Python versions are discussed in #1, and care needs to be 32 | taken to not use features that are unavailable in the older supported 33 | releases. 34 | 35 | With the possible exception of major releases, all contributions must 36 | maintain the existing API's syntax and semantics. 37 | 38 | Dev Setup 39 | - 40 | 41 | 1. Enable pre-commit hook and run manually. 42 | ``` 43 | pre-commit install 44 | git add . && pre-commit run 45 | ``` 46 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VObject 2 | 3 | [![PyPI version](https://badge.fury.io/py/vobject.svg)](https://pypi.python.org/pypi/vobject) 4 | [![PyPI downloads](https://img.shields.io/pypi/dm/vobject.svg)](https://pypi.python.org/pypi/vobject) 5 | [![Build](https://github.com/py-vobject/vobject/actions/workflows/test.yml/badge.svg)](https://github.com/py-vobject/vobject/actions/workflows/test.yml) 6 | [![License](https://img.shields.io/pypi/l/vobject.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) 7 | 8 | VObject is intended to be a full-featured Python package for parsing and 9 | generating Card and Calendar files. 10 | 11 | Currently, iCalendar files are supported and well tested. vCard 3.0 files are 12 | supported, and all data should be imported, but only a few components are 13 | understood in a sophisticated way. 14 | 15 | The [Calendar Server](http://calendarserver.org/) team has added VAVAILABILITY 16 | support to VObject's iCalendar parsing. 17 | 18 | There are two series of releases: 19 | 20 | * Versions 0.9.x continue to support Python 2.7. This branch is maintained only for backward compatibility, and does not 21 | get new features. 22 | * Versions 1.x support only Python 3.8 or later, and is the focus of ongoing feature development. 23 | 24 | Please report bugs and issues directly on [GitHub](https://github.com/py-vobject/vobject/issues). 25 | 26 | VObject is licensed under the [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0). 27 | 28 | Useful scripts included with VObject: 29 | 30 | * [ics_diff](https://github.com/py-vobject/vobject/blob/master/vobject/ics_diff.py): order is irrelevant in iCalendar 31 | files, return a diff of meaningful changes between icalendar files 32 | * [change_tz](https://github.com/py-vobject/vobject/blob/master/vobject/change_tz.py): Take an iCalendar file with 33 | events in the wrong timezone, change all events or just UTC events into one of the timezones **pytz** supports. 34 | Requires [pytz](https://pypi.python.org/pypi/pytz/). 35 | 36 | # History 37 | 38 | VObject was originally developed in concert with the Open Source Application Foundation's _Chandler_ project by 39 | Jeffrey Harris. Maintenance was later passed to [Sameen Karim](https://github.com/skarim) and 40 | [Will Percival](https://github.com/wpercy) at [Eventable](https://github.com/eventable). 41 | After several years of inactivity, the project was revived under a dedicated GitHub organization, with new volunteers. 42 | 43 | **Please note**: the original repository at [eventable/vobject](https://github.com/eventable/vobject/) is 44 | _unmaintained_. This project forked the latest code from that repository, after attempts to revive the existing project 45 | with new maintainers were unsuccessful. 46 | 47 | Many thanks to [all the contributors](https://github.com/py-vobject/vobject/blob/master/ACKNOWLEDGEMENTS.txt) for their 48 | dedication and support. 49 | 50 | # Installation 51 | 52 | To install with [pip](https://pypi.python.org/pypi/pip), run: 53 | 54 | ``` 55 | pip install vobject 56 | ``` 57 | 58 | Or download the package and run: 59 | 60 | ``` 61 | python setup.py install 62 | ``` 63 | 64 | VObject requires Python 2.7 or higher, along with the [dateutil](https://pypi.python.org/pypi/python-dateutil/) 65 | and [six](https://pypi.python.org/pypi/six) packages. 66 | 67 | # Running tests 68 | 69 | To run all tests, use: 70 | 71 | ``` 72 | python tests.py 73 | ``` 74 | 75 | # Usage 76 | 77 | ## iCalendar 78 | 79 | #### Creating iCalendar objects 80 | 81 | VObject has a basic datastructure for working with iCalendar-like syntaxes. Additionally, it defines specialized 82 | behaviors for many of the commonly used iCalendar objects. 83 | 84 | To create an object that already has a behavior defined, run: 85 | 86 | ``` 87 | >>> import vobject 88 | >>> cal = vobject.newFromBehavior('vcalendar') 89 | >>> cal.behavior 90 | 91 | ``` 92 | 93 | Convenience functions exist to create iCalendar and vCard objects: 94 | 95 | ``` 96 | >>> cal = vobject.iCalendar() 97 | >>> cal.behavior 98 | 99 | >>> card = vobject.vCard() 100 | >>> card.behavior 101 | 102 | ``` 103 | 104 | Once you have an object, you can use the add method to create 105 | children: 106 | 107 | ``` 108 | >>> cal.add('vevent') 109 | 110 | >>> cal.vevent.add('summary').value = "This is a note" 111 | >>> cal.prettyPrint() 112 | VCALENDAR 113 | VEVENT 114 | SUMMARY: This is a note 115 | ``` 116 | 117 | Note that summary is a little different from vevent, it's a ContentLine, not a Component. It can't have children, and it 118 | has a special value attribute. 119 | 120 | ContentLines can also have parameters. They can be accessed with regular attribute names with _param appended: 121 | 122 | ``` 123 | >>> cal.vevent.summary.x_random_param = 'Random parameter' 124 | >>> cal.prettyPrint() 125 | VCALENDAR 126 | VEVENT 127 | SUMMARY: This is a note 128 | params for SUMMARY: 129 | X-RANDOM ['Random parameter'] 130 | ``` 131 | 132 | There are a few things to note about this example 133 | 134 | * The underscore in x_random is converted to a dash (dashes are legal in iCalendar, underscores legal in Python) 135 | * X-RANDOM's value is a list. 136 | 137 | If you want to access the full list of parameters, not just the first, 138 | use <paramname>_paramlist: 139 | 140 | ``` 141 | >>> cal.vevent.summary.x_random_paramlist 142 | ['Random parameter'] 143 | >>> cal.vevent.summary.x_random_paramlist.append('Other param') 144 | >>> cal.vevent.summary 145 | 146 | ``` 147 | 148 | Similar to parameters, If you want to access more than just the first child of a Component, you can access the full list 149 | of children of a given name by appending _list to the attribute name: 150 | 151 | ``` 152 | >>> cal.add('vevent').add('summary').value = "Second VEVENT" 153 | >>> for ev in cal.vevent_list: 154 | ... print ev.summary.value 155 | This is a note 156 | Second VEVENT 157 | ``` 158 | 159 | The interaction between the del operator and the hiding of the 160 | underlying list is a little tricky, del cal.vevent and del 161 | cal.vevent_list both delete all vevent children: 162 | 163 | ``` 164 | >>> first_ev = cal.vevent 165 | >>> del cal.vevent 166 | >>> cal 167 | 168 | >>> cal.vevent = first_ev 169 | ``` 170 | 171 | VObject understands Python's datetime module and tzinfo classes. 172 | 173 | ``` 174 | >>> import datetime 175 | >>> utc = vobject.icalendar.utc 176 | >>> start = cal.vevent.add('dtstart') 177 | >>> start.value = datetime.datetime(2006, 2, 16, tzinfo = utc) 178 | >>> first_ev.prettyPrint() 179 | VEVENT 180 | DTSTART: 2006-02-16 00:00:00+00:00 181 | SUMMARY: This is a note 182 | params for SUMMARY: 183 | X-RANDOM ['Random parameter', 'Other param'] 184 | ``` 185 | 186 | Components and ContentLines have serialize methods: 187 | 188 | ``` 189 | >>> cal.vevent.add('uid').value = 'Sample UID' 190 | >>> icalstream = cal.serialize() 191 | >>> print icalstream 192 | BEGIN:VCALENDAR 193 | VERSION:2.0 194 | PRODID:-//PYVOBJECT//NONSGML Version 1//EN 195 | BEGIN:VEVENT 196 | UID:Sample UID 197 | DTSTART:20060216T000000Z 198 | SUMMARY;X-RANDOM=Random parameter,Other param:This is a note 199 | END:VEVENT 200 | END:VCALENDAR 201 | ``` 202 | 203 | Observe that serializing adds missing required lines like version and 204 | prodid. A random UID would be generated, too, if one didn't exist. 205 | 206 | If dtstart's tzinfo had been something other than UTC, an appropriate 207 | vtimezone would be created for it. 208 | 209 | #### Parsing iCalendar objects 210 | 211 | To parse one top level component from an existing iCalendar stream or 212 | string, use the readOne function: 213 | 214 | ``` 215 | >>> parsedCal = vobject.readOne(icalstream) 216 | >>> parsedCal.vevent.dtstart.value 217 | datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) 218 | ``` 219 | 220 | Similarly, readComponents is a generator yielding one top level component at a time from a stream or string. 221 | 222 | ``` 223 | >>> vobject.readComponents(icalstream).next().vevent.dtstart.value 224 | datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) 225 | ``` 226 | 227 | More examples can be found in source code doctests. 228 | 229 | ## vCards 230 | 231 | #### Creating vCard objects 232 | 233 | Making vCards proceeds in much the same way. Note that the 'N' and 'FN' 234 | attributes are required. 235 | 236 | ``` 237 | >>> j = vobject.vCard() 238 | >>> j.add('n') 239 | 240 | >>> j.n.value = vobject.vcard.Name( family='Harris', given='Jeffrey' ) 241 | >>> j.add('fn') 242 | 243 | >>> j.fn.value ='Jeffrey Harris' 244 | >>> j.add('email') 245 | 246 | >>> j.email.value = 'jeffrey@osafoundation.org' 247 | >>> j.email.type_param = 'INTERNET' 248 | >>> j.add('org') 249 | 250 | >>> j.org.value = ['Open Source Applications Foundation'] 251 | >>> j.prettyPrint() 252 | VCARD 253 | ORG: ['Open Source Applications Foundation'] 254 | EMAIL: jeffrey@osafoundation.org 255 | params for EMAIL: 256 | TYPE ['INTERNET'] 257 | FN: Jeffrey Harris 258 | N: Jeffrey Harris 259 | ``` 260 | 261 | serializing will add any required computable attributes (like 'VERSION') 262 | 263 | ``` 264 | >>> j.serialize() 265 | 'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nORG:Open Source Applications Foundation\r\nEND:VCARD\r\n' 266 | >>> j.prettyPrint() 267 | VCARD 268 | ORG: Open Source Applications Foundation 269 | VERSION: 3.0 270 | EMAIL: jeffrey@osafoundation.org 271 | params for EMAIL: 272 | TYPE ['INTERNET'] 273 | FN: Jeffrey Harris 274 | N: Jeffrey Harris 275 | ``` 276 | 277 | #### Parsing vCard objects 278 | 279 | ``` 280 | >>> s = """ 281 | ... BEGIN:VCARD 282 | ... VERSION:3.0 283 | ... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org 284 | ... EMAIL;TYPE=INTERNET:jeffery@example.org 285 | ... ORG:Open Source Applications Foundation 286 | ... FN:Jeffrey Harris 287 | ... N:Harris;Jeffrey;;; 288 | ... END:VCARD 289 | ... """ 290 | >>> v = vobject.readOne( s ) 291 | >>> v.prettyPrint() 292 | VCARD 293 | ORG: Open Source Applications Foundation 294 | VERSION: 3.0 295 | EMAIL: jeffrey@osafoundation.org 296 | params for EMAIL: 297 | TYPE [u'INTERNET'] 298 | FN: Jeffrey Harris 299 | N: Jeffrey Harris 300 | >>> v.n.value.family 301 | u'Harris' 302 | >>> v.email_list 303 | [, 304 | ] 305 | ``` 306 | 307 | Just like with iCalendar example above readComponents will yield a generator from a stream or string containing multiple 308 | vCards objects. 309 | 310 | ``` 311 | >>> vobject.readComponents(vCardStream).next().email.value 312 | 'jeffrey@osafoundation.org' 313 | ``` 314 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | Roadmap 2 | ======= 3 | 4 | Immediate tasks, starting early 2024 5 | 6 | - [ ] Apply all open good PRs 7 | - Add unit test coverage where missing 8 | - Add comments to _eventable_ repo, so people can see they're being 9 | fixed in the _py-vobject_ project 10 | - [ ] Do a pass through the open issues at _eventable_ 11 | - Fix anything easy 12 | - Copy the issue over to _py-vobject_ for bigger items that can't be 13 | fixed right away 14 | - [ ] Renumber _master_ for 1.0.x 15 | - And rename to `main` while we're here? 16 | - [ ] Set up GitHub issue triage, etc 17 | - Group members and permissions 18 | - Labels 19 | - Templates 20 | - Pinned discussions posts 21 | - Revamped README 22 | - CoC? 23 | - [ ] Talk to downstream users about pain-points 24 | - Beyond just lack of maintenance 25 | - eg. Radicale, Debian 26 | 27 | ### Bigger projects 28 | 29 | These should be prioritised once the basic maintenance and revamping work 30 | has been completed. 31 | 32 | - [ ] Create new Sphinx-based programmer's guide document 33 | - Publish via readthedocs 34 | - Move example code out of README.md 35 | - Publish automagically via GitHub Actions 36 | - [ ] Begin removal of 2.x code 37 | - In particular, clean up `bytes` vs `str` everywhere 38 | - Remove `six` 39 | - Remove various `import` compatibility hacks 40 | - [ ] Robust vCard 4.0 support 41 | - [ ] Parsing performance 42 | - [ ] Unit-test coverage 43 | -------------------------------------------------------------------------------- /programmers-guide/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /programmers-guide/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /programmers-guide/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # For the full list of built-in configuration values, see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Project information ----------------------------------------------------- 8 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 9 | 10 | import datetime 11 | 12 | import vobject 13 | 14 | project = "Python vObject" 15 | copyright = f"© {datetime.datetime.now().year}, David Arnold" 16 | author = "David Arnold" 17 | release = vobject.VERSION 18 | 19 | # -- General configuration --------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | extensions = [] 23 | 24 | templates_path = ["_templates"] 25 | exclude_patterns = [] 26 | 27 | 28 | # -- Options for HTML output ------------------------------------------------- 29 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 30 | 31 | html_theme = "alabaster" 32 | 33 | html_theme_options = { 34 | "github_user": "py-vobject", 35 | "github_repo": "vobject", 36 | "github_type": "star", 37 | "github_button": "true", 38 | "github_count": "true", 39 | } 40 | 41 | html_static_path = ["_static"] 42 | -------------------------------------------------------------------------------- /programmers-guide/source/index.rst: -------------------------------------------------------------------------------- 1 | .. vobject programmers guide 2 | Copyright (C) 2024, David Arnold 3 | 4 | vObject 5 | ======= 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | :caption: Contents: 10 | 11 | Introduction 12 | ============ 13 | `vobject` is a pure-Python package for generating and parsing *vCard* 14 | and *iCalendar* (aka *vCalendar*) objects, used for sharing and storage 15 | of personal contacts and calendar events. 16 | 17 | It supports Python 3.8 or later. Releases in the 0.9.x series support 18 | Python 2.7, and earlier releases of Python 3. 19 | 20 | This document gives an overview of the *vCard* and *iCalendar* standards, 21 | sufficient to begin using the package to generate or parse those that you 22 | will likely encounter in general use. And it explains the API, with 23 | examples of common tasks. 24 | 25 | 26 | .. ##################################################################### 27 | 28 | Quick Start 29 | =========== 30 | 31 | Install ``vobject`` from PyPI using ``pip``, usually into a suitable 32 | virtual environment: 33 | 34 | .. code-block:: sh 35 | :linenos: 36 | 37 | pip install vobject 38 | 39 | In all code examples in this chapter, we assume that the package has 40 | been imported like this: 41 | 42 | .. code-block:: python 43 | :linenos: 44 | 45 | import vobject 46 | 47 | You can parse an existing contact (typically ``.vcf``) or calendar 48 | (typically ``.ics``) file, and get an iterator to the contained 49 | objects: 50 | 51 | .. code-block:: python 52 | :linenos: 53 | 54 | with open("my-cards-file.vcf") as vo_stream: 55 | for vo in vobject.readComponents(vo_stream): 56 | vo.prettyPrint() 57 | 58 | If you only want to read a single object, you can use ``readOne()`` 59 | rather than ``readComponents()``. 60 | 61 | Given a Python instance of a vObject, you can then perform many 62 | operations on it. 63 | 64 | You can get its name: 65 | 66 | .. code-block:: python 67 | :linenos: 68 | 69 | >>> print(item.name) 70 | VCARD 71 | >>> 72 | 73 | Get its children (if any): 74 | 75 | .. code-block:: python 76 | :linenos: 77 | 78 | >>> list(item.getChildren()) 79 | 80 | or get its children in sorted order (alphabetic, except for the required 81 | children in the standard-specified order): 82 | 83 | .. code-block:: python 84 | :linenos: 85 | 86 | >>> list(item.getSortedChildren()) 87 | 88 | When there are no children, an empty list is returned. 89 | 90 | A component's children can be accessed through these generators (as 91 | above), or using a function: 92 | 93 | .. code-block:: python 94 | :linenos: 95 | 96 | >>> print(item.getChildValue("version") 97 | 3.0 98 | >>> 99 | 100 | Or using the ``contents`` dictionary: 101 | 102 | .. code-block:: python 103 | :linenos: 104 | 105 | >>> print(item.contents["version"][0].value) 106 | 3.0 107 | >>> 108 | 109 | Note two things when accessing the item's properties via the ``contents`` 110 | dictionary: first, the dictionary is a collection is *lists*, so even 111 | singleton property values need to access the first list element, and 112 | second, the object in the list is a class that holds the actual value, 113 | accessed using the ``value`` attribute (there are other attributes of 114 | the value available from that class instance as well). 115 | 116 | Or using their names to access them directly as attributes: 117 | 118 | .. code-block:: python 119 | :linenos: 120 | 121 | >>> print(item.version.value) 122 | 3.0 123 | >>> 124 | 125 | When accessed as named attributes of the parsed item, singleton 126 | properties *aren't* a list: you can access their value directly. If 127 | the child has parameters, in addition to its value, they are available 128 | as a dictionary: 129 | 130 | .. code-block:: python 131 | :linenos: 132 | 133 | >>> print(item.contents["adr"][0].params) 134 | {'TYPE': ['WORK', 'pref']} 135 | >>> 136 | 137 | Some values are structured types: names (NAME) and addresses (ADR) in 138 | vCards, for example: 139 | 140 | .. code-block:: python 141 | :linenos: 142 | 143 | >>> address = item.contents["adr"][0].value 144 | >>> print(address.street) 145 | 42 Main Street 146 | >>> print(address.country) 147 | USA 148 | >>> 149 | 150 | vObjects can be created by parsing (as above), or by using a helper 151 | function: 152 | 153 | .. code-block:: python 154 | :linenos: 155 | 156 | >>> my_card = vobject.vCard() 157 | 158 | or using the registry of known component types: 159 | 160 | .. code-block:: python 161 | :linenos: 162 | 163 | >>> my_todo = vobject.newFromBehavior("vtodo") 164 | 165 | Having created, and then populated a vobject as required, you can 166 | generate its serialized string format: 167 | 168 | .. code-block:: python 169 | :linenos: 170 | 171 | >>> entry = vobject.newFromBehavior("vjournal") 172 | >>> entry.add("summary").value = "Summary" 173 | >>> entry.add("description").value = "The whole description" 174 | >>> 175 | >>> entry.serialize() 176 | 'BEGIN:VJOURNAL\r\nDESCRIPTION:The whole description\r\n' 177 | 'DTSTAMP:20240331T013220Z\r\nSUMMARY:Summary\r\n' 178 | 'UID:20240331T015748Z - 66283@laptop.local\r\nEND:VJOURNAL\r\n' 179 | >>> 180 | 181 | (the serialized value has been split over several lines for clarity: it 182 | is a single string, shown on a single line, in the original interpreter 183 | output). 184 | 185 | Note that ``vobject`` has added the mandatory ``UID`` and ``DTSTAMP`` 186 | components during serialization. 187 | 188 | 189 | .. ##################################################################### 190 | 191 | Installing 192 | ========== 193 | 194 | ``vobject`` is distributed via PyPI_ or from GitHub_. For most people, 195 | using ``pip`` and PyPI is easiest and best way to install, but other 196 | options and reasons to use them are discussed in this chapter. 197 | 198 | PyPI 199 | ---- 200 | You can install ``vobject`` using ``pip``: 201 | 202 | .. code-block:: shell 203 | 204 | $ pip install vobject 205 | 206 | It's usually a good idea to install ``vobject`` into a virtual 207 | environment, to avoid issues with incompatible versions and system-wide 208 | packaging schemes. 209 | 210 | Installing using ``pip`` this way will also install ``vobject``'s 211 | runtime dependencies, so it should be immediately ready for use. 212 | 213 | Other Options 214 | ------------- 215 | ``vobject`` is distributed as a universal *wheel*, and should install 216 | from PyPI using ``pip`` without difficulty in most cases. There is 217 | also an *sdist* available from PyPI and GitHub which can be used as a 218 | fallback. 219 | 220 | If your development environment cannot access PyPI for some reason, 221 | then downloading the *wheel* (or *sdist*) from a machine with Internet 222 | access, and transferring to your development environment would make 223 | sense. 224 | 225 | Alternatively, you can clone the source repository from GitHub using 226 | git_. If you're intending to modify ``vobject`` and potentially 227 | contribute those changes back to the project, you should use the 228 | ``git clone`` method. 229 | 230 | Finally, the latest package source code for any release can be 231 | downloaded from GitHub as either a ``tar`` or ``zip`` file. This 232 | is probably not useful in the majority of cases, but it might be 233 | useful to archive the source code for auditing or similar purposes. 234 | 235 | Installing a wheel 236 | ~~~~~~~~~~~~~~~~~~ 237 | If you've downloaded a *wheel* file (it should have a ``.whl`` suffix), 238 | you can install it using ``pip`` as well: 239 | 240 | .. code-block:: shell 241 | 242 | $ pip install .whl 243 | 244 | Using this method, ``pip`` will automatically try to satisfy the runtime 245 | dependencies by downloading their wheels in turn, unless they're already 246 | available in the target (virtual) environment. But it will work fine 247 | without access to PyPI if the required dependencies are already 248 | installed. 249 | 250 | Installing an sdist (source distribution) 251 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 252 | A Python *sdist* (source distribution) is a tar file (with extension 253 | ``.tar.gz``) produced as part of making a Python package. It is very 254 | easily confused with the source *code* distribution, because the names 255 | are basically identical, and both can have the same file extension. 256 | 257 | *sdist* files can be downloaded from PyPI or from GitHub, and 258 | will have a name like ``vobject-0.9.7.tar.gz``. At GitHub, this file 259 | is listed with that filename under the release assets and **NOT** as 260 | the link called *"Source code (tar.gz)"*. 261 | 262 | You can download the *sdist* manually, and then install it with ``pip``: 263 | 264 | .. code-block:: shell 265 | 266 | $ pip install .tar.gz 267 | 268 | Installing from cloned source 269 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 270 | If you want to use a cloned source repository, likely the best way to 271 | install ``vobject`` is to use ``pip``'s editable install mechanism. 272 | 273 | First, activate the virtual environment to be used for development, 274 | and install using ``pip`` like: 275 | 276 | .. code-block:: 277 | 278 | $ git clone git://github.com/py-vobject/vobject.git 279 | $ cd vobject 280 | $ pip install -e . 281 | 282 | This will collect, build, and install the dependencies (from PyPI) and 283 | then install an *editable* version of the ``vobject`` package (that's 284 | the ``-e`` parameter). An editable install directly uses the files in 285 | your checkout area, including any local modifications you've made at 286 | the time you imported the package. 287 | 288 | 289 | .. ##################################################################### 290 | 291 | Importing 292 | ========= 293 | 294 | You can import the ``vobject`` module maintaining its internal structure, 295 | or you can import some or all bindings directly. 296 | 297 | .. index:: vobject, vCard 298 | .. code-block:: python 299 | :linenos: 300 | 301 | import vobject 302 | 303 | card = vobject.vCard() 304 | 305 | 306 | Or 307 | 308 | .. code-block:: python 309 | :linenos: 310 | 311 | from vobject import * 312 | 313 | card = vCard() 314 | 315 | 316 | All the example code in this document will use the first form, which is 317 | strongly recommended. 318 | 319 | .. index:: import, namespace 320 | 321 | Note that the ``import *`` form is explicitly supported, with the 322 | exposed namespace controlled to contain only the public features 323 | of the package. 324 | 325 | 326 | .. ##################################################################### 327 | 328 | Calendars and Cards 329 | =================== 330 | 331 | .. index:: Apple, AT&T, IBM, IETF, Internet Mail Consortium, Lucent, Siemens, Versit Consortium 332 | .. index:: vCard, vCalendar, iCalendar 333 | 334 | The *vCard* and *vCalendar* specifications were originally developed 335 | by the Versit Consortium (and that's why they're named with a leading 336 | "v"), with contributions from Apple, IBM, AT&T/Lucent, and Siemens. 337 | Their stewardship subsequently passed to the Internet Mail Consortium, 338 | before the standards were later adopted by the IETF. 339 | 340 | Within the IETF, vCard versions 3.0 and 4.0, and a revised and expanded 341 | calendar specification renamed to *iCalendar* 2.0 have been published, 342 | together with numerous extensions, clarifications, and related standards. 343 | 344 | The two main standards (vCard and iCalendar) form a way to store and 345 | communicate information about contacts (names, addresses, phone numbers, 346 | etc), and scheduling (events, todos, etc). They have been widely 347 | adopted for both storage and sharing of contact and calendar data. 348 | 349 | vCard 350 | ----- 351 | 352 | .. index:: RFC-2426, RFC-6350 353 | 354 | vCard 2.1 was the final version published by Versit, and is still widely 355 | used. vCard 3.0, published as IETF RFC-2426, is also widely used, however 356 | both versions are frequently extended by vendors or have implementation 357 | quirks in part due to unclear specification. vCard 4.0 (IETF RFC-6350) 358 | attempted to resolve the issues of earlier versions, but in doing so 359 | necessarily broke some backward compatibility, and as a result, it has 360 | not been universally adopted. 361 | 362 | iCalendar 363 | --------- 364 | 365 | .. index:: CalDAV, RFC-2445 366 | 367 | vCalendar 1.0 was published by Versit, however it was not until the 368 | renamed iCalendar 2.0 (IETF RFC-2445) was published that wide-spread 369 | interoperability was possible. Together with various extensions, and in 370 | particular the CalDAV specification for sharing calendars, iCalendar is 371 | now almost universally used for communication of calendaring data. 372 | 373 | Related Standards 374 | ----------------- 375 | 376 | .. index:: JSON, microformat, XML 377 | 378 | Alongside vCard and iCalendar several different formats have been 379 | developed that reuse most (or all) of their semantic model, while 380 | adopting a different syntax. XML and JSON variants of both standards 381 | exist, as well as HTML microformats. 382 | 383 | More recent work in the IETF is looking to revise the semantic models 384 | of vCard and iCalendar, proposing new standards that can be translated 385 | to and from vCard and iCalendar, but add extra functionality. 386 | 387 | vObject 388 | ------- 389 | 390 | The Python ``vobject`` module supports parsing vCard and iCalendar objects 391 | from strings (email attachments, files, etc) and generating those 392 | formatted strings from Python card and calendar objects. It maintains 393 | compatibility with older versions of the specifications, and supports 394 | various quirks of widely-used implementations, simplifying real-life 395 | usage. 396 | 397 | .. index:: Open Source Applications Foundation, OSAF, Chandler, Eventable, GitHub, Apache 398 | .. index:: single: Harris, Jeffrey 399 | .. index:: single: Karim, Sameen 400 | 401 | ``vobject`` was originally developed by Jeffrey Harris, working at the Open 402 | Source Applications Foundation (OSAF) on their Chandler project. It was 403 | subsequently adopted by Sameen Karim at Eventable, before passing to 404 | community maintenance. The source code is freely available under the 405 | Apache 2.0 license, and developed in a public repository at GitHub. 406 | 407 | Model 408 | ----- 409 | 410 | .. index:: MIME, email 411 | 412 | *iCalendar* and *vCard* are both Multipurpose Internet Mail Extension 413 | (MIME) *profiles*. Originally designed as a way to attach files to 414 | emails, the MIME standards include profiles for various types of 415 | things that are attached to emails from files, including calendar 416 | events and contacts, allowing email clients to understand what they 417 | are, and how to decode them. 418 | 419 | Over time, the use of MIME types and their encoding/decoding standards 420 | has extended beyond email, particularly to web browsers, and to 421 | operating systems in general, where the concept of a "default 422 | application" for a MIME type is used to open downloaded files. 423 | 424 | .. index:: JSON, XML 425 | 426 | *iCalendar* and *vCard* share a MIME syntax and basic encoding 427 | mechanism that is worth explaining up front because it's a little 428 | different to more recent alternatives such as JSON or XML. 429 | 430 | .. index:: enclosure, component, content line, parameter, value 431 | 432 | The major elements of the model are: 433 | 434 | * Enclosure 435 | * Components 436 | * Content Lines 437 | * Parameters 438 | * Values 439 | 440 | Each of these is discussed in detail below. It's not *necessary* to 441 | understand the full detail of these elements, but you will need at 442 | least an overview to work with the ``vobject`` API. 443 | 444 | Enclosure 445 | ~~~~~~~~~ 446 | 447 | A MIME *enclosure* (for example, an ``.ics`` file) is an ordered 448 | sequence of octets, formatted in accordance with a standard MIME 449 | profile. For the *iCalendar* and *vCard* profiles, the MIME 450 | enclosure's contents can be identified in one of two ways: using 451 | ``PROFILE`` or using ``BEGIN`` and ``END``. 452 | 453 | When using ``PROFILE``, only one object can be encoded within the MIME 454 | enclosure. The octet stream contains an initial text line beginning 455 | with "``PROFILE``" that defines the type of the object described by 456 | the following lines. All lines in the MIME enclosure refer to a single 457 | object of the profile type. 458 | 459 | More commonly, using ``BEGIN`` and ``END`` allows multiple objects to 460 | be encoded into a single MIME enclosure. This is usually used for 461 | both *iCalendar* and *vCard*, but the ``PROFILE`` format is also 462 | supported by ``vobject``. 463 | 464 | Component 465 | ~~~~~~~~~ 466 | 467 | Within the enclosure then, one or more objects are encoded. Each 468 | object is called a *component* which represents a complete entity: a 469 | person, an event, a journal entry, a timezone, etc. Components are 470 | described by a set of properties possibly including other nested 471 | components. 472 | 473 | The type of the component is identified by either the ``PROFILE`` or 474 | the ``BEGIN`` / ``END`` lines, for example like: 475 | 476 | .. code-block:: 477 | 478 | BEGIN:VCARD 479 | ... 480 | END:VCARD 481 | 482 | The component types used by the *iCalendar* and *vCard* standards 483 | include: 484 | 485 | * ``VCALENDAR`` 486 | * ``VEVENT`` 487 | * ``VTODO`` 488 | * ``VJOURNAL`` 489 | * ``VTIMEZONE`` 490 | * ``VFREEBUSY`` 491 | * ``VALARM`` 492 | * ``VCARD`` 493 | 494 | Content Line 495 | ~~~~~~~~~~~~ 496 | 497 | A MIME enclosure exists as a sequence of octets (bytes). These octets 498 | represent characters, using a specified *character encoding* -- 499 | typically UTF-8 in modern usage, but possibly ASCII, or other 500 | language-specific encodings, depending on the source application. 501 | 502 | That sequence of characters is broken into *physical lines* by the 503 | character pair ``CRLF``: a *Carriage Return*, followed by a *Line 504 | Feed*. The strings of characters separated by ``CRLF`` pairs are the 505 | physical lines. 506 | 507 | According to the specification, physical lines should not exceed 80 508 | octets, including the ``CRLF``. Because the content itself might 509 | exceed that length, encoding first breaks the content into shorter 510 | lines (called *folding*), and decoding must reassemble the content 511 | from those broken up physical lines (called *unfolding*). 512 | 513 | The unfolded content, possibly longer than 80 octets, is called a 514 | *content line*. Each content line within a component describes a 515 | property of that component. 516 | 517 | A content line has a name, usually written in ALL CAPS style. It may 518 | also have zero or more *parameters*, and finally a *value*. 519 | 520 | Parameters 521 | ~~~~~~~~~~ 522 | 523 | The optional parameters of a content line either describe its encoding, 524 | or clarify its meaning within the component. Parameters have a name, 525 | and optional set of parameter values. 526 | 527 | Example parameters include things like the BASE64 encoding of a 528 | contact's photgraph, or the type of a phone number: voice, fax, work, 529 | home, etc. 530 | 531 | Some properties are represented just by their name, like ``JPEG``, 532 | while others have one or more parameter values, like ``TZID=EST``. 533 | 534 | Value 535 | ~~~~~ 536 | 537 | Finally, the content line will have a value. The formatting of the 538 | value depends upon what property type is represents, and it might be 539 | either a single simple type, a sequence, or a complex multi-part 540 | object. 541 | 542 | For exmaple, the ``VERSION`` property has a single, string-type value. 543 | 544 | .. code-block:: 545 | 546 | VERSION:3.0 547 | 548 | But a *vCard* name property has a complex type value, with five 549 | different attributes, separated by semi-colons: 550 | 551 | .. code-block:: 552 | 553 | N:Public;John;Quinlan;Mr;Esq 554 | 555 | The types of values for each standard property are defined by the 556 | standard documents, and implemented by ``vobject``. 557 | 558 | 559 | .. ##################################################################### 560 | 561 | Parsing 562 | ======= 563 | 564 | To parse one top level component from an existing *iCalendar* or 565 | *vCard* stream or string, use the ``readOne()`` function: 566 | 567 | .. code-block:: python 568 | :linenos: 569 | 570 | >>> parsedCal = vobject.readOne(icalstream) 571 | >>> parsedCal.vevent.dtstart.value 572 | datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) 573 | 574 | Similarly, ``readComponents()`` is a generator yielding one top level 575 | component at a time from a stream or string. 576 | 577 | .. code-block:: python 578 | :linenos: 579 | 580 | >>> vobject.readComponents(icalstream).next().vevent.dtstart.value 581 | datetime.datetime(2006, 2, 16, 0, 0, tzinfo=tzutc()) 582 | 583 | Parsing vCards is very similar. 584 | 585 | .. code-block:: python 586 | :linenos: 587 | 588 | >>> s = """ 589 | ... BEGIN:VCARD 590 | ... VERSION:3.0 591 | ... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org 592 | ... EMAIL;TYPE=INTERNET:jeffery@example.org 593 | ... ORG:Open Source Applications Foundation 594 | ... FN:Jeffrey Harris 595 | ... N:Harris;Jeffrey;;; 596 | ... END:VCARD 597 | ... """ 598 | >>> v = vobject.readOne( s ) 599 | >>> v.prettyPrint() 600 | VCARD 601 | ORG: Open Source Applications Foundation 602 | VERSION: 3.0 603 | EMAIL: jeffrey@osafoundation.org 604 | params for EMAIL: 605 | TYPE [u'INTERNET'] 606 | FN: Jeffrey Harris 607 | N: Jeffrey Harris 608 | >>> v.n.value.family 609 | u'Harris' 610 | >>> v.email_list 611 | [, 612 | ] 613 | 614 | Just like with the *iCalendar* example above, ``readComponents()`` will 615 | yield a generator from a stream or string containing multiple *vCard* 616 | objects. 617 | 618 | .. code-block:: python 619 | :linenos: 620 | 621 | >>> vobject.readComponents(vCardStream).next().email.value 622 | 'jeffrey@osafoundation.org' 623 | 624 | 625 | .. ##################################################################### 626 | 627 | Creating Objects 628 | ================ 629 | 630 | iCalendar 631 | --------- 632 | 633 | vObject has a basic datastructure for working with iCalendar-like 634 | syntax. Additionally, it defines specialized behaviors for many of 635 | the standard iCalendar components. 636 | 637 | The iCalendar standard defines six object types: 638 | 639 | * VEVENT 640 | * VTODO 641 | * VJOURNAL 642 | * VTIMEZONE 643 | * VFREEBUSY 644 | * VALARM 645 | 646 | plus the containing VCALENDAR object (note the name, a hold-over from 647 | the v1.0 Versit standard). 648 | 649 | An iCalendar stream (eg. an .ics file) is comprised of a sequence of 650 | these objects. 651 | 652 | Within vobject, each standard object has a defined *behavior* class, 653 | that specifies its allowed cardinality, base data type, ability to 654 | convert to/from native Python data types, etc. These behaviors are 655 | maintained in a registry within the vobject module, and identified by 656 | name. 657 | 658 | To create an object that already has a behavior defined, run: 659 | 660 | .. index:: newFromBehavior 661 | .. code-block:: python 662 | :linenos: 663 | 664 | >>> import vobject 665 | >>> cal = vobject.newFromBehavior('vcalendar') 666 | >>> cal.behavior 667 | 668 | 669 | Convenience functions exist to create iCalendar and vCard objects: 670 | 671 | .. code-block:: python 672 | :linenos: 673 | 674 | >>> cal = vobject.iCalendar() 675 | >>> cal.behavior 676 | 677 | >>> card = vobject.vCard() 678 | >>> card.behavior 679 | 680 | 681 | Once you have an object, you can use the add method to create 682 | children: 683 | 684 | .. index:: add, prettyPrint 685 | .. code-block:: python 686 | :linenos: 687 | 688 | >>> cal.add('vevent') 689 | 690 | >>> cal.vevent.add('summary').value = "This is a note" 691 | >>> cal.prettyPrint() 692 | VCALENDAR 693 | VEVENT 694 | SUMMARY: This is a note 695 | 696 | Note that summary is a little different from vevent, it's a 697 | ContentLine, not a Component. It can't have children, and it has a 698 | special value attribute. 699 | 700 | ContentLines can also have parameters. They can be accessed with 701 | regular attribute names with _param appended: 702 | 703 | .. code-block:: python 704 | :linenos: 705 | 706 | >>> cal.vevent.summary.x_random_param = 'Random parameter' 707 | >>> cal.prettyPrint() 708 | VCALENDAR 709 | VEVENT 710 | SUMMARY: This is a note 711 | params for SUMMARY: 712 | X-RANDOM ['Random parameter'] 713 | 714 | There are a few things to note about this example 715 | 716 | * The underscore in x_random is converted to a dash (dashes are 717 | legal in iCalendar, underscores legal in Python) 718 | * X-RANDOM's value is a list. 719 | 720 | If you want to access the full list of parameters, not just the first, 721 | use <paramname>_paramlist: 722 | 723 | .. code-block:: python 724 | :linenos: 725 | 726 | >>> cal.vevent.summary.x_random_paramlist 727 | ['Random parameter'] 728 | >>> cal.vevent.summary.x_random_paramlist.append('Other param') 729 | >>> cal.vevent.summary 730 | 731 | 732 | Similar to parameters, If you want to access more than just the first 733 | child of a Component, you can access the full list of children of a 734 | given name by appending `_list` to the attribute name: 735 | 736 | .. code-block:: python 737 | :linenos: 738 | 739 | >>> cal.add('vevent').add('summary').value = "Second VEVENT" 740 | >>> for ev in cal.vevent_list: 741 | ... print ev.summary.value 742 | This is a note 743 | Second VEVENT 744 | 745 | The interaction between the del operator and the hiding of the 746 | underlying list is a little tricky, del cal.vevent and del 747 | cal.vevent_list both delete all vevent children: 748 | 749 | .. code-block:: python 750 | :linenos: 751 | 752 | >>> first_ev = cal.vevent 753 | >>> del cal.vevent 754 | >>> cal 755 | 756 | >>> cal.vevent = first_ev 757 | 758 | VObject understands Python's datetime module and tzinfo classes. 759 | 760 | .. code-block:: python 761 | :linenos: 762 | 763 | >>> import datetime 764 | >>> utc = vobject.icalendar.utc 765 | >>> start = cal.vevent.add('dtstart') 766 | >>> start.value = datetime.datetime(2006, 2, 16, tzinfo = utc) 767 | >>> first_ev.prettyPrint() 768 | VEVENT 769 | DTSTART: 2006-02-16 00:00:00+00:00 770 | SUMMARY: This is a note 771 | params for SUMMARY: 772 | X-RANDOM ['Random parameter', 'Other param'] 773 | 774 | Components and ContentLines have serialize methods: 775 | 776 | .. code-block:: python 777 | :linenos: 778 | 779 | >>> cal.vevent.add('uid').value = 'Sample UID' 780 | >>> icalstream = cal.serialize() 781 | >>> print icalstream 782 | BEGIN:VCALENDAR 783 | VERSION:2.0 784 | PRODID:-//PYVOBJECT//NONSGML Version 1//EN 785 | BEGIN:VEVENT 786 | UID:Sample UID 787 | DTSTART:20060216T000000Z 788 | SUMMARY;X-RANDOM=Random parameter,Other param:This is a note 789 | END:VEVENT 790 | END:VCALENDAR 791 | 792 | Observe that serializing adds missing required lines like version and 793 | prodid. A random UID would be generated, too, if one didn't exist. 794 | 795 | If dtstart's tzinfo had been something other than UTC, an appropriate 796 | vtimezone would be created for it. 797 | 798 | vCard 799 | ----- 800 | 801 | Making vCards proceeds in much the same way. Note that the 'N' and 'FN' 802 | attributes are required. 803 | 804 | .. code-block:: python 805 | :linenos: 806 | 807 | >>> j = vobject.vCard() 808 | >>> j.add('n') 809 | 810 | >>> j.n.value = vobject.vcard.Name( family='Harris', given='Jeffrey' ) 811 | >>> j.add('fn') 812 | 813 | >>> j.fn.value ='Jeffrey Harris' 814 | >>> j.add('email') 815 | 816 | >>> j.email.value = 'jeffrey@osafoundation.org' 817 | >>> j.email.type_param = 'INTERNET' 818 | >>> j.add('org') 819 | 820 | >>> j.org.value = ['Open Source Applications Foundation'] 821 | >>> j.prettyPrint() 822 | VCARD 823 | ORG: ['Open Source Applications Foundation'] 824 | EMAIL: jeffrey@osafoundation.org 825 | params for EMAIL: 826 | TYPE ['INTERNET'] 827 | FN: Jeffrey Harris 828 | N: Jeffrey Harris 829 | 830 | serializing will add any required computable attributes (like 'VERSION') 831 | 832 | .. code-block:: python 833 | :linenos: 834 | 835 | >>> j.serialize() 836 | 'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nORG:Open Source Applications Foundation\r\nEND:VCARD\r\n' 837 | >>> j.prettyPrint() 838 | VCARD 839 | ORG: Open Source Applications Foundation 840 | VERSION: 3.0 841 | EMAIL: jeffrey@osafoundation.org 842 | params for EMAIL: 843 | TYPE ['INTERNET'] 844 | FN: Jeffrey Harris 845 | N: Jeffrey Harris 846 | 847 | 848 | 849 | .. ##################################################################### 850 | 851 | Common Problems 852 | =============== 853 | 854 | - Non-ASCII characters 855 | - Selecting a serialization format version 856 | - Validation 857 | - Flags controlling compatibility with popular application's bugs 858 | 859 | 860 | 861 | .. ##################################################################### 862 | 863 | Getting Help 864 | ============ 865 | 866 | TBD 867 | 868 | .. _GitHub: https://github.com/py-vobject/vobject 869 | .. _Python: http://www.python.org/ 870 | .. _PyPI: https://pypi.org/project/vobject/ 871 | .. _pip: http://www.pip-installer.org/ 872 | .. _git: https://git-scm.com/ 873 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vobject" 3 | description = "Python Calendar and Address object management" 4 | readme = "README.md" 5 | license = {file = "LICENSE-2.0.txt"} 6 | keywords = ["vobject", "icalendar", "vcard", "ics", "vcf", "hcalendar"] 7 | 8 | dynamic = ["version"] 9 | dependencies = [ 10 | "python-dateutil >= 2.5.0; python_version < '3.10'", 11 | "python-dateutil >= 2.7.0; python_version >= '3.10'", 12 | "pytz >= 2019.1", 13 | ] 14 | requires-python = ">= 3.8" 15 | authors = [ 16 | {name = "Jeffrey Harris", email = "jeffrey@osafoundation.org"}, 17 | ] 18 | 19 | maintainers = [ 20 | {name = "David Arnold", email = "davida@pobox.com"}, 21 | ] 22 | 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Environment :: Console", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: Apache Software License", 28 | "Natural Language :: English", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | "Topic :: Text Processing", 39 | ] 40 | 41 | [project.urls] 42 | homepage = "https://py-vobject.github.io/" 43 | source = "https://github.com/py-vobject/vobject" 44 | download = "https://github.com/py-vobject/vobject/releases" 45 | documentation = "https://vobject.readthedocs.io" 46 | issues = "https://github.com/py-vobject/vobject/issues" 47 | 48 | [project.scripts] 49 | ics_diff = "vobject.ics_diff:main" 50 | change_tz = "vobject.change_tz:main" 51 | 52 | [project.optional-dependencies] 53 | dev = [ 54 | "build", 55 | "coverage", 56 | "flake8", 57 | "flit", 58 | "pre-commit", 59 | "pylint", 60 | "pytest", 61 | "sphinx", 62 | ] 63 | 64 | [build-system] 65 | requires = ["flit_core >= 3.4"] 66 | build-backend = "flit_core.buildapi" 67 | 68 | [tool.flit.module] 69 | name = "vobject" 70 | 71 | [tool.flit.sdist] 72 | include = [ 73 | "ACKNOWLEDGEMENTS.txt", 74 | "CHANGELOG.md", 75 | "CONTRIBUTING.md", 76 | "programmers-guide", 77 | "test_files/*.ics", 78 | "test_files/*.vcf", 79 | "tests", 80 | ] 81 | 82 | [tool.black] 83 | target-version = ["py39", "py310", "py311", "py312", "py313"] 84 | line-length = 120 85 | skip-magic-trailing-comma = true 86 | 87 | [tool.flake8] 88 | max-line-length = 120 89 | ignore = ["E203", "E501", "W503"] 90 | exclude = [".git", "__pycache__", ".venv", "venv"] 91 | per-file-ignores = ["*/__init__.py: F401"] 92 | 93 | [tool.isort] 94 | profile = "black" 95 | line_length = 120 96 | multi_line_output = 3 97 | 98 | [tool.tox] 99 | requires = ["tox>=4.19"] 100 | env_list = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 101 | 102 | [tool.tox.env_run_base] 103 | description = "Run test under {base_python}" 104 | deps = ["pytest"] 105 | commands = [["pytest"]] 106 | -------------------------------------------------------------------------------- /specs/rfc4770-im-attrs.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group C. Jennings 8 | Request for Comments: 4770 Cisco Systems 9 | Category: Standards Track J. Reschke, Ed. 10 | greenbytes 11 | January 2007 12 | 13 | 14 | vCard Extensions for Instant Messaging (IM) 15 | 16 | Status of This Memo 17 | 18 | This document specifies an Internet standards track protocol for the 19 | Internet community, and requests discussion and suggestions for 20 | improvements. Please refer to the current edition of the "Internet 21 | Official Protocol Standards" (STD 1) for the standardization state 22 | and status of this protocol. Distribution of this memo is unlimited. 23 | 24 | Copyright Notice 25 | 26 | Copyright (C) The IETF Trust (2007). 27 | 28 | Abstract 29 | 30 | This document describes an extension to vCard to support Instant 31 | Messaging (IM) and Presence Protocol (PP) applications. IM and PP 32 | are becoming increasingly common ways of communicating, and users 33 | want to save this contact information in their address books. It 34 | allows a URI that is associated with IM or PP to be specified inside 35 | a vCard. 36 | 37 | Table of Contents 38 | 39 | 1. Overview ........................................................2 40 | 2. IANA Considerations .............................................3 41 | 3. Formal Grammar ..................................................4 42 | 4. Example .........................................................4 43 | 5. Security Considerations .........................................4 44 | 6. Acknowledgments .................................................4 45 | 7. References ......................................................5 46 | 7.1. Normative References .......................................5 47 | 7.2. Informational References ...................................5 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Jennings & Reschke Standards Track [Page 1] 59 | 60 | RFC 4770 IMPP vCard January 2007 61 | 62 | 63 | 1. Overview 64 | 65 | As more and more people use various instant messaging (IM) and 66 | presence protocol (PP) applications, it becomes important for them to 67 | be able to share this contact address information, along with the 68 | rest of their contact information. RFC 2425 [1] and RFC 2426 [2] 69 | define a standard format for this information, which is referred to 70 | as vCard. This document defines a new type in a vCard for 71 | representing instant IM and PP URIs. It is very similar to existing 72 | types for representing email address and telephone contact 73 | information. 74 | 75 | The type entry to hold this new contact information is an IMPP type. 76 | The IMPP entry has a single URI (see RFC 3986 [3]) that indicates the 77 | address of a service that provides IM, PP, or both. Also defined are 78 | some parameters that give hints as to when certain URIs would be 79 | appropriate. A given vCard can have multiple IMPP entries, but each 80 | entry can contain only one URI. Each IMPP entry can contain multiple 81 | parameters. Any combination of parameters is valid, although a 82 | parameter should occur, at most, once in a given IMPP entry. 83 | 84 | The type of URI indicates what protocols might be usable for 85 | accessing it, but this document does not define any of the types. 86 | For example, a URI type of 87 | 88 | o "sip" [5] indicates to use SIP/SIMPLE, 89 | o "xmpp" [6] indicates to use XMPP, 90 | o "irc" indicates to use IRC, 91 | o "ymsgr" indicates to use yahoo, 92 | o "msn" might indicate to use Microsoft messenger, 93 | o "aim" indicates to use AOL, and 94 | o "im" [7] or "pres" [8] indicates that a CPIM or CPP gateway should 95 | be used. 96 | 97 | The normative definition of this new vCard type is given in Section 98 | 2, and an informational ABNF is provided in Section 3. 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Jennings & Reschke Standards Track [Page 2] 115 | 116 | RFC 4770 IMPP vCard January 2007 117 | 118 | 119 | 2. IANA Considerations 120 | 121 | The required email to define this extension (as defined in RFC 2425 122 | [1]) was sent on October 29, 2004, to the ietf-mime-direct@imc.org 123 | mailing list with the subject "Registration of text/directory MIME 124 | type IMPP" (see ). 126 | 127 | This specification updates the "text/directory MIME Types" 128 | subregistry in the "text/directory MIME Registrations" registry at 129 | http://www.iana.org/assignments/text-directory-registrations with the 130 | following information: 131 | 132 | Type name: IMPP 133 | 134 | Type purpose: To specify the URI for instant messaging and presence 135 | protocol communications with the object the vCard represents. 136 | 137 | Type encoding: 8bit 138 | 139 | Type value: A single URI. The type of the URI indicates the protocol 140 | that can be used for this contact. 141 | 142 | Type special notes: The type may include the type parameter "TYPE" to 143 | specify an intended use for the URI. The TYPE parameter values 144 | include one or more of the following: 145 | 146 | o An indication of the type of communication for which this URI is 147 | appropriate. This can be a value of PERSONAL or BUSINESS. 148 | 149 | o An indication of the location of a device associated with this 150 | URI. Values can be HOME, WORK, or MOBILE. 151 | 152 | o The value PREF indicates this is a preferred address and has the 153 | same semantics as the PREF value in a TEL type. 154 | 155 | Additional information can be found in RFC 4770. 156 | 157 | Intended usage: COMMON 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Jennings & Reschke Standards Track [Page 3] 171 | 172 | RFC 4770 IMPP vCard January 2007 173 | 174 | 175 | 3. Formal Grammar 176 | 177 | The following ABNF grammar [4] extends the grammar found in RFC 2425 178 | [1] (Section 5.8.2) and RFC 2426 [2] (Section 4). 179 | 180 | ;For name="IMPP" 181 | param = impp-param ; Only impp parameters are allowed 182 | 183 | value = URI 184 | ; URI defined in Section 3 of [3] 185 | 186 | impp-param = "TYPE" "=" impp-type *("," impp-type) 187 | 188 | impp-type = "PERSONAL" / "BUSINESS" / ; purpose of communications 189 | "HOME" / "WORK" / "MOBILE" / 190 | "PREF" / 191 | iana-token / x-name; 192 | ; Values are case insensitive 193 | 194 | 4. Example 195 | 196 | BEGIN:vCard 197 | VERSION:3.0 198 | FN:Alice Doe 199 | IMPP;TYPE=personal,pref:im:alice@example.com 200 | END:vCard 201 | 202 | 5. Security Considerations 203 | 204 | This does not introduce additional security issues beyond the current 205 | vCard specification. It is worth noting that many people consider 206 | their presence information more sensitive than other address 207 | information. Any system that stores or transfers vCards needs to 208 | carefully consider the privacy issues around this information. 209 | 210 | 6. Acknowledgments 211 | 212 | Thanks to Brian Carpenter, Lars Eggert, Ted Hardie, Paul Hoffman, Sam 213 | Roberts, and Pekka Pessi for their comments. 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Jennings & Reschke Standards Track [Page 4] 227 | 228 | RFC 4770 IMPP vCard January 2007 229 | 230 | 231 | 7. References 232 | 233 | 7.1. Normative References 234 | 235 | 236 | [1] Howes, T., Smith, M., and F. Dawson, "A MIME Content-Type for 237 | Directory Information", RFC 2425, September 1998. 238 | 239 | [2] Dawson, F. and T. Howes, "vCard MIME Directory Profile", RFC 240 | 2426, September 1998. 241 | 242 | [3] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform 243 | Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, 244 | January 2005. 245 | 246 | [4] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax 247 | Specifications: ABNF", RFC 4234, October 2005. 248 | 249 | 7.2. Informational References 250 | 251 | [5] Rosenberg, J., Schulzrinne, H., Camarillo, G., Johnston, A., 252 | Peterson, J., Sparks, R., Handley, M., and E. Schooler, "SIP: 253 | Session Initiation Protocol", RFC 3261, June 2002. 254 | 255 | [6] Saint-Andre, P., "Internationalized Resource Identifiers (IRIs) 256 | and Uniform Resource Identifiers (URIs) for the Extensible 257 | Messaging and Presence Protocol (XMPP)", RFC 4622, July 2006. 258 | 259 | [7] Peterson, J., "Common Profile for Instant Messaging (CPIM)", RFC 260 | 3860, August 2004. 261 | 262 | [8] Peterson, J., "Common Profile for Presence (CPP)", RFC 3859, 263 | August 2004. 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Jennings & Reschke Standards Track [Page 5] 283 | 284 | RFC 4770 IMPP vCard January 2007 285 | 286 | 287 | Authors' Addresses 288 | 289 | Cullen Jennings 290 | Cisco Systems 291 | 170 West Tasman Drive 292 | MS: SJC-21/2 293 | San Jose, CA 95134 294 | USA 295 | 296 | Phone: +1 408 902-3341 297 | EMail: fluffy@cisco.com 298 | 299 | 300 | Julian F. Reschke (editor) 301 | greenbytes GmbH 302 | Hafenweg 16 303 | Muenster, NW 48155 304 | Germany 305 | 306 | Phone: +49 251 2807760 307 | EMail: julian.reschke@greenbytes.de 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Jennings & Reschke Standards Track [Page 6] 339 | 340 | RFC 4770 IMPP vCard January 2007 341 | 342 | 343 | Full Copyright Statement 344 | 345 | Copyright (C) The IETF Trust (2007). 346 | 347 | This document is subject to the rights, licenses and restrictions 348 | contained in BCP 78, and except as set forth therein, the authors 349 | retain all their rights. 350 | 351 | This document and the information contained herein are provided on an 352 | "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS 353 | OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE IETF TRUST, 354 | AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, 355 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT 356 | THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY 357 | IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR 358 | PURPOSE. 359 | 360 | Intellectual Property 361 | 362 | The IETF takes no position regarding the validity or scope of any 363 | Intellectual Property Rights or other rights that might be claimed to 364 | pertain to the implementation or use of the technology described in 365 | this document or the extent to which any license under such rights 366 | might or might not be available; nor does it represent that it has 367 | made any independent effort to identify any such rights. Information 368 | on the procedures with respect to rights in RFC documents can be 369 | found in BCP 78 and BCP 79. 370 | 371 | Copies of IPR disclosures made to the IETF Secretariat and any 372 | assurances of licenses to be made available, or the result of an 373 | attempt made to obtain a general license or permission for the use of 374 | such proprietary rights by implementers or users of this 375 | specification can be obtained from the IETF on-line IPR repository at 376 | http://www.ietf.org/ipr. 377 | 378 | The IETF invites any interested party to bring to its attention any 379 | copyrights, patents or patent applications, or other proprietary 380 | rights that may cover technology that may be required to implement 381 | this standard. Please address the information to the IETF at 382 | ietf-ipr@ietf.org. 383 | 384 | Acknowledgement 385 | 386 | Funding for the RFC Editor function is currently provided by the 387 | Internet Society. 388 | 389 | 390 | 391 | 392 | 393 | 394 | Jennings & Reschke Standards Track [Page 7] 395 | 396 | -------------------------------------------------------------------------------- /specs/rfc6868-param-value-encoding.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) C. Daboo 8 | Request for Comments: 6868 Apple 9 | Updates: 5545, 6321, 6350, 6351 February 2013 10 | Category: Standards Track 11 | ISSN: 2070-1721 12 | 13 | 14 | Parameter Value Encoding in iCalendar and vCard 15 | 16 | Abstract 17 | 18 | This specification updates the data formats for iCalendar (RFC 5545) 19 | and vCard (RFC 6350) to allow parameter values to include certain 20 | characters forbidden by the existing specifications. 21 | 22 | Status of This Memo 23 | 24 | This is an Internet Standards Track document. 25 | 26 | This document is a product of the Internet Engineering Task Force 27 | (IETF). It represents the consensus of the IETF community. It has 28 | received public review and has been approved for publication by the 29 | Internet Engineering Steering Group (IESG). Further information on 30 | Internet Standards is available in Section 2 of RFC 5741. 31 | 32 | Information about the current status of this document, any errata, 33 | and how to provide feedback on it may be obtained at 34 | http://www.rfc-editor.org/info/rfc6868. 35 | 36 | Copyright Notice 37 | 38 | Copyright (c) 2013 IETF Trust and the persons identified as the 39 | document authors. All rights reserved. 40 | 41 | This document is subject to BCP 78 and the IETF Trust's Legal 42 | Provisions Relating to IETF Documents 43 | (http://trustee.ietf.org/license-info) in effect on the date of 44 | publication of this document. Please review these documents 45 | carefully, as they describe your rights and restrictions with respect 46 | to this document. Code Components extracted from this document must 47 | include Simplified BSD License text as described in Section 4.e of 48 | the Trust Legal Provisions and are provided without warranty as 49 | described in the Simplified BSD License. 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Daboo Standards Track [Page 1] 59 | 60 | RFC 6868 Parameter Encoding February 2013 61 | 62 | 63 | Table of Contents 64 | 65 | 1. Introduction ....................................................2 66 | 2. Conventions Used in This Document ...............................2 67 | 3. Parameter Value Encoding Scheme .................................3 68 | 3.1. iCalendar Example ..........................................4 69 | 3.2. vCard Example ..............................................4 70 | 4. Security Considerations .........................................4 71 | 5. Acknowledgments .................................................4 72 | 6. Normative References ............................................5 73 | Appendix A. Choice of Quoting Mechanism ............................6 74 | 75 | 1. Introduction 76 | 77 | The iCalendar [RFC5545] specification defines a standard way to 78 | describe calendar data. The vCard [RFC6350] specification defines a 79 | standard way to describe contact data. Both of these use a similar 80 | text-based data format. Each iCalendar and vCard data object can 81 | include "properties" that have "parameters" and a "value". The value 82 | of a "parameter" is typically a token or URI value, but a "generic" 83 | text value is also allowed. However, the syntax rules for both 84 | iCalendar and vCard prevent the use of a double-quote character or 85 | control characters in such values, though double-quote characters and 86 | some subset of control characters are allowed in the actual property 87 | values. 88 | 89 | As more and more extensions are being developed for these data 90 | formats, there is a need to allow at least double-quotes and line 91 | feeds to be included in parameter values. The \-escaping mechanism 92 | used for property text values is not defined for use with parameter 93 | values and cannot be easily used in a backwards-compatible manner. 94 | This specification defines a new character escaping mechanism, 95 | compatible with existing parsers and chosen to minimize any impact on 96 | existing data. 97 | 98 | 2. Conventions Used in This Document 99 | 100 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 101 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 102 | "OPTIONAL" in this document are to be interpreted as described in 103 | [RFC2119]. 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Daboo Standards Track [Page 2] 115 | 116 | RFC 6868 Parameter Encoding February 2013 117 | 118 | 119 | 3. Parameter Value Encoding Scheme 120 | 121 | This specification defines the ^ character (U+005E -- Circumflex 122 | Accent) as an escape character in parameter values whose value type 123 | is defined using the "param-value" syntax element (Section 3.1 of 124 | iCalendar [RFC5545] and Section 3.3 of vCard [RFC6350]). The 125 | ^-escaping mechanism can be used when the value is either unquoted or 126 | quoted (i.e., whether or not the value is surrounded by double- 127 | quotes). 128 | 129 | When generating iCalendar or vCard parameter values, the following 130 | apply: 131 | 132 | o formatted text line breaks are encoded into ^n (U+005E, U+006E) 133 | 134 | o the ^ character (U+005E) is encoded into ^^ (U+005E, U+005E) 135 | 136 | o the " character (U+0022) is encoded into ^' (U+005E, U+0027) 137 | 138 | When parsing iCalendar or vCard parameter values, the following 139 | apply: 140 | 141 | o the character sequence ^n (U+005E, U+006E) is decoded into an 142 | appropriate formatted line break according to the type of system 143 | being used 144 | 145 | o the character sequence ^^ (U+005E, U+005E) is decoded into the ^ 146 | character (U+005E) 147 | 148 | o the character sequence ^' (U+005E, U+0027) is decoded into the " 149 | character (U+0022) 150 | 151 | o if a ^ (U+005E) character is followed by any character other than 152 | the ones above, parsers MUST leave both the ^ and the following 153 | character in place 154 | 155 | When converting between iCalendar and vCard text-based data formats 156 | and alternative data-format representations such as XML (as described 157 | in [RFC6321] and [RFC6351], respectively), implementations MUST 158 | ensure that parameter value escape sequences are generated correctly 159 | in the text-based format and are decoded when the parameter values 160 | appear in the alternate data formats. 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Daboo Standards Track [Page 3] 171 | 172 | RFC 6868 Parameter Encoding February 2013 173 | 174 | 175 | 3.1. iCalendar Example 176 | 177 | The following example is an "ATTENDEE" property with a "CN" parameter 178 | whose value includes two double-quote characters. The parameter 179 | value is not quoted, as there are no characters in the value that 180 | would trigger quoting as required by iCalendar. 181 | 182 | ATTENDEE;CN=George Herman ^'Babe^' Ruth:mailto:babe@example.com 183 | 184 | The unescaped parameter value is 185 | 186 | George Herman "Babe" Ruth 187 | 188 | 3.2. vCard Example 189 | 190 | The following example is a "GEO" property with an "X-ADDRESS" 191 | parameter whose value includes several line feed characters. The 192 | parameter value is also quoted, since it contains a comma, which 193 | triggers quoting as required by vCard. 194 | 195 | GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt 196 | sburgh, PA 15212":geo:40.446816,-80.00566 197 | 198 | The unescaped parameter value (where each line is terminated by a 199 | line break character sequence) is 200 | 201 | Pittsburgh Pirates 202 | 115 Federal St 203 | Pittsburgh, PA 15212 204 | 205 | 4. Security Considerations 206 | 207 | There are no additional security issues beyond those of iCalendar 208 | [RFC5545] and vCard [RFC6350]. 209 | 210 | 5. Acknowledgments 211 | 212 | Thanks to Michael Angstadt, Tim Bray, Mike Douglass, Barry Leiba, 213 | Simon Perreault, and Pete Resnick for feedback on this specification. 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Daboo Standards Track [Page 4] 227 | 228 | RFC 6868 Parameter Encoding February 2013 229 | 230 | 231 | 6. Normative References 232 | 233 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 234 | Requirement Levels", BCP 14, RFC 2119, March 1997. 235 | 236 | [RFC5545] Desruisseaux, B., "Internet Calendaring and Scheduling 237 | Core Object Specification (iCalendar)", RFC 5545, 238 | September 2009. 239 | 240 | [RFC6321] Daboo, C., Douglass, M., and S. Lees, "xCal: The XML 241 | Format for iCalendar", RFC 6321, August 2011. 242 | 243 | [RFC6350] Perreault, S., "vCard Format Specification", RFC 6350, 244 | August 2011. 245 | 246 | [RFC6351] Perreault, S., "xCard: vCard XML Representation", 247 | RFC 6351, August 2011. 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Daboo Standards Track [Page 5] 283 | 284 | RFC 6868 Parameter Encoding February 2013 285 | 286 | 287 | Appendix A. Choice of Quoting Mechanism 288 | 289 | Having recognized the need for escaping parameter values, the 290 | question is what mechanism to use? One obvious choice would be to 291 | adopt the \-escaping used for property values. However, that could 292 | not be used as-is, because it escapes a double-quote as the sequence 293 | of \ followed by double-quote. Consider what the example in 294 | Section 3.1 might look like using \-escaping: 295 | 296 | ATTENDEE;CN="George Herman \"Babe\" Ruth":mailto:babe@example.com 297 | 298 | Existing iCalendar/vCard parsers know nothing about escape sequences 299 | in parameters. So they would parse the parameter value as: 300 | 301 | George Herman \ 302 | 303 | i.e., the text between the first and second occurrence of a double- 304 | quote. However, the text after the second double-quote ought to be 305 | either a : or a ; (to delimit the parameter value from the following 306 | parameter or property) but is not, so the parser could legitimately 307 | throw an error at that point because the data is syntactically 308 | invalid. Thus, for backwards-compatibility reasons, a double-quote 309 | cannot be escaped using a sequence that itself includes a double- 310 | quote, and hence the choice of using a single-quote in this 311 | specification. 312 | 313 | Another option would be to use a form of \-escaping modified for use 314 | in parameter values only. However, some incorrect, non-interoperable 315 | use of \ in parameter values has been observed, and thus it is best 316 | to steer clear of that to achieve guaranteed, reliable 317 | interoperability. Also, given that double-quote gets changed to 318 | single-quote in the escape sequence for a parameter, but not for a 319 | value, it is better to not give the impression that the same escape 320 | mechanism (and thus code) can be used for both (which could lead to 321 | other issues, such as an implementation incorrectly escaping a ; as 322 | \; as opposed to quoting the parameter value). 323 | 324 | The choice of ^ as the escape character was made based on the 325 | requirement that an ASCII symbol (non-alphanumeric character) be 326 | used, and it ought to be one least likely to be found in existing 327 | data. 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Daboo Standards Track [Page 6] 339 | 340 | RFC 6868 Parameter Encoding February 2013 341 | 342 | 343 | Author's Address 344 | 345 | Cyrus Daboo 346 | Apple Inc. 347 | 1 Infinite Loop 348 | Cupertino, CA 95014 349 | USA 350 | 351 | EMail: cyrus@daboo.name 352 | URI: http://www.apple.com/ 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Daboo Standards Track [Page 7] 395 | 396 | -------------------------------------------------------------------------------- /specs/vcalendar-10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-vobject/vobject/e2403a2b8cb8c75b92e6bd18f1feeb129298d592/specs/vcalendar-10.pdf -------------------------------------------------------------------------------- /specs/vcard-21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py-vobject/vobject/e2403a2b8cb8c75b92e6bd18f1feeb129298d592/specs/vcard-21.pdf -------------------------------------------------------------------------------- /test_files/more_tests.txt: -------------------------------------------------------------------------------- 1 | 2 | Unicode in vCards 3 | ................. 4 | 5 | >>> import vobject 6 | >>> card = vobject.vCard() 7 | >>> card.add('fn').value = u'Hello\u1234 World!' 8 | >>> card.add('n').value = vobject.vcard.Name('World', u'Hello\u1234') 9 | >>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') 10 | >>> card 11 | , , ]> 12 | >>> card.serialize() 13 | u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n' 14 | >>> print(card.serialize()) 15 | BEGIN:VCARD 16 | VERSION:3.0 17 | ADR:;;5ሴ Nowhere\, Apt 1;Berkeley;CA;94704;USA 18 | FN:Helloሴ World! 19 | N:World;Helloሴ;;; 20 | END:VCARD 21 | 22 | Helper function 23 | ............... 24 | >>> from pkg_resources import resource_stream 25 | >>> def get_stream(path): 26 | ... try: 27 | ... return resource_stream(__name__, 'test_files/' + path) 28 | ... except: # different paths, depending on whether doctest is run directly 29 | ... return resource_stream(__name__, path) 30 | 31 | Unicode in TZID 32 | ............... 33 | >>> f = get_stream("tzid_8bit.ics") 34 | >>> cal = vobject.readOne(f) 35 | >>> print(cal.vevent.dtstart.value) 36 | 2008-05-30 15:00:00+06:00 37 | >>> print(cal.vevent.dtstart.serialize()) 38 | DTSTART;TZID=Екатеринбург:20080530T150000 39 | 40 | Commas in TZID 41 | .............. 42 | >>> f = get_stream("ms_tzid.ics") 43 | >>> cal = vobject.readOne(f) 44 | >>> print(cal.vevent.dtstart.value) 45 | 2008-05-30 15:00:00+10:00 46 | 47 | Equality in vCards 48 | .................. 49 | 50 | >>> card.adr.value == vobject.vcard.Address('Just a street') 51 | False 52 | >>> card.adr.value == vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') 53 | True 54 | 55 | Organization (org) 56 | .................. 57 | 58 | >>> card.add('org').value = ["Company, Inc.", "main unit", "sub-unit"] 59 | >>> print(card.org.serialize()) 60 | ORG:Company\, Inc.;main unit;sub-unit 61 | 62 | Ruby escapes semi-colons in rrules 63 | .................................. 64 | 65 | >>> f = get_stream("ruby_rrule.ics") 66 | >>> cal = vobject.readOne(f) 67 | >>> iter(cal.vevent.rruleset).next() 68 | datetime.datetime(2003, 1, 1, 7, 0) 69 | 70 | quoted-printable 71 | ................ 72 | 73 | >>> vcf = 'BEGIN:VCARD\nVERSION:2.1\nN;ENCODING=QUOTED-PRINTABLE:;=E9\nFN;ENCODING=QUOTED-PRINTABLE:=E9\nTEL;HOME:0111111111\nEND:VCARD\n\n' 74 | >>> vcf = vobject.readOne(vcf) 75 | >>> vcf.n.value 76 | 77 | >>> vcf.n.value.given 78 | u'\xe9' 79 | >>> vcf.serialize() 80 | 'BEGIN:VCARD\r\nVERSION:2.1\r\nFN:\xc3\xa9\r\nN:;\xc3\xa9;;;\r\nTEL:0111111111\r\nEND:VCARD\r\n' 81 | 82 | >>> vcs = 'BEGIN:VCALENDAR\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nVERSION:1.0\r\nBEGIN:VEVENT\r\nDESCRIPTION;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:foo =C3=A5=0Abar =C3=A4=\r\n=0Abaz =C3=B6\r\nUID:20080406T152030Z-7822\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' 83 | >>> vcs = vobject.readOne(vcs, allowQP = True) 84 | >>> vcs.serialize() 85 | 'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' 86 | -------------------------------------------------------------------------------- /test_files/ms_tzid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Canberra, Melbourne, Sydney 6 | BEGIN:STANDARD 7 | DTSTART:20010325T020000 8 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20050327T070000Z 9 | TZOFFSETFROM:+1100 10 | TZOFFSETTO:+1000 11 | TZNAME:Standard Time 12 | END:STANDARD 13 | BEGIN:STANDARD 14 | DTSTART:20060402T020000 15 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z 16 | TZOFFSETFROM:+1100 17 | TZOFFSETTO:+1000 18 | TZNAME:Standard Time 19 | END:STANDARD 20 | BEGIN:STANDARD 21 | DTSTART:20070325T020000 22 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 23 | TZOFFSETFROM:+1100 24 | TZOFFSETTO:+1000 25 | TZNAME:Standard Time 26 | END:STANDARD 27 | BEGIN:DAYLIGHT 28 | DTSTART:20001029T020000 29 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 30 | TZOFFSETFROM:+1000 31 | TZOFFSETTO:+1100 32 | TZNAME:Daylight Savings Time 33 | END:DAYLIGHT 34 | END:VTIMEZONE 35 | BEGIN:VEVENT 36 | UID:CommaTest 37 | DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000 38 | END:VEVENT 39 | END:VCALENDAR 40 | -------------------------------------------------------------------------------- /test_files/radicale-0816.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//PYVOBJECT//NONSGML Version 1//EN 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/London 6 | BEGIN:STANDARD 7 | DTSTART:20001029T030000 8 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 9 | TZNAME:GMT 10 | TZOFFSETFROM:+0100 11 | TZOFFSETTO:+0000 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:20000326T010000 15 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 16 | TZNAME:BST 17 | TZOFFSETFROM:+0000 18 | TZOFFSETTO:+0100 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:6b6fbf57-ea05-4717-a62e-24b314bd72af 23 | DTSTART;TZID=Europe/London:20160616T090000 24 | DTEND;TZID=Europe/London:20160616T162000 25 | CATEGORIES:My special category 26 | CREATED:20160413T143427Z 27 | DESCRIPTION:Description 28 | DTSTAMP:20160413T143524Z 29 | LAST-MODIFIED:20160413T143524Z 30 | LOCATION:Secret Location 31 | SUMMARY:STG 32 | TRANSP:OPAQUE 33 | END:VEVENT 34 | END:VCALENDAR 35 | -------------------------------------------------------------------------------- /test_files/radicale-0827.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Paris 6 | BEGIN:DAYLIGHT 7 | TZOFFSETFROM:+0100 8 | TZOFFSETTO:+0200 9 | TZNAME:CEST 10 | DTSTART:19700329T020000 11 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 12 | END:DAYLIGHT 13 | BEGIN:STANDARD 14 | TZOFFSETFROM:+0200 15 | TZOFFSETTO:+0100 16 | TZNAME:CET 17 | DTSTART:19701025T030000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 19 | END:STANDARD 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | CREATED:20171006T120227Z 23 | LAST-MODIFIED:20180509T072357Z 24 | DTSTAMP:20180509T072357Z 25 | UID:812dae9d-4114-45c1-ba5b-4ee34756ab47 26 | SUMMARY:Cours Balboa 27 | RRULE:FREQ=WEEKLY;UNTIL=20180623T183000Z 28 | EXDATE:20171023T183000Z 29 | EXDATE:20171030T193000Z 30 | EXDATE:20171225T193000Z 31 | EXDATE:20180101T193000Z 32 | EXDATE:20180212T193000Z 33 | EXDATE:20180219T193000Z 34 | EXDATE:20180409T183000Z 35 | EXDATE:20180416T183000Z 36 | EXDATE:20180402T183000Z 37 | DTSTART;TZID=Europe/Paris:20171009T203000 38 | DTEND;TZID=Europe/Paris:20171009T213000 39 | TRANSP:OPAQUE 40 | SEQUENCE:8 41 | X-MOZ-GENERATION:10 42 | LOCATION:Some place\, Some where 43 | END:VEVENT 44 | BEGIN:VEVENT 45 | CREATED:20180327T075738Z 46 | LAST-MODIFIED:20180327T075754Z 47 | DTSTAMP:20180327T075754Z 48 | UID:812dae9d-4114-45c1-ba5b-4ee34756ab47 49 | SUMMARY:Cours Balboa 50 | STATUS:CANCELLED 51 | RECURRENCE-ID;TZID=Europe/Paris:20180326T203000 52 | DTSTART;TZID=Europe/Paris:20180326T203000 53 | DTEND;TZID=Europe/Paris:20180326T213000 54 | TRANSP:OPAQUE 55 | SEQUENCE:8 56 | X-MOZ-GENERATION:8 57 | LOCATION:Some place\, Some where 58 | END:VEVENT 59 | BEGIN:VEVENT 60 | CREATED:20180509T072349Z 61 | LAST-MODIFIED:20180509T072357Z 62 | DTSTAMP:20180509T072357Z 63 | UID:812dae9d-4114-45c1-ba5b-4ee34756ab47 64 | SUMMARY:Cours Balboa 65 | STATUS:CANCELLED 66 | RECURRENCE-ID;TZID=Europe/Paris:20180430T203000 67 | DTSTART;TZID=Europe/Paris:20180430T203000 68 | DTEND;TZID=Europe/Paris:20180430T213000 69 | TRANSP:OPAQUE 70 | SEQUENCE:9 71 | X-MOZ-GENERATION:10 72 | LOCATION:Some place\, Some where 73 | END:VEVENT 74 | END:VCALENDAR 75 | -------------------------------------------------------------------------------- /test_files/radicale-1587.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | FN:Given Family 4 | N:Family;Given;Additional;Prefix;Suffix 5 | GEO:37.386013;-122.082932 6 | END:VCARD 7 | -------------------------------------------------------------------------------- /test_files/ruby_rrule.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | METHOD:PUBLISH 5 | PRODID:-//LinkeSOFT GmbH//NONSGML DIMEX//EN 6 | BEGIN:VEVENT 7 | SEQUENCE:0 8 | RRULE:FREQ=DAILY\;COUNT=10 9 | DTEND:20030101T080000 10 | UID:2008-05-29T17:31:42+02:00_865561242 11 | CATEGORIES:Unfiled 12 | SUMMARY:Something 13 | DTSTART:20030101T070000 14 | DTSTAMP:20080529T152100 15 | END:VEVENT 16 | END:VCALENDAR 17 | -------------------------------------------------------------------------------- /test_files/simple_test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | SUMMARY;blah=hi!:Bastille Day Party 4 | END:VEVENT 5 | END:VCALENDAR 6 | -------------------------------------------------------------------------------- /test_files/timezones.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VTIMEZONE 2 | TZID:US/Pacific 3 | BEGIN:STANDARD 4 | DTSTART:19671029T020000 5 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 6 | TZOFFSETFROM:-0700 7 | TZOFFSETTO:-0800 8 | TZNAME:PST 9 | END:STANDARD 10 | BEGIN:DAYLIGHT 11 | DTSTART:19870405T020000 12 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 13 | TZOFFSETFROM:-0800 14 | TZOFFSETTO:-0700 15 | TZNAME:PDT 16 | END:DAYLIGHT 17 | END:VTIMEZONE 18 | 19 | BEGIN:VTIMEZONE 20 | TZID:US/Eastern 21 | BEGIN:STANDARD 22 | DTSTART:19671029T020000 23 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 24 | TZOFFSETFROM:-0400 25 | TZOFFSETTO:-0500 26 | TZNAME:EST 27 | END:STANDARD 28 | BEGIN:DAYLIGHT 29 | DTSTART:19870405T020000 30 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 31 | TZOFFSETFROM:-0500 32 | TZOFFSETTO:-0400 33 | TZNAME:EDT 34 | END:DAYLIGHT 35 | END:VTIMEZONE 36 | 37 | BEGIN:VTIMEZONE 38 | TZID:Santiago 39 | BEGIN:STANDARD 40 | DTSTART:19700314T000000 41 | TZOFFSETFROM:-0300 42 | TZOFFSETTO:-0400 43 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA 44 | TZNAME:Pacific SA Standard Time 45 | END:STANDARD 46 | BEGIN:DAYLIGHT 47 | DTSTART:19701010T000000 48 | TZOFFSETFROM:-0400 49 | TZOFFSETTO:-0300 50 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA 51 | TZNAME:Pacific SA Daylight Time 52 | END:DAYLIGHT 53 | END:VTIMEZONE 54 | 55 | BEGIN:VTIMEZONE 56 | TZID:W. Europe 57 | BEGIN:STANDARD 58 | DTSTART:19701025T030000 59 | TZOFFSETFROM:+0200 60 | TZOFFSETTO:+0100 61 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 62 | TZNAME:W. Europe Standard Time 63 | END:STANDARD 64 | BEGIN:DAYLIGHT 65 | DTSTART:19700329T020000 66 | TZOFFSETFROM:+0100 67 | TZOFFSETTO:+0200 68 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 69 | TZNAME:W. Europe Daylight Time 70 | END:DAYLIGHT 71 | END:VTIMEZONE 72 | 73 | BEGIN:VTIMEZONE 74 | TZID:US/Fictitious-Eastern 75 | LAST-MODIFIED:19870101T000000Z 76 | BEGIN:STANDARD 77 | DTSTART:19671029T020000 78 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 79 | TZOFFSETFROM:-0400 80 | TZOFFSETTO:-0500 81 | TZNAME:EST 82 | END:STANDARD 83 | BEGIN:DAYLIGHT 84 | DTSTART:19870405T020000 85 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z 86 | TZOFFSETFROM:-0500 87 | TZOFFSETTO:-0400 88 | TZNAME:EDT 89 | END:DAYLIGHT 90 | END:VTIMEZONE 91 | 92 | BEGIN:VTIMEZONE 93 | TZID:America/Montreal 94 | LAST-MODIFIED:20051013T233643Z 95 | BEGIN:DAYLIGHT 96 | DTSTART:20050403T070000 97 | TZOFFSETTO:-0400 98 | TZOFFSETFROM:+0000 99 | TZNAME:EDT 100 | END:DAYLIGHT 101 | BEGIN:STANDARD 102 | DTSTART:20051030T020000 103 | TZOFFSETTO:-0500 104 | TZOFFSETFROM:-0400 105 | TZNAME:EST 106 | END:STANDARD 107 | END:VTIMEZONE 108 | -------------------------------------------------------------------------------- /test_files/tz_us_eastern.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VTIMEZONE 2 | TZID:US/Eastern 3 | BEGIN:STANDARD 4 | DTSTART:20001029T020000 5 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z 6 | TZNAME:EST 7 | TZOFFSETFROM:-0400 8 | TZOFFSETTO:-0500 9 | END:STANDARD 10 | BEGIN:STANDARD 11 | DTSTART:20071104T020000 12 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 13 | TZNAME:EST 14 | TZOFFSETFROM:-0400 15 | TZOFFSETTO:-0500 16 | END:STANDARD 17 | BEGIN:DAYLIGHT 18 | DTSTART:20000402T020000 19 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z 20 | TZNAME:EDT 21 | TZOFFSETFROM:-0500 22 | TZOFFSETTO:-0400 23 | END:DAYLIGHT 24 | BEGIN:DAYLIGHT 25 | DTSTART:20070311T020000 26 | RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 27 | TZNAME:EDT 28 | TZOFFSETFROM:-0500 29 | TZOFFSETTO:-0400 30 | END:DAYLIGHT 31 | END:VTIMEZONE 32 | -------------------------------------------------------------------------------- /test_files/tzid_8bit.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Екатеринбург 6 | BEGIN:STANDARD 7 | DTSTART:16011028T030000 8 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 9 | TZOFFSETFROM:+0600 10 | TZOFFSETTO:+0500 11 | END:STANDARD 12 | BEGIN:DAYLIGHT 13 | DTSTART:16010325T020000 14 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 15 | TZOFFSETFROM:+0500 16 | TZOFFSETTO:+0600 17 | END:DAYLIGHT 18 | END:VTIMEZONE 19 | BEGIN:VEVENT 20 | UID:CyrillicTest 21 | DTSTART;TZID=Екатеринбург:20080530T150000 22 | END:VEVENT 23 | END:VCALENDAR 24 | -------------------------------------------------------------------------------- /test_files/vobject_0050.ics: -------------------------------------------------------------------------------- 1 | 2 | BEGIN:VCALENDAR 3 | PRODID:-//Force.com Labs//iCalendar Export//EN 4 | VERSION:2.0 5 | METHOD: REQUEST 6 | CALSCALE:GREGORIAN 7 | BEGIN:VEVENT 8 | STATUS:CONFIRMED 9 | ORGANIZER;CN=Wells Fargo and Company:mailto:appointments@wellsfargo.com 10 | UID:appointments@wellsfargo.com 11 | LOCATION:POJOAQUE 12 | CREATED:20240812T192015Z 13 | DTSTART:20240812T213000Z 14 | DTEND: 20240812T223000Z 15 | TRANSP:OPAQUE 16 | DURATION:PT60M 17 | SUMMARY:Personal: Open a new account 18 | DTSTAMP:20240812T192015Z 19 | LAST-MODIFIED:20240812T192015Z 20 | SEQUENCE:0 21 | DESCRIPTION:Personal: Open a new account 22 | END:VEVENT 23 | END:VCALENDAR 24 | -------------------------------------------------------------------------------- /test_files/vtodo.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//Example Corp.//CalDAV Client//EN 4 | BEGIN:VTODO 5 | UID:20070313T123432Z-456553@example.com 6 | DTSTAMP:20070313T123432Z 7 | DUE;VALUE=DATE:20070501 8 | SUMMARY:Submit Quebec Income Tax Return for 2006 9 | CLASS:CONFIDENTIAL 10 | CATEGORIES:FAMILY,FINANCE 11 | STATUS:NEEDS-ACTION 12 | END:VTODO 13 | END:VCALENDAR 14 | -------------------------------------------------------------------------------- /tests/test_behaviors.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import vobject 4 | 5 | 6 | def test_general_behavior(): 7 | """ 8 | Tests for behavior registry, getting and creating a behavior. 9 | """ 10 | # test get_behavior 11 | behavior = vobject.base.getBehavior("VCALENDAR") 12 | assert str(behavior) == "" 13 | assert behavior.isComponent 14 | assert vobject.base.getBehavior("invalid_name") is None 15 | 16 | # test for ContentLine (not a component) 17 | non_component_behavior = vobject.base.getBehavior("RDATE") 18 | assert not non_component_behavior.isComponent 19 | 20 | 21 | def test_multi_date_behavior(): 22 | """ 23 | Test MultiDateBehavior 24 | """ 25 | parse_r_date = vobject.icalendar.MultiDateBehavior.transformToNative 26 | 27 | expected = ( 28 | "" 30 | ) 31 | result = str( 32 | parse_r_date(vobject.base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904")) 33 | ) 34 | assert result == expected 35 | 36 | expected = ( 37 | "" 40 | ) 41 | result = str( 42 | parse_r_date( 43 | vobject.base.textLineToContentLine( 44 | "RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H" 45 | ) 46 | ) 47 | ) 48 | assert result == expected 49 | 50 | 51 | def test_period_behavior(): 52 | """ 53 | Test PeriodBehavior 54 | """ 55 | two_hours = datetime.timedelta(hours=2) 56 | 57 | line = vobject.base.ContentLine("test", [], "", isNative=True) 58 | line.behavior = vobject.icalendar.PeriodBehavior 59 | 60 | line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] 61 | assert line.transformFromNative().value == "20060216T100000/PT2H" 62 | expected = [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] 63 | assert line.transformToNative().value == expected 64 | 65 | line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) 66 | assert line.serialize().strip() == "TEST:20060216T100000/PT2H,20060516T100000/PT2H" 67 | -------------------------------------------------------------------------------- /tests/test_calendar_serialization.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import io 3 | import json 4 | 5 | import dateutil 6 | 7 | import vobject 8 | 9 | simple_2_0_test = ( 10 | "BEGIN:VCALENDAR\r\n" 11 | "VERSION:2.0\r\n" 12 | "PRODID:-//PYVOBJECT//NONSGML Version %s//EN\r\n" 13 | "BEGIN:VEVENT\r\n" 14 | "UID:Not very random UID\r\n" 15 | "DTSTART:20060509T000000\r\n" 16 | "ATTENDEE;CN=Fröhlich:mailto:froelich@example.com\r\n" 17 | "CREATED:20060101T180000Z\r\n" 18 | "DESCRIPTION:Test event\r\n" 19 | "DTSTAMP:20170626T000000Z\r\n" 20 | "END:VEVENT\r\n" 21 | "END:VCALENDAR\r\n" 22 | ) 23 | 24 | us_pacific = ( 25 | "BEGIN:VTIMEZONE\r\n" 26 | "TZID:US/Pacific\r\n" 27 | "BEGIN:STANDARD\r\n" 28 | "DTSTART:19671029T020000\r\n" 29 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 30 | "TZOFFSETFROM:-0700\r\n" 31 | "TZOFFSETTO:-0800\r\n" 32 | "TZNAME:PST\r\n" 33 | "END:STANDARD\r\n" 34 | "BEGIN:DAYLIGHT\r\n" 35 | "DTSTART:19870405T020000\r\n" 36 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\n" 37 | "TZOFFSETFROM:-0800\r\n" 38 | "TZOFFSETTO:-0700\r\n" 39 | "TZNAME:PDT\r\n" 40 | "END:DAYLIGHT\r\n" 41 | "END:VTIMEZONE\r\n" 42 | ) 43 | 44 | utf8_test = ( 45 | "BEGIN:VCALENDAR\r\n" 46 | "METHOD:PUBLISH\r\n" 47 | "CALSCALE:GREGORIAN\r\n" 48 | "PRODID:-//EVDB//www.evdb.com//EN\r\n" 49 | "VERSION:2.0\r\n" 50 | "X-WR-CALNAME:EVDB Event Feed\r\n" 51 | "BEGIN:VEVENT\r\n" 52 | "DTSTART:20060922T000100Z\r\n" 53 | "DTEND:20060922T050100Z\r\n" 54 | "DTSTAMP:20050914T163414Z\r\n" 55 | "SUMMARY:The title こんにちはキティ\r\n" 56 | "DESCRIPTION:hello\\nHere is a description\\n\\n\\nこんにちはキティ\r\n" 57 | " \\n\\n\\n\\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond\r\n" 58 | " erbar-klingende Netzsichtbarmachungöffnung\\, an einer interessanten F\r\n" 59 | " irma im Gebäude\\, in dem ich angerufenen Semantic Research bearbeite.\r\n" 60 | " 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof\r\n" 61 | " tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei\r\n" 62 | " ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark\r\n" 63 | " und die Projekte sind sehr kühl und schließen irgendeinen Spielraum\r\n" 64 | " ein. Wenn ich Ihnen irgendwie mehr erkläre\\, muß ich Sie töten. Ps\r\n" 65 | " . Tat schnell -- jemand ist\\, wenn es hier interviewt\\, wie ich dieses\r\n" 66 | " schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft\r\n" 67 | " ware Engineer FIRMA: Semantische Forschung\\, Inc. REPORTS ZU: Vizeprä\r\n" 68 | " sident\\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE:\r\n" 69 | " www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT\r\n" 70 | " ERGRUND Semantische Forschung ist der führende Versorger der semantis\r\n" 71 | " cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung \r\n" 72 | " Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\\, zur reg.\\, zum\r\n" 73 | " EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS\r\n" 74 | " AMMENFASSUNG IN POSITION Semantische Forschung\\, Inc. basiert in San D\r\n" 75 | " iego\\, Ca im alten realen Weltsan Diego Haus...\\, das wir den Weltbest\r\n" 76 | " en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\\,\r\n" 77 | " um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri\r\n" 78 | " e zu unterstützen.\r\n" 79 | "LOCATION:こんにちはキティ\r\n" 80 | "SEQUENCE:0\r\n" 81 | "UID:E0-001-000276068-2\r\n" 82 | "END:VEVENT\r\n" 83 | "END:VCALENDAR\r\n" 84 | ) 85 | 86 | journal = ( 87 | "BEGIN:VJOURNAL\r\n" 88 | "UID:19970901T130000Z-123405@example.com\r\n" 89 | "DTSTAMP:19970901T130000Z\r\n" 90 | "DTSTART;VALUE=DATE:19970317\r\n" 91 | "SUMMARY:Staff meeting minutes\r\n" 92 | "DESCRIPTION:1. Staff meeting: Participants include Joe\\,\r\n" 93 | " Lisa\\, and Bob. Aurora project plans were reviewed.\r\n" 94 | " There is currently no budget reserves for this project.\r\n" 95 | " Lisa will escalate to management. Next meeting on Tuesday.\\n\r\n" 96 | " 2. Telephone Conference: ABC Corp. sales representative\r\n" 97 | " called to discuss new printer. Promised to get us a demo by\r\n" 98 | " Friday.\\n3. Henry Miller (Handsoff Insurance): Car was\r\n" 99 | " totaled by tree. Is looking into a loaner car. 555-2323\r\n" 100 | " (tel).\r\n" 101 | "END:VJOURNAL\r\n" 102 | ) 103 | 104 | 105 | def test_scratch_build(): 106 | """ 107 | CreateCalendar 2.0 format from scratch 108 | """ 109 | cal = vobject.base.newFromBehavior("vcalendar", "2.0") 110 | cal.add("vevent") 111 | cal.vevent.add("dtstart").value = datetime.datetime(2006, 5, 9) 112 | cal.vevent.add("description").value = "Test event" 113 | cal.vevent.add("created").value = datetime.datetime( 114 | 2006, 1, 1, 10, tzinfo=dateutil.tz.tzical(io.StringIO(us_pacific)).get("US/Pacific") 115 | ) 116 | cal.vevent.add("uid").value = "Not very random UID" 117 | cal.vevent.add("dtstamp").value = datetime.datetime(2017, 6, 26, 0, tzinfo=datetime.timezone.utc) 118 | 119 | cal.vevent.add("attendee").value = "mailto:froelich@example.com" 120 | cal.vevent.attendee.params["CN"] = ["Fröhlich"] 121 | 122 | # Note we're normalizing line endings, because no one got time for that. 123 | assert cal.serialize().replace("\r\n", "\n") == simple_2_0_test.replace("\r\n", "\n") % vobject.VERSION 124 | 125 | 126 | def test_unicode(): 127 | """ 128 | Test unicode characters 129 | """ 130 | vevent = vobject.base.readOne(utf8_test).vevent 131 | vevent2 = vobject.base.readOne(vevent.serialize()) 132 | 133 | assert str(vevent) == str(vevent2) 134 | assert vevent.summary.value == "The title こんにちはキティ" 135 | 136 | 137 | def test_wrapping(): 138 | """ 139 | Should support input file with a long text field covering multiple lines 140 | """ 141 | vobj = vobject.base.readOne(journal) 142 | vjournal = vobject.base.readOne(vobj.serialize()) 143 | assert "Joe, Lisa, and Bob" in vjournal.description.value 144 | assert "Tuesday.\n2." in vjournal.description.value 145 | 146 | 147 | def test_multiline(): 148 | """ 149 | Multi-text serialization test 150 | """ 151 | category = vobject.base.newFromBehavior("categories") 152 | category.value = ["Random category"] 153 | assert category.serialize().strip() == "CATEGORIES:Random category" 154 | 155 | category.value.append("Other category") 156 | assert category.serialize().strip() == "CATEGORIES:Random category,Other category" 157 | 158 | 159 | def test_semicolon_separated(): 160 | """ 161 | Semi-colon separated multi-text serialization test 162 | """ 163 | request_status = vobject.base.newFromBehavior("request-status") 164 | request_status.value = ["5.1", "Service unavailable"] 165 | assert request_status.serialize().strip() == "REQUEST-STATUS:5.1;Service unavailable" 166 | 167 | 168 | def test_unicode_multiline(): 169 | """ 170 | Test multiline unicode characters 171 | """ 172 | cal = vobject.iCalendar() 173 | cal.add("method").value = "REQUEST" 174 | cal.add("vevent") 175 | cal.vevent.add("created").value = datetime.datetime.now() 176 | cal.vevent.add("summary").value = "Классное событие" 177 | cal.vevent.add("description").value = ( 178 | "Классное событие Классное событие Классное событие Классное событие Классное событие Классsdssdное событие" 179 | ) 180 | 181 | # json tries to encode as utf-8 and it would break if some chars could not be encoded 182 | json.dumps(cal.serialize()) 183 | 184 | 185 | def test_ical_to_hcal(): 186 | """ 187 | Serializing iCalendar to hCalendar. 188 | 189 | Since Hcalendar is experimental and the behavior doesn't seem to want to load, 190 | This test will have to wait. 191 | 192 | 193 | tzs = dateutil.tz.tzical("test_files/timezones.ics") 194 | cal = base.newFromBehavior('hcalendar') 195 | self.assertEqual( 196 | str(cal.behavior), 197 | "" 198 | ) 199 | cal.add('vevent') 200 | cal.vevent.add('summary').value = "this is a note" 201 | cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" 202 | cal.vevent.add('dtstart').value = datetime.date(2006,2,27) 203 | cal.vevent.add('location').value = "a place" 204 | cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) 205 | 206 | event2 = cal.add('vevent') 207 | event2.add('summary').value = "Another one" 208 | event2.add('description').value = "The greatest thing ever!" 209 | event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) 210 | event2.add('location').value = "somewhere else" 211 | event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) 212 | hcal = cal.serialize() 213 | """ 214 | # self.assertEqual( 215 | # str(hcal), 216 | # """ 217 | # 218 | # this is a note: 219 | # Monday, February 27 220 | # - Tuesday, February 28 221 | # at a place 222 | # 223 | # 224 | # 225 | # Another one: 226 | # Thursday, December 17, 16:42 227 | # - Wednesday, December 23, 16:42 228 | # at somewhere else 229 | #
The greatest thing ever!
230 | #
231 | # """ 232 | # ) 233 | -------------------------------------------------------------------------------- /tests/test_change_tz.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import dateutil 4 | 5 | from vobject.change_tz import change_tz 6 | 7 | 8 | class StubCal: 9 | class StubEvent: 10 | class Node: 11 | def __init__(self, value): 12 | self.value = value 13 | 14 | def __init__(self, dtstart, dtend): 15 | self.dtstart = self.Node(dtstart) 16 | self.dtend = self.Node(dtend) 17 | 18 | def __init__(self, dates): 19 | """ 20 | dates is a list of tuples (dtstart, dtend) 21 | """ 22 | self.vevent_list = [self.StubEvent(*d) for d in dates] 23 | 24 | 25 | def test_change_tz(): 26 | """ 27 | Change the timezones of events in a component to a different 28 | timezone 29 | """ 30 | 31 | # Setup - create a stub vevent list 32 | old_tz = dateutil.tz.gettz("UTC") # 0:00 33 | new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 34 | 35 | dates = [ 36 | ( 37 | datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), 38 | datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz), 39 | ), 40 | ( 41 | datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), 42 | datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz), 43 | ), 44 | ] 45 | 46 | cal = StubCal(dates) 47 | 48 | # Exercise - change the timezone 49 | change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) 50 | 51 | # Test - that the tzs were converted correctly 52 | expected_new_dates = [ 53 | ( 54 | datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), 55 | datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), 56 | ), 57 | ( 58 | datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), 59 | datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz), 60 | ), 61 | ] 62 | 63 | for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): 64 | assert vevent.dtstart.value == expected_datepair[0] 65 | assert vevent.dtend.value == expected_datepair[1] 66 | 67 | 68 | def test_change_tz_utc_only(): 69 | """ 70 | Change any UTC timezones of events in a component to a different 71 | timezone 72 | """ 73 | 74 | # Setup - create a stub vevent list 75 | utc_tz = dateutil.tz.gettz("UTC") # 0:00 76 | non_utc_tz = dateutil.tz.gettz("America/Santiago") # -4:00 77 | new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 78 | 79 | dates = [ 80 | ( 81 | datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), 82 | datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz), 83 | ) 84 | ] 85 | 86 | cal = StubCal(dates) 87 | 88 | # Exercise - change the timezone passing utc_only=True 89 | change_tz(cal, new_tz, dateutil.tz.gettz("UTC"), utc_only=True) 90 | 91 | # Test - that only the utc item has changed 92 | expected_new_dates = [(datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), dates[0][1])] 93 | 94 | for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): 95 | assert vevent.dtstart.value == expected_datepair[0] 96 | assert vevent.dtend.value == expected_datepair[1] 97 | 98 | 99 | def test_change_tz_default(): 100 | """ 101 | Change the timezones of events in a component to a different 102 | timezone, passing a default timezone that is assumed when the events 103 | don't have one 104 | """ 105 | 106 | # Setup - create a stub vevent list 107 | new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 108 | 109 | dates = [ 110 | ( 111 | datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), 112 | datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None), 113 | ) 114 | ] 115 | 116 | cal = StubCal(dates) 117 | 118 | # Exercise - change the timezone 119 | change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) 120 | 121 | # Test - that the tzs were converted correctly 122 | expected_new_dates = [ 123 | ( 124 | datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), 125 | datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), 126 | ) 127 | ] 128 | 129 | for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): 130 | assert vevent.dtstart.value == expected_datepair[0] 131 | assert vevent.dtend.value == expected_datepair[1] 132 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from typing import List 3 | 4 | 5 | class Cli: 6 | ics_diff = "ics_diff" 7 | change_tz = "change_tz" 8 | 9 | 10 | def run_cli_tool(toolname: str, args: List[str]): 11 | return subprocess.run([toolname] + args, capture_output=True, text=True, check=False) 12 | 13 | 14 | def test_change_tz(): 15 | result = run_cli_tool(Cli.change_tz, ["--version"]) 16 | assert result.returncode == 0 17 | 18 | result = run_cli_tool(Cli.change_tz, []) 19 | assert result.returncode == 2 20 | assert "one of the arguments -l/--list ics_file is required" in result.stderr 21 | 22 | 23 | def test_ics_diff(): 24 | result = run_cli_tool(Cli.ics_diff, ["--version"]) 25 | assert result.returncode == 0 26 | 27 | result = run_cli_tool(Cli.ics_diff, []) 28 | assert result.returncode == 2 29 | assert "required: ics_file1, ics_file2" in result.stderr 30 | -------------------------------------------------------------------------------- /tests/test_icalendar.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import io 3 | import re 4 | 5 | import dateutil 6 | import pytest 7 | 8 | import vobject 9 | 10 | # Only available from CPython 3.9 onwards 11 | try: 12 | import zoneinfo 13 | except ImportError: 14 | pass 15 | 16 | 17 | timezones = ( 18 | "BEGIN:VTIMEZONE\r\n" 19 | "TZID:US/Pacific\r\n" 20 | "BEGIN:STANDARD\r\n" 21 | "DTSTART:19671029T020000\r\n" 22 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 23 | "TZOFFSETFROM:-0700\r\n" 24 | "TZOFFSETTO:-0800\r\n" 25 | "TZNAME:PST\r\n" 26 | "END:STANDARD\r\n" 27 | "BEGIN:DAYLIGHT\r\n" 28 | "DTSTART:19870405T020000\r\n" 29 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\n" 30 | "TZOFFSETFROM:-0800\r\n" 31 | "TZOFFSETTO:-0700\r\n" 32 | "TZNAME:PDT\r\n" 33 | "END:DAYLIGHT\r\n" 34 | "END:VTIMEZONE\r\n" 35 | "\r\n" 36 | "BEGIN:VTIMEZONE\r\n" 37 | "TZID:US/Eastern\r\n" 38 | "BEGIN:STANDARD\r\n" 39 | "DTSTART:19671029T020000\r\n" 40 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 41 | "TZOFFSETFROM:-0400\r\n" 42 | "TZOFFSETTO:-0500\r\n" 43 | "TZNAME:EST\r\n" 44 | "END:STANDARD\r\n" 45 | "BEGIN:DAYLIGHT\r\n" 46 | "DTSTART:19870405T020000\r\n" 47 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\n" 48 | "TZOFFSETFROM:-0500\r\n" 49 | "TZOFFSETTO:-0400\r\n" 50 | "TZNAME:EDT\r\n" 51 | "END:DAYLIGHT\r\n" 52 | "END:VTIMEZONE\r\n" 53 | "\r\n" 54 | "BEGIN:VTIMEZONE\r\n" 55 | "TZID:Santiago\r\n" 56 | "BEGIN:STANDARD\r\n" 57 | "DTSTART:19700314T000000\r\n" 58 | "TZOFFSETFROM:-0300\r\n" 59 | "TZOFFSETTO:-0400\r\n" 60 | "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA\r\n" 61 | "TZNAME:Pacific SA Standard Time\r\n" 62 | "END:STANDARD\r\n" 63 | "BEGIN:DAYLIGHT\r\n" 64 | "DTSTART:19701010T000000\r\n" 65 | "TZOFFSETFROM:-0400\r\n" 66 | "TZOFFSETTO:-0300\r\n" 67 | "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA\r\n" 68 | "TZNAME:Pacific SA Daylight Time\r\n" 69 | "END:DAYLIGHT\r\n" 70 | "END:VTIMEZONE\r\n" 71 | "\r\n" 72 | "BEGIN:VTIMEZONE\r\n" 73 | "TZID:W. Europe\r\n" 74 | "BEGIN:STANDARD\r\n" 75 | "DTSTART:19701025T030000\r\n" 76 | "TZOFFSETFROM:+0200\r\n" 77 | "TZOFFSETTO:+0100\r\n" 78 | "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\n" 79 | "TZNAME:W. Europe Standard Time\r\n" 80 | "END:STANDARD\r\n" 81 | "BEGIN:DAYLIGHT\r\n" 82 | "DTSTART:19700329T020000\r\n" 83 | "TZOFFSETFROM:+0100\r\n" 84 | "TZOFFSETTO:+0200\r\n" 85 | "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\n" 86 | "TZNAME:W. Europe Daylight Time\r\n" 87 | "END:DAYLIGHT\r\n" 88 | "END:VTIMEZONE\r\n" 89 | "\r\n" 90 | "BEGIN:VTIMEZONE\r\n" 91 | "TZID:US/Fictitious-Eastern\r\n" 92 | "LAST-MODIFIED:19870101T000000Z\r\n" 93 | "BEGIN:STANDARD\r\n" 94 | "DTSTART:19671029T020000\r\n" 95 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 96 | "TZOFFSETFROM:-0400\r\n" 97 | "TZOFFSETTO:-0500\r\n" 98 | "TZNAME:EST\r\n" 99 | "END:STANDARD\r\n" 100 | "BEGIN:DAYLIGHT\r\n" 101 | "DTSTART:19870405T020000\r\n" 102 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z\r\n" 103 | "TZOFFSETFROM:-0500\r\n" 104 | "TZOFFSETTO:-0400\r\n" 105 | "TZNAME:EDT\r\n" 106 | "END:DAYLIGHT\r\n" 107 | "END:VTIMEZONE\r\n" 108 | "\r\n" 109 | "BEGIN:VTIMEZONE\r\n" 110 | "TZID:America/Montreal\r\n" 111 | "LAST-MODIFIED:20051013T233643Z\r\n" 112 | "BEGIN:DAYLIGHT\r\n" 113 | "DTSTART:20050403T070000\r\n" 114 | "TZOFFSETTO:-0400\r\n" 115 | "TZOFFSETFROM:+0000\r\n" 116 | "TZNAME:EDT\r\n" 117 | "END:DAYLIGHT\r\n" 118 | "BEGIN:STANDARD\r\n" 119 | "DTSTART:20051030T020000\r\n" 120 | "TZOFFSETTO:-0500\r\n" 121 | "TZOFFSETFROM:-0400\r\n" 122 | "TZNAME:EST\r\n" 123 | "END:STANDARD\r\n" 124 | "END:VTIMEZONE\r\n" 125 | ) 126 | 127 | us_eastern = ( 128 | "BEGIN:VTIMEZONE\r\n" 129 | "TZID:US/Eastern\r\n" 130 | "BEGIN:STANDARD\r\n" 131 | "DTSTART:20001029T020000\r\n" 132 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z\r\n" 133 | "TZNAME:EST\r\n" 134 | "TZOFFSETFROM:-0400\r\n" 135 | "TZOFFSETTO:-0500\r\n" 136 | "END:STANDARD\r\n" 137 | "BEGIN:STANDARD\r\n" 138 | "DTSTART:20071104T020000\r\n" 139 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n" 140 | "TZNAME:EST\r\n" 141 | "TZOFFSETFROM:-0400\r\n" 142 | "TZOFFSETTO:-0500\r\n" 143 | "END:STANDARD\r\n" 144 | "BEGIN:DAYLIGHT\r\n" 145 | "DTSTART:20000402T020000\r\n" 146 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z\r\n" 147 | "TZNAME:EDT\r\n" 148 | "TZOFFSETFROM:-0500\r\n" 149 | "TZOFFSETTO:-0400\r\n" 150 | "END:DAYLIGHT\r\n" 151 | "BEGIN:DAYLIGHT\r\n" 152 | "DTSTART:20070311T020000\r\n" 153 | "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n" 154 | "TZNAME:EDT\r\n" 155 | "TZOFFSETFROM:-0500\r\n" 156 | "TZOFFSETTO:-0400\r\n" 157 | "END:DAYLIGHT\r\n" 158 | "END:VTIMEZONE\r\n" 159 | ) 160 | 161 | availability = ( 162 | "BEGIN:VAVAILABILITY\r\n" 163 | "UID:test\r\n" 164 | "DTSTART:20060216T000000Z\r\n" 165 | "DTEND:20060217T000000Z\r\n" 166 | "BEGIN:AVAILABLE\r\n" 167 | "UID:test1\r\n" 168 | "DTSTART:20060216T090000Z\r\n" 169 | "DTEND:20060216T120000Z\r\n" 170 | "DTSTAMP:20060215T000000Z\r\n" 171 | "SUMMARY:Available in the morning\r\n" 172 | "END:AVAILABLE\r\n" 173 | "BUSYTYPE:BUSY\r\n" 174 | "DTSTAMP:20060215T000000Z\r\n" 175 | "END:VAVAILABILITY\r\n" 176 | ) 177 | 178 | freebusy = ( 179 | "BEGIN:VFREEBUSY\r\n" 180 | "UID:test\r\n" 181 | "DTSTART:20060216T010000Z\r\n" 182 | "DTEND:20060216T030000Z\r\n" 183 | "DTSTAMP:20060215T000000Z\r\n" 184 | "FREEBUSY:20060216T010000Z/PT1H\r\n" 185 | "FREEBUSY:20060216T010000Z/20060216T030000Z\r\n" 186 | "END:VFREEBUSY\r\n" 187 | ) 188 | 189 | recurrence = ( 190 | "BEGIN:VCALENDAR\r\n" 191 | "VERSION\r\n" 192 | " :2.0\r\n" 193 | "PRODID\r\n" 194 | " :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN\r\n" 195 | "BEGIN:VEVENT\r\n" 196 | "CREATED\r\n" 197 | " :20060327T214227Z\r\n" 198 | "LAST-MODIFIED\r\n" 199 | " :20060313T080829Z\r\n" 200 | "DTSTAMP\r\n" 201 | " :20060116T231602Z\r\n" 202 | "UID\r\n" 203 | " :70922B3051D34A9E852570EC00022388\r\n" 204 | "SUMMARY\r\n" 205 | " :Monthly - All Hands Meeting with Joe Smith\r\n" 206 | "STATUS\r\n" 207 | " :CONFIRMED\r\n" 208 | "CLASS\r\n" 209 | " :PUBLIC\r\n" 210 | "RRULE\r\n" 211 | " :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH\r\n" 212 | "DTSTART\r\n" 213 | " :20060126T230000Z\r\n" 214 | "DTEND\r\n" 215 | " :20060127T000000Z\r\n" 216 | "DESCRIPTION\r\n" 217 | " :Repeat Meeting: - Occurs every 4th Thursday of each month\r\n" 218 | "END:VEVENT\r\n" 219 | "END:VCALENDAR\r\n" 220 | ) 221 | 222 | recurrence_without_tz = ( 223 | "BEGIN:VCALENDAR\r\n" 224 | "VERSION:2.0\r\n" 225 | "BEGIN:VEVENT\r\n" 226 | "DTSTART;VALUE=DATE:20130117\r\n" 227 | "DTEND;VALUE=DATE:20130118\r\n" 228 | "RRULE:FREQ=WEEKLY;UNTIL=20130330;BYDAY=TH\r\n" 229 | "SUMMARY:Meeting\r\n" 230 | "END:VEVENT\r\n" 231 | "END:VCALENDAR\r\n" 232 | ) 233 | 234 | recurrence_offset_naive = ( 235 | "BEGIN:VCALENDAR\r\n" 236 | "VERSION:2.0\r\n" 237 | "BEGIN:VEVENT\r\n" 238 | "DTSTART;VALUE=DATE:20130117\r\n" 239 | "DTEND;VALUE=DATE:20130118\r\n" 240 | "RRULE:FREQ=WEEKLY;UNTIL=20130330T230000Z;BYDAY=TH\r\n" 241 | "SUMMARY:Meeting\r\n" 242 | "END:VEVENT\r\n" 243 | "END:VCALENDAR\r\n" 244 | ) 245 | 246 | vobject_0050 = ( 247 | "BEGIN:VCALENDAR\r\n" 248 | "PRODID:-//Force.com Labs//iCalendar Export//EN\r\n" 249 | "VERSION:2.0\r\n" 250 | "METHOD: REQUEST\r\n" 251 | "CALSCALE:GREGORIAN\r\n" 252 | "BEGIN:VEVENT\r\n" 253 | "STATUS:CONFIRMED\r\n" 254 | "ORGANIZER;CN=Wells Fargo and Company:mailto:appointments@wellsfargo.com\r\n" 255 | "UID:appointments@wellsfargo.com\r\n" 256 | "LOCATION:POJOAQUE\r\n" 257 | "CREATED:20240812T192015Z\r\n" 258 | "DTSTART:20240812T213000Z\r\n" 259 | "DTEND: 20240812T223000Z\r\n" 260 | "TRANSP:OPAQUE\r\n" 261 | "DURATION:PT60M\r\n" 262 | "SUMMARY:Personal: Open a new account\r\n" 263 | "DTSTAMP:20240812T192015Z\r\n" 264 | "LAST-MODIFIED:20240812T192015Z\r\n" 265 | "SEQUENCE:0\r\n" 266 | "DESCRIPTION:Personal: Open a new account\r\n" 267 | "END:VEVENT\r\n" 268 | "END:VCALENDAR\r\n" 269 | ) 270 | 271 | 272 | def test_parse_dtstart(): 273 | """ 274 | Should take a content line and return a datetime object. 275 | """ 276 | assert vobject.icalendar.parseDtstart( 277 | vobject.base.textLineToContentLine("DTSTART:20060509T000000") 278 | ) == datetime.datetime(2006, 5, 9, 0, 0) 279 | 280 | 281 | def test_regexes(): 282 | """ 283 | Test regex patterns 284 | """ 285 | assert re.findall(vobject.base.P_NAME, "12foo-bar:yay") == ["12foo-bar", "yay"] 286 | assert re.findall(vobject.base.P_SAFE_CHAR, 'a;b"*,cd') == ["a", "b", "*", "c", "d"] 287 | assert re.findall(vobject.base.P_QSAFE_CHAR, 'a;b"*,cd') == ["a", ";", "b", "*", ",", "c", "d"] 288 | assert re.findall( 289 | vobject.base.P_PARAM_VALUE, '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE # black hack 290 | ) == ['"quoted"', "", "not-quoted", "", "start", "", "after-illegal-quote", ""] 291 | 292 | match = vobject.base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') 293 | assert match.group("value") == 'value:;"' 294 | assert match.group("name") == "TEST" 295 | assert match.group("params") == ';ALTREP="http://www.wiz.org"' 296 | 297 | 298 | def test_string_to_text_values(): 299 | """ 300 | Test string lists 301 | """ 302 | assert vobject.icalendar.stringToTextValues("") == [""] 303 | assert vobject.icalendar.stringToTextValues("abcd,efgh") == ["abcd", "efgh"] 304 | 305 | 306 | def test_string_to_period(): 307 | """ 308 | Test datetime strings 309 | """ 310 | assert vobject.icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z") == ( 311 | datetime.datetime(1997, 1, 1, 18, 0, tzinfo=datetime.timezone.utc), 312 | datetime.datetime(1997, 1, 2, 7, 0, tzinfo=datetime.timezone.utc), 313 | ) 314 | assert vobject.icalendar.stringToPeriod("19970101T180000Z/PT1H") == ( 315 | datetime.datetime(1997, 1, 1, 18, 0, tzinfo=datetime.timezone.utc), 316 | datetime.timedelta(0, 3600), 317 | ) 318 | 319 | 320 | def test_timedelta_to_string(): 321 | """ 322 | Test timedelta strings 323 | """ 324 | assert vobject.icalendar.timedeltaToString(datetime.timedelta(hours=2)) == "PT2H" 325 | assert vobject.icalendar.timedeltaToString(datetime.timedelta(minutes=20)) == "PT20M" 326 | 327 | 328 | def test_delta_to_offset(): 329 | """Test deltaToOffset() function.""" 330 | 331 | # Sydney 332 | delta = datetime.timedelta(hours=10) 333 | assert vobject.icalendar.deltaToOffset(delta) == "+1000" 334 | 335 | # New York 336 | delta = datetime.timedelta(hours=-5) 337 | assert vobject.icalendar.deltaToOffset(delta), "-0500" 338 | 339 | # Adelaide (see https://github.com/py-vobject/vobject/pull/12) 340 | delta = datetime.timedelta(hours=9, minutes=30) 341 | assert vobject.icalendar.deltaToOffset(delta), "+0930" 342 | 343 | 344 | def test_vtimezone_creation(): 345 | """ 346 | Test timezones 347 | """ 348 | tzs = dateutil.tz.tzical(io.StringIO(timezones)) 349 | pacific = vobject.icalendar.TimezoneComponent(tzs.get("US/Pacific")) 350 | assert str(pacific) == ">" 351 | 352 | santiago = vobject.icalendar.TimezoneComponent(tzs.get("Santiago")) 353 | assert str(santiago) == ">" 354 | 355 | for year in range(2001, 2010): 356 | for month in (2, 9): 357 | dt = datetime.datetime(year, month, 15, tzinfo=tzs.get("Santiago")) 358 | assert dt.replace(tzinfo=tzs.get("Santiago")) == dt 359 | 360 | 361 | def test_timezone_serializing(): 362 | """ 363 | Serializing with timezones test 364 | """ 365 | tzs = dateutil.tz.tzical(io.StringIO(timezones)) 366 | pacific = tzs.get("US/Pacific") 367 | cal = vobject.base.Component("VCALENDAR") 368 | cal.setBehavior(vobject.icalendar.VCalendar2_0) 369 | ev = cal.add("vevent") 370 | ev.add("dtstart").value = datetime.datetime(2005, 10, 12, 9, tzinfo=pacific) 371 | 372 | evruleset = dateutil.rrule.rruleset() 373 | evruleset.rrule( 374 | dateutil.rrule.rrule( 375 | dateutil.rrule.WEEKLY, interval=2, byweekday=[2, 4], until=datetime.datetime(2005, 12, 15, 9) 376 | ) 377 | ) 378 | evruleset.rrule(dateutil.rrule.rrule(dateutil.rrule.MONTHLY, bymonthday=[-1, -5])) 379 | evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) 380 | ev.rruleset = evruleset 381 | ev.add("duration").value = datetime.timedelta(hours=1) 382 | 383 | apple = tzs.get("America/Montreal") 384 | ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) 385 | 386 | 387 | def test_pytz_timezone_serializing(): 388 | """ 389 | Serializing with timezones from pytz test 390 | """ 391 | try: 392 | import pytz 393 | except ImportError: 394 | pytest.skip("pytz not installed") 395 | 396 | # Avoid conflicting cached tzinfo from other tests 397 | def unregister_tzid(tzid): 398 | """Clear tzid from icalendar TZID registry""" 399 | if vobject.icalendar.getTzid(tzid, False): 400 | vobject.icalendar.registerTzid(tzid, None) 401 | 402 | unregister_tzid("US/Eastern") 403 | eastern = pytz.timezone("US/Eastern") 404 | cal = vobject.base.Component("VCALENDAR") 405 | cal.setBehavior(vobject.icalendar.VCalendar2_0) 406 | ev = cal.add("vevent") 407 | ev.add("dtstart").value = eastern.localize(datetime.datetime(2008, 10, 12, 9)) 408 | serialized = cal.serialize() 409 | 410 | assert us_eastern.replace("\r\n", "\n") in serialized.replace("\r\n", "\n") 411 | 412 | # Exhaustively test all zones (just looking for no errors) 413 | for tzname in pytz.all_timezones: 414 | unregister_tzid(tzname) 415 | tz = vobject.icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) 416 | tz.serialize() 417 | 418 | 419 | def test_free_busy(): 420 | """ 421 | Test freebusy components 422 | """ 423 | vfb = vobject.base.newFromBehavior("VFREEBUSY") 424 | vfb.add("uid").value = "test" 425 | vfb.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=dateutil.tz.tzutc()) 426 | vfb.add("dtstart").value = datetime.datetime(2006, 2, 16, 1, tzinfo=dateutil.tz.tzutc()) 427 | vfb.add("dtend").value = vfb.dtstart.value + datetime.timedelta(hours=2) 428 | vfb.add("freebusy").value = [(vfb.dtstart.value, datetime.timedelta(hours=1))] 429 | vfb.add("freebusy").value = [(vfb.dtstart.value, vfb.dtend.value)] 430 | 431 | assert vfb.serialize().replace("\r\n", "\n") == freebusy.replace("\r\n", "\n") 432 | 433 | 434 | def test_availability(): 435 | """ 436 | Test availability components 437 | """ 438 | vcal = vobject.base.newFromBehavior("VAVAILABILITY") 439 | vcal.add("uid").value = "test" 440 | vcal.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=dateutil.tz.tzutc()) 441 | vcal.add("dtstart").value = datetime.datetime(2006, 2, 16, 0, tzinfo=dateutil.tz.tzutc()) 442 | vcal.add("dtend").value = datetime.datetime(2006, 2, 17, 0, tzinfo=dateutil.tz.tzutc()) 443 | vcal.add("busytype").value = "BUSY" 444 | 445 | av = vobject.base.newFromBehavior("AVAILABLE") 446 | av.add("uid").value = "test1" 447 | av.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=dateutil.tz.tzutc()) 448 | av.add("dtstart").value = datetime.datetime(2006, 2, 16, 9, tzinfo=dateutil.tz.tzutc()) 449 | av.add("dtend").value = datetime.datetime(2006, 2, 16, 12, tzinfo=dateutil.tz.tzutc()) 450 | av.add("summary").value = "Available in the morning" 451 | vcal.add(av) 452 | 453 | assert vcal.serialize().replace("\r\n", "\n") == availability.replace("\r\n", "\n") 454 | 455 | 456 | def test_recurrence(): 457 | """ 458 | Ensure date valued UNTILs in rrules are in a reasonable timezone, 459 | and include that day (12/28 in this test) 460 | """ 461 | cal = vobject.base.readOne(recurrence) 462 | dates = list(cal.vevent.getrruleset()) 463 | assert dates[0] == datetime.datetime(2006, 1, 26, 23, 0, tzinfo=dateutil.tz.tzutc()) 464 | assert dates[1] == datetime.datetime(2006, 2, 23, 23, 0, tzinfo=dateutil.tz.tzutc()) 465 | assert dates[-1] == datetime.datetime(2006, 12, 28, 23, 0, tzinfo=dateutil.tz.tzutc()) 466 | 467 | 468 | def test_recurring_component(): 469 | """ 470 | Test recurring events 471 | """ 472 | # init 473 | vevent = vobject.icalendar.RecurringComponent(name="VEVENT") 474 | assert vevent.isNative 475 | 476 | # rruleset should be None at this point. 477 | # No rules have been passed or created. 478 | assert vevent.rruleset is None 479 | 480 | # Now add start and rule for recurring event 481 | vevent.add("dtstart").value = datetime.datetime(2005, 1, 19, 9) 482 | vevent.add("rrule").value = "FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" 483 | assert list(vevent.rruleset) == [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] 484 | assert list(vevent.getrruleset(addRDate=True)) == [ 485 | datetime.datetime(2005, 1, 19, 9, 0), 486 | datetime.datetime(2005, 1, 20, 9, 0), 487 | ] 488 | 489 | # Also note that dateutil will expand all-day events (datetime.date values) 490 | # to datetime.datetime value with time 0 and no timezone. 491 | vevent.dtstart.value = datetime.date(2005, 3, 18) 492 | assert list(vevent.rruleset) == [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] 493 | assert list(vevent.getrruleset(True)) == [ 494 | datetime.datetime(2005, 3, 18, 0, 0), 495 | datetime.datetime(2005, 3, 29, 0, 0), 496 | ] 497 | 498 | 499 | def test_recurrence_without_tz(): 500 | """ 501 | Test recurring vevent missing any time zone definitions. 502 | """ 503 | 504 | cal = vobject.base.readOne(recurrence_without_tz) 505 | dates = list(cal.vevent.getrruleset()) 506 | assert dates[0] == datetime.datetime(2013, 1, 17, 0, 0) 507 | assert dates[1] == datetime.datetime(2013, 1, 24, 0, 0) 508 | assert dates[-1] == datetime.datetime(2013, 3, 28, 0, 0) 509 | 510 | 511 | def test_recurrence_offset_naive(): 512 | """ 513 | Ensure recurring vevent missing some time zone definitions is 514 | parsing. See issue #75. 515 | """ 516 | cal = vobject.base.readOne(recurrence_offset_naive) 517 | dates = list(cal.vevent.getrruleset()) 518 | assert dates[0] == datetime.datetime(2013, 1, 17, 0, 0) 519 | assert dates[1] == datetime.datetime(2013, 1, 24, 0, 0) 520 | assert dates[-1] == datetime.datetime(2013, 3, 28, 0, 0) 521 | 522 | 523 | def test_issue50(): 524 | """ 525 | Ensure leading spaces in a DATE-TIME value are ignored when not in 526 | strict mode. 527 | 528 | See https://github.com/py-vobject/vobject/issues/50 529 | """ 530 | cal = vobject.base.readOne(vobject_0050) 531 | assert cal.vevent.dtend.value == datetime.datetime(2024, 8, 12, 22, 30, tzinfo=dateutil.tz.tzutc()) 532 | 533 | 534 | def test_includes_dst_offset(): 535 | tz = dateutil.tz.gettz("US/Eastern") 536 | assert tz is not None 537 | 538 | # Simple first 539 | dt = datetime.datetime(2020, 1, 1) 540 | assert not vobject.icalendar.includes_dst_offset(tz, dt) 541 | dt = datetime.datetime(2020, 7, 1) 542 | assert vobject.icalendar.includes_dst_offset(tz, dt) 543 | 544 | # Leaving DST: 2024-11-03 02:00:00 reverts to 01:00 545 | 546 | 547 | def test_omits_dst_offset(): 548 | 549 | # Check dateutil, pytz, and zoneinfo (3.9+) tzinfo instances 550 | _timezones = [] 551 | if "dateutil" in globals(): 552 | tz = dateutil.tz.gettz("America/New_York") 553 | assert tz is not None 554 | _timezones.append(tz) 555 | if "zoneinfo" in globals(): 556 | tz = zoneinfo.ZoneInfo("America/New_York") 557 | assert tz is not None 558 | _timezones.append(tz) 559 | 560 | for tz in _timezones: 561 | dt = datetime.datetime(2020, 1, 1) 562 | assert vobject.icalendar.omits_dst_offset(tz, dt) 563 | 564 | dt = datetime.datetime(2020, 7, 1) 565 | assert not vobject.icalendar.omits_dst_offset(tz, dt) 566 | 567 | # Entering DST: 2024-03-10 02:00:00 advances to 03:00 568 | dt = datetime.datetime(2024, 3, 10, 1, 59, 59) 569 | assert vobject.icalendar.omits_dst_offset(tz, dt) 570 | 571 | dt = datetime.datetime(2024, 3, 10, 3, 0, 0) 572 | assert not vobject.icalendar.omits_dst_offset(tz, dt) 573 | 574 | dt = datetime.datetime(2024, 3, 10, 3, 0, 1) 575 | assert not vobject.icalendar.omits_dst_offset(tz, dt) 576 | 577 | # Leaving DST: 2024-11-03 02:00:00 reverts to 01:00:00 578 | dt = datetime.datetime(2024, 11, 3, 1, 0, 0) # fold=0 579 | assert not vobject.icalendar.omits_dst_offset(tz, dt) 580 | 581 | dt = datetime.datetime(2024, 11, 3, 1, 59, 59) # fold=0 582 | assert not vobject.icalendar.omits_dst_offset(tz, dt) 583 | 584 | dt = datetime.datetime(2024, 11, 3, 1, 0, 0, fold=1) 585 | assert vobject.icalendar.omits_dst_offset(tz, dt) 586 | 587 | dt = datetime.datetime(2024, 11, 3, 2, 0, 0, fold=1) 588 | assert vobject.icalendar.omits_dst_offset(tz, dt) 589 | 590 | dt = datetime.datetime(2024, 11, 3, 2, 0, 1, fold=0) 591 | assert vobject.icalendar.omits_dst_offset(tz, dt) 592 | 593 | 594 | def test_first_transition_all_match(): 595 | dts = [ 596 | datetime.datetime(2000, 1, 1, 0, 0, 0), 597 | datetime.datetime(2000, 1, 1, 1, 0, 0), 598 | datetime.datetime(2000, 1, 1, 2, 0, 0), 599 | datetime.datetime(2000, 1, 1, 3, 0, 0), 600 | ] 601 | 602 | # All datetimes have seconds value of zero, so match, so expecting 'None' 603 | result = vobject.icalendar.first_transition(dateutil.tz.tzutc(), dts, lambda tz, dt: dt.second == 0) 604 | assert result is None 605 | 606 | 607 | def test_first_transition_none_match(): 608 | dts = [ 609 | datetime.datetime(2000, 1, 1, 0, 0, 1), 610 | datetime.datetime(2000, 1, 1, 1, 0, 1), 611 | datetime.datetime(2000, 1, 1, 2, 0, 1), 612 | datetime.datetime(2000, 1, 1, 3, 0, 1), 613 | ] 614 | 615 | # No datetimes have seconds value of zero, so none match, so first? or last? match FIXME! 616 | result = vobject.icalendar.first_transition(dateutil.tz.tzutc(), dts, lambda tz, dt: dt.second == 0) 617 | assert result == dts[3] 618 | 619 | 620 | def test_first_transition_last_not_match(): 621 | dts = [ 622 | datetime.datetime(2000, 1, 1, 0, 0, 0), 623 | datetime.datetime(2000, 1, 1, 1, 0, 0), 624 | datetime.datetime(2000, 1, 1, 2, 0, 0), 625 | datetime.datetime(2000, 1, 1, 3, 0, 1), 626 | ] 627 | 628 | result = vobject.icalendar.first_transition(dateutil.tz.tzutc(), dts, lambda tz, dt: dt.second == 0) 629 | assert result == dts[3] 630 | 631 | 632 | def test_first_transition_first_not_match(): 633 | dts = [ 634 | datetime.datetime(2000, 1, 1, 0, 0, 1), 635 | datetime.datetime(2000, 1, 1, 1, 0, 0), 636 | datetime.datetime(2000, 1, 1, 2, 0, 0), 637 | datetime.datetime(2000, 1, 1, 3, 0, 0), 638 | ] 639 | 640 | result = vobject.icalendar.first_transition(dateutil.tz.tzutc(), dts, lambda tz, dt: dt.second == 0) 641 | assert result == dts[0] 642 | 643 | 644 | def test_first_transition_multi_not_match(): 645 | dts = [ 646 | datetime.datetime(2000, 1, 1, 0, 0, 0), 647 | datetime.datetime(2000, 1, 1, 1, 0, 1), 648 | datetime.datetime(2000, 1, 1, 2, 0, 0), 649 | datetime.datetime(2000, 1, 1, 3, 0, 1), 650 | ] 651 | 652 | result = vobject.icalendar.first_transition(dateutil.tz.tzutc(), dts, lambda tz, dt: dt.second == 0) 653 | assert result == dts[1] 654 | -------------------------------------------------------------------------------- /tests/test_vcards.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import vobject 4 | 5 | vcard_with_groups = ( 6 | "home.begin:vcard\r\n" 7 | "version:3.0\r\n" 8 | "source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE\r\n" 9 | "name:Meister Berger\r\n" 10 | "fn:Meister Berger\r\n" 11 | "n:Berger;Meister\r\n" 12 | "bday;value=date:1963-09-21\r\n" 13 | "o:Universitæt Görlitz\r\n" 14 | "title:Mayor\r\n" 15 | "title;language=de;value=text:Burgermeister\r\n" 16 | "note:The Mayor of the great city of\r\n" 17 | " Goerlitz in the great country of Germany.\\nNext line.\r\n" 18 | "email;internet:mb@goerlitz.de\r\n" 19 | "home.tel;type=fax,voice;type=msg:+49 3581 123456\r\n" 20 | "home.label:Hufenshlagel 1234\\n\r\n" 21 | " 02828 Goerlitz\\n\r\n" 22 | " Deutschland\r\n" 23 | "END:VCARD\r\n" 24 | ) 25 | 26 | simple_3_0_test = ( 27 | "BEGIN:VCARD\r\n" 28 | "VERSION:3.0\r\n" 29 | "FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto)\r\n" 30 | "N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto)\r\n" 31 | "NICKNAME:gnat and gnu and pluto\r\n" 32 | "BDAY;value=date:02-10\r\n" 33 | "TEL;type=HOME:+01-(0)2-765.43.21\r\n" 34 | "TEL;type=CELL:+01-(0)5-555.55.55\r\n" 35 | "ACCOUNT;type=HOME:010-1234567-05\r\n" 36 | "ADR;type=HOME:;;Haight Street 512\\;\\nEscape\\, Test;Novosibirsk;;80214;Gnuland\r\n" 37 | "TEL;type=HOME:+01-(0)2-876.54.32\r\n" 38 | "ORG:University of Novosibirsk;Department of Octopus Parthenogenesis\r\n" 39 | "END:VCARD\r\n" 40 | ) 41 | 42 | 43 | def test_vcard_creation(): 44 | """ 45 | Test creating a vCard 46 | """ 47 | vcard = vobject.base.newFromBehavior("vcard", "3.0") 48 | assert str(vcard) == "" 49 | 50 | 51 | def test_default_behavior(): 52 | """ 53 | Default behavior test. 54 | """ 55 | card = vobject.readOne(io.StringIO(vcard_with_groups)) 56 | assert vobject.base.getBehavior("note") is None 57 | assert ( 58 | str(card.note.value) == "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." 59 | ) 60 | 61 | 62 | def test_with_groups(): 63 | """ 64 | vCard groups test 65 | """ 66 | card = vobject.readOne(io.StringIO(vcard_with_groups)) 67 | assert str(card.group) == "home" 68 | assert str(card.tel.group) == "home" 69 | 70 | card.group = card.tel.group = "new" 71 | assert str(card.tel.serialize().strip()) == "new.TEL;TYPE=fax,voice,msg:+49 3581 123456" 72 | assert str(card.serialize().splitlines()[0]) == "new.BEGIN:VCARD" 73 | 74 | 75 | def test_vcard_3_parsing(): 76 | """ 77 | VCARD 3.0 parse test 78 | """ 79 | card = vobject.base.readOne(io.StringIO(simple_3_0_test)) 80 | # value not rendering correctly? 81 | # self.assertEqual( 82 | # card.adr.value, 83 | # "" 84 | # ) 85 | assert card.org.value == ["University of Novosibirsk", "Department of Octopus Parthenogenesis"] 86 | 87 | for _ in range(3): 88 | new_card = vobject.base.readOne(card.serialize()) 89 | assert new_card.org.value == card.org.value 90 | card = new_card 91 | -------------------------------------------------------------------------------- /tests/test_vobject.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import pytest 4 | 5 | import vobject 6 | 7 | ics_text = ( 8 | "BEGIN:VCALENDAR\r\n" 9 | "BEGIN:VEVENT\r\n" 10 | "SUMMARY;blah=hi!:Bastille Day Party\r\n" 11 | "END:VEVENT\r\n" 12 | "END:VCALENDAR\r\n" 13 | ) 14 | 15 | 16 | def test_read_components(): 17 | """ 18 | Test if reading components correctly 19 | """ 20 | cal = next(vobject.readComponents(io.StringIO(ics_text))) 21 | 22 | assert str(cal) == "]>]>" 23 | assert str(cal.vevent.summary) == "" 24 | 25 | 26 | def test_parse_line(): 27 | """ 28 | Test line parsing 29 | """ 30 | assert vobject.base.parseLine("BLAH:") == ("BLAH", [], "", None) 31 | assert vobject.base.parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904") == ( 32 | "RDATE", 33 | [], 34 | "VALUE=DATE:19970304,19970504,19970704,19970904", 35 | None, 36 | ) 37 | assert vobject.base.parseLine( 38 | 'DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA' 39 | ) == ( 40 | "DESCRIPTION", 41 | [["ALTREP", "http://www.wiz.org"]], 42 | "The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA", 43 | None, 44 | ) 45 | assert vobject.base.parseLine("EMAIL;PREF;INTERNET:john@nowhere.com") == ( 46 | "EMAIL", 47 | [["PREF"], ["INTERNET"]], 48 | "john@nowhere.com", 49 | None, 50 | ) 51 | assert vobject.base.parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com') == ( 52 | "EMAIL", 53 | [["TYPE", "blah", "hah"], ["INTERNET", "DIGI", "DERIDOO"]], 54 | "john@nowhere.com", 55 | None, 56 | ) 57 | assert vobject.base.parseLine("item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;") == ( 58 | "ADR", 59 | [["type", "HOME"], ["type", "pref"]], 60 | ";;Reeperbahn 116;Hamburg;;20359;", 61 | "item1", 62 | ) 63 | with pytest.raises(vobject.base.ParseError): 64 | vobject.base.parseLine(":") 65 | -------------------------------------------------------------------------------- /tests/test_vobject_parsing.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | import vobject 6 | 7 | silly_test_text = ( 8 | "sillyname:name\r\n" 9 | "profile:sillyprofile\r\n" 10 | "stuff:folded\r\n" 11 | " line\r\n" 12 | "morestuff;asinine:this line is not folded, but in practice probably ought to be, as it is exceptionally long, " 13 | "and moreover demonstratively stupid\r\n" 14 | ) 15 | 16 | standard_test_text = ( 17 | "BEGIN:VCALENDAR\r\n" 18 | "CALSCALE:GREGORIAN\r\n" 19 | "X-WR-TIMEZONE;VALUE=TEXT:US/Pacific\r\n" 20 | "METHOD:PUBLISH\r\n" 21 | "PRODID:-//Apple Computer\\, Inc//iCal 1.0//EN\r\n" 22 | "X-WR-CALNAME;VALUE=TEXT:Example\r\n" 23 | "VERSION:2.0\r\n" 24 | "BEGIN:VEVENT\r\n" 25 | "SEQUENCE:5\r\n" 26 | "DTSTART;TZID=US/Pacific:20021028T140000\r\n" 27 | "RRULE:FREQ=Weekly;COUNT=10\r\n" 28 | "DTSTAMP:20021028T011706Z\r\n" 29 | "SUMMARY:Coffee with Jason\r\n" 30 | "UID:EC9439B1-FF65-11D6-9973-003065F99D04\r\n" 31 | "DTEND;TZID=US/Pacific:20021028T150000\r\n" 32 | "BEGIN:VALARM\r\n" 33 | "TRIGGER;VALUE=DURATION:-P1D\r\n" 34 | "ACTION:DISPLAY\r\n" 35 | "DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n" 36 | "END:VALARM\r\n" 37 | "END:VEVENT\r\n" 38 | "BEGIN:VTIMEZONE\r\n" 39 | "X-LIC-LOCATION:Random location\r\n" 40 | "TZID:US/Pacific\r\n" 41 | "LAST-MODIFIED:19870101T000000Z\r\n" 42 | "BEGIN:STANDARD\r\n" 43 | "DTSTART:19671029T020000\r\n" 44 | "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 45 | "TZOFFSETFROM:-0700\r\n" 46 | "TZOFFSETTO:-0800\r\n" 47 | "TZNAME:PST\r\n" 48 | "END:STANDARD\r\n" 49 | "BEGIN:DAYLIGHT\r\n" 50 | "DTSTART:19870405T020000\r\n" 51 | "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\n" 52 | "TZOFFSETFROM:-0800\r\n" 53 | "TZOFFSETTO:-0700\r\n" 54 | "TZNAME:PDT\r\n" 55 | "END:DAYLIGHT\r\n" 56 | "END:VTIMEZONE\r\n" 57 | "END:VCALENDAR\r\n" 58 | ) 59 | 60 | bad_stream = ( 61 | "BEGIN:VCALENDAR\r\n" 62 | "CALSCALE:GREGORIAN\r\n" 63 | "X-WR-TIMEZONE;VALUE=TEXT:US/Pacific\r\n" 64 | "METHOD:PUBLISH\r\n" 65 | "PRODID:-//Apple Computer\\, Inc//iCal 1.0//EN\r\n" 66 | "X-WR-CALNAME;VALUE=TEXT:Example\r\n" 67 | "VERSION:2.0\r\n" 68 | "BEGIN:VEVENT\r\n" 69 | "DTSTART:20021028T140000Z\r\n" 70 | "BEGIN:VALARM\r\n" 71 | "TRIGGER:a20021028120000\r\n" 72 | "ACTION:DISPLAY\r\n" 73 | "DESCRIPTION:This trigger has a nonsensical value\r\n" 74 | "END:VALARM\r\n" 75 | "END:VEVENT\r\n" 76 | "END:VCALENDAR\r\n" 77 | ) 78 | 79 | bad_line = ( 80 | "BEGIN:VCALENDAR\r\n" 81 | "METHOD:PUBLISH\r\n" 82 | "VERSION:2.0\r\n" 83 | "BEGIN:VEVENT\r\n" 84 | "DTSTART:19870405T020000\r\n" 85 | "X-BAD/SLASH:TRUE\r\n" 86 | "X-BAD_UNDERSCORE:TRUE\r\n" 87 | "UID:EC9439B1-FF65-11D6-9973-003065F99D04\r\n" 88 | "END:VEVENT\r\n" 89 | "END:VCALENDAR\r\n" 90 | ) 91 | 92 | quoted_printable = ( 93 | "BEGIN:VCARD\r\n" 94 | "VERSION:2.1\r\n" 95 | "N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E9=BB=84;=E4=B8=96=E5=8B=87;;;\r\n" 96 | "FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E9=BB=84=E4=B8=96=E5=8B=87\r\n" 97 | "TEL;CELL:15810139237\r\n" 98 | "TEL;WORK:01088520374\r\n" 99 | "ADR;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;;;=E5=8C=97=E4=BA=AC=20=E4=B8=B0=E5=8F=B0=E5=8C=BA;;;\r\n" 100 | "URL;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=68=74=74=70=3A=2F=2F=77=65=69=62=6F=2E=63=6F=6D=2F=33=30=39=34=39=30=" 101 | "\r\n" 102 | "=30=34=33=33=3F=E9=97=AA=E9=97=AA=48=E7=BA=A2=E6=98=9F\r\n" 103 | "END:VCARD\r\n" 104 | ) 105 | 106 | 107 | def test_readOne(): 108 | """ 109 | Test reading first component of ics 110 | """ 111 | cal = silly_test_text 112 | silly = vobject.base.readOne(cal) 113 | assert ( 114 | str(silly) 115 | == ", , ]>" 117 | ) 118 | assert str(silly.stuff) == "" 119 | 120 | 121 | def test_importing(): 122 | """ 123 | Test importing ics 124 | """ 125 | cal = standard_test_text 126 | c = vobject.base.readOne(cal, validate=True) 127 | assert str(c.vevent.valarm.trigger) == "" 128 | assert str(c.vevent.dtstart.value) == "2002-10-28 14:00:00-08:00" 129 | assert isinstance(c.vevent.dtstart.value, datetime.datetime) 130 | assert str(c.vevent.dtend.value) == "2002-10-28 15:00:00-08:00" 131 | assert isinstance(c.vevent.dtend.value, datetime.datetime) 132 | assert c.vevent.dtstamp.value == datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=datetime.timezone.utc) 133 | 134 | vevent = c.vevent.transformFromNative() 135 | assert str(vevent.rrule) == "" 136 | 137 | 138 | def test_bad_stream(): 139 | """ 140 | Test bad ics stream 141 | """ 142 | with pytest.raises(vobject.base.ParseError): 143 | vobject.base.readOne(bad_stream) 144 | 145 | 146 | def test_bad_line(): 147 | """ 148 | Test bad line in ics file 149 | """ 150 | with pytest.raises(vobject.base.ParseError): 151 | vobject.base.readOne(bad_line) 152 | 153 | cal = vobject.base.readOne(bad_line, ignoreUnreadable=True) 154 | assert str(cal.vevent.x_bad_underscore) == "" 155 | 156 | 157 | def test_parse_params(): 158 | """ 159 | Test parsing parameters 160 | """ 161 | assert vobject.base.parseParams(';ALTREP="http://www.wiz.org"') == [["ALTREP", "http://www.wiz.org"]] 162 | assert vobject.base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR') == [ 163 | ["ALTREP", "http://www.wiz.org;;", "Blah", "Foo"], 164 | ["NEXT", "Nope"], 165 | ["BAR"], 166 | ] 167 | 168 | 169 | def test_quoted_printable(): 170 | """ 171 | The use of QUOTED-PRINTABLE encoding 172 | """ 173 | vobjs = vobject.base.readComponents(quoted_printable, allowQP=True) 174 | for vo in vobjs: 175 | assert vo is not None 176 | -------------------------------------------------------------------------------- /tests/test_vtodo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import io 3 | 4 | import vobject 5 | 6 | text = ( 7 | "BEGIN:VCALENDAR\r\n" 8 | "VERSION:2.0\r\n" 9 | "PRODID:-//Example Corp.//CalDAV Client//EN\r\n" 10 | "BEGIN:VTODO\r\n" 11 | "UID:20070313T123432Z-456553@example.com\r\n" 12 | "DTSTAMP:20070313T123432Z\r\n" 13 | "DUE;VALUE=DATE:20070501\r\n" 14 | "SUMMARY:Submit Quebec Income Tax Return for 2006\r\n" 15 | "CLASS:CONFIDENTIAL\r\n" 16 | "CATEGORIES:FAMILY,FINANCE\r\n" 17 | "STATUS:NEEDS-ACTION\r\n" 18 | "END:VTODO\r\n" 19 | "END:VCALENDAR\r\n" 20 | ) 21 | 22 | 23 | def test_vtodo(): 24 | """ 25 | Test VTodo 26 | """ 27 | obj = vobject.readOne(io.StringIO(text)) 28 | obj.vtodo.add("completed") 29 | obj.vtodo.completed.value = datetime.datetime(2015, 5, 5, 13, 30) 30 | assert obj.vtodo.completed.serialize()[0:23] == "COMPLETED:20150505T1330" 31 | 32 | obj = vobject.readOne(obj.serialize()) 33 | assert obj.vtodo.completed.value == datetime.datetime(2015, 5, 5, 13, 30) 34 | -------------------------------------------------------------------------------- /vobject/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | VObject Overview 3 | ================ 4 | vobject parses vCard or vCalendar files, returning a tree of Python objects. 5 | It also provids an API to create vCard or vCalendar data structures which 6 | can then be serialized. 7 | 8 | Parsing existing streams 9 | ------------------------ 10 | Streams containing one or many L{Component}s can be 11 | parsed using L{readComponents}. As each Component 12 | is parsed, vobject will attempt to give it a L{Behavior}. 13 | If an appropriate Behavior is found, any base64, quoted-printable, or 14 | backslash escaped data will automatically be decoded. Dates and datetimes 15 | will be transformed to datetime.date or datetime.datetime instances. 16 | Components containing recurrence information will have a special rruleset 17 | attribute (a dateutil.rrule.rruleset instance). 18 | 19 | Validation 20 | ---------- 21 | L{Behavior} classes implement validation for 22 | L{Component}s. To validate, an object must have all 23 | required children. There (TODO: will be) a toggle to raise an exception or 24 | just log unrecognized, non-experimental children and parameters. 25 | 26 | Creating objects programatically 27 | -------------------------------- 28 | A L{Component} can be created from scratch. No encoding 29 | is necessary, serialization will encode data automatically. Factory 30 | functions (TODO: will be) available to create standard objects. 31 | 32 | Serializing objects 33 | ------------------- 34 | Serialization: 35 | - Looks for missing required children that can be automatically generated, 36 | like a UID or a PRODID, and adds them 37 | - Encodes all values that can be automatically encoded 38 | - Checks to make sure the object is valid (unless this behavior is 39 | explicitly disabled) 40 | - Appends the serialized object to a buffer, or fills a new 41 | buffer and returns it 42 | 43 | Examples 44 | -------- 45 | 46 | >>> import datetime 47 | >>> import dateutil.rrule as rrule 48 | >>> x = iCalendar() 49 | >>> x.add('vevent') 50 | 51 | >>> x 52 | ]> 53 | >>> v = x.vevent 54 | >>> utc = icalendar.utc 55 | >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc) 56 | >>> v 57 | ]> 58 | >>> x 59 | ]>]> 60 | >>> newrule = rrule.rruleset() 61 | >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value)) 62 | >>> v.rruleset = newrule 63 | >>> list(v.rruleset) 64 | [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())] 65 | >>> v.add('uid').value = "randomuid@MYHOSTNAME" 66 | >>> print x.serialize() 67 | BEGIN:VCALENDAR 68 | VERSION:2.0 69 | PRODID:-//PYVOBJECT//NONSGML Version 1//EN 70 | BEGIN:VEVENT 71 | UID:randomuid@MYHOSTNAME 72 | DTSTART:20041215T140000Z 73 | RRULE:FREQ=WEEKLY;COUNT=2 74 | END:VEVENT 75 | END:VCALENDAR 76 | 77 | """ 78 | 79 | from . import icalendar, vcard 80 | from .base import VERSION, newFromBehavior, readComponents, readOne 81 | 82 | # Package version 83 | __version__ = VERSION 84 | 85 | 86 | def iCalendar(): 87 | return newFromBehavior("vcalendar", "2.0") 88 | 89 | 90 | def vCard(): 91 | return newFromBehavior("vcard", "3.0") 92 | -------------------------------------------------------------------------------- /vobject/behavior.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | 3 | 4 | # ------------------------ Abstract class for behavior -------------------------- 5 | class Behavior: 6 | """ 7 | Behavior (validation, encoding, and transformations) for vobjects. 8 | 9 | Abstract class to describe vobject options, requirements and encodings. 10 | 11 | Behaviors are used for root components like VCALENDAR, for subcomponents 12 | like VEVENT, and for individual lines in components. 13 | 14 | Behavior subclasses are not meant to be instantiated, all methods should 15 | be classmethods. 16 | 17 | @cvar name: 18 | The uppercase name of the object described by the class, or a generic 19 | name if the class defines behavior for many objects. 20 | @cvar description: 21 | A brief excerpt from the RFC explaining the function of the component or 22 | line. 23 | @cvar versionString: 24 | The string associated with the component, for instance, 2.0 if there's a 25 | line like VERSION:2.0, an empty string otherwise. 26 | @cvar knownChildren: 27 | A dictionary with uppercased component/property names as keys and a 28 | tuple (min, max, id) as value, where id is the id used by 29 | L{registerBehavior}, min and max are the limits on how many of this child 30 | must occur. None is used to denote no max or no id. 31 | @cvar quotedPrintable: 32 | A boolean describing whether the object should be encoded and decoded 33 | using quoted printable line folding and character escaping. 34 | @cvar defaultBehavior: 35 | Behavior to apply to ContentLine children when no behavior is found. 36 | @cvar hasNative: 37 | A boolean describing whether the object can be transformed into a more 38 | Pythonic object. 39 | @cvar isComponent: 40 | A boolean, True if the object should be a Component. 41 | @cvar sortFirst: 42 | The lower-case list of children which should come first when sorting. 43 | @cvar allowGroup: 44 | Whether or not vCard style group prefixes are allowed. 45 | """ 46 | 47 | name = "" 48 | description = "" 49 | versionString = "" 50 | knownChildren = {} 51 | quotedPrintable = False 52 | defaultBehavior = None 53 | hasNative = False 54 | isComponent = False 55 | allowGroup = False 56 | forceUTC = False 57 | sortFirst = [] 58 | 59 | def __init__(self): 60 | err = "Behavior subclasses are not meant to be instantiated" 61 | raise base.VObjectError(err) 62 | 63 | @classmethod 64 | def validate(cls, obj, raiseException=False, complainUnrecognized=False): 65 | """Check if the object satisfies this behavior's requirements. 66 | 67 | @param obj: 68 | The L{ContentLine} or 69 | L{Component} to be validated. 70 | @param raiseException: 71 | If True, raise a L{base.ValidateError} on validation failure. 72 | Otherwise return a boolean. 73 | @param complainUnrecognized: 74 | If True, fail to validate if an uncrecognized parameter or child is 75 | found. Otherwise log the lack of recognition. 76 | 77 | """ 78 | if not cls.allowGroup and obj.group is not None: 79 | raise base.VObjectError(f"{obj} has a group, but this object doesn't support groups") 80 | if isinstance(obj, base.ContentLine): 81 | return cls.lineValidate(obj, raiseException, complainUnrecognized) 82 | elif isinstance(obj, base.Component): 83 | count = {} 84 | for child in obj.getChildren(): 85 | if not child.validate(raiseException, complainUnrecognized): 86 | return False 87 | name = child.name.upper() 88 | count[name] = count.get(name, 0) + 1 89 | for key, val in cls.knownChildren.items(): 90 | if count.get(key, 0) < val[0]: 91 | if raiseException: 92 | raise base.ValidateError(f"{cls.name} components must contain at least {val[0]} {key}") 93 | return False 94 | if val[1] and count.get(key, 0) > val[1]: 95 | if raiseException: 96 | raise base.ValidateError(f"{cls.name} components cannot contain more than {val[1]} {key}") 97 | return False 98 | return True 99 | else: 100 | raise base.VObjectError(f"{obj} is not a Component or Contentline") 101 | 102 | @classmethod 103 | def lineValidate(cls, line, raiseException, complainUnrecognized): 104 | """Examine a line's parameters and values, return True if valid.""" 105 | return True 106 | 107 | @classmethod 108 | def decode(cls, line): 109 | if line.encoded: 110 | line.encoded = 0 111 | 112 | @classmethod 113 | def encode(cls, line): 114 | if not line.encoded: 115 | line.encoded = 1 116 | 117 | @classmethod 118 | def transformToNative(cls, obj): 119 | """ 120 | Turn a ContentLine or Component into a Python-native representation. 121 | 122 | If appropriate, turn dates or datetime strings into Python objects. 123 | Components containing VTIMEZONEs turn into VtimezoneComponents. 124 | 125 | """ 126 | return obj 127 | 128 | @classmethod 129 | def transformFromNative(cls, obj): 130 | """ 131 | Inverse of transformToNative. 132 | """ 133 | raise base.NativeError("No transformFromNative defined") 134 | 135 | @classmethod 136 | def generateImplicitParameters(cls, obj): 137 | """Generate any required information that don't yet exist.""" 138 | 139 | @classmethod 140 | def serialize(cls, obj, buf, lineLength, validate=True, *args, **kwargs): 141 | """ 142 | Set implicit parameters, do encoding, return unicode string. 143 | 144 | If validate is True, raise VObjectError if the line doesn't validate 145 | after implicit parameters are generated. 146 | 147 | Default is to call base.defaultSerialize. 148 | 149 | """ 150 | 151 | cls.generateImplicitParameters(obj) 152 | if validate: 153 | cls.validate(obj, raiseException=True) 154 | 155 | if obj.isNative: 156 | transformed = obj.transformFromNative() 157 | undoTransform = True 158 | else: 159 | transformed = obj 160 | undoTransform = False 161 | 162 | out = base.defaultSerialize(transformed, buf, lineLength) 163 | if undoTransform: 164 | obj.transformToNative() 165 | return out 166 | 167 | @classmethod 168 | def valueRepr(cls, line): 169 | """return the representation of the given content line value""" 170 | return line.value 171 | -------------------------------------------------------------------------------- /vobject/change_tz.py: -------------------------------------------------------------------------------- 1 | """Translate an ics file's events to a different timezone.""" 2 | 3 | from argparse import ArgumentParser 4 | from datetime import datetime 5 | 6 | import pytz 7 | from dateutil import tz 8 | 9 | import vobject 10 | 11 | version = vobject.VERSION 12 | 13 | 14 | def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=vobject.icalendar.utc): 15 | """ 16 | Change the timezone of the specified component. 17 | 18 | Args: 19 | cal (Component): the component to change 20 | new_timezone (tzinfo): the timezone to change to 21 | default (tzinfo): a timezone to assume if the dtstart or dtend in cal doesn't have an existing timezone 22 | utc_only (bool): only convert dates that are in utc 23 | utc_tz (tzinfo): the tzinfo to compare to for UTC when processing utc_only=True 24 | """ 25 | 26 | for vevent in getattr(cal, "vevent_list", []): 27 | start = getattr(vevent, "dtstart", None) 28 | end = getattr(vevent, "dtend", None) 29 | for node in (start, end): 30 | if node: 31 | dt = node.value 32 | if isinstance(dt, datetime) and (not utc_only or dt.tzinfo == utc_tz): 33 | if dt.tzinfo is None: 34 | dt = dt.replace(tzinfo=default) 35 | node.value = dt.astimezone(new_timezone) 36 | 37 | 38 | def show_timezones(): 39 | for tz_string in pytz.all_timezones: 40 | print(tz_string) 41 | 42 | 43 | def convert_events(utc_only, ics_file, timezone_="UTC"): 44 | print(f'Converting {"only UTC" if utc_only else "all"} events') 45 | 46 | print(f"... Reading {ics_file}") 47 | with open(ics_file, "r") as f: 48 | cal = vobject.readOne(f) 49 | change_tz(cal, new_timezone=tz.gettz(timezone_), default=tz.gettz("UTC"), utc_only=utc_only) 50 | 51 | out_name = f"{ics_file}.converted" 52 | print(f"... Writing {out_name}") 53 | with open(out_name, "wb") as out: 54 | cal.serialize(out) 55 | 56 | print("Done") 57 | 58 | 59 | def main(): 60 | args = get_arguments() 61 | if args.list: 62 | show_timezones() 63 | elif args.ics_file: 64 | convert_events(utc_only=args.utc, ics_file=args.ics_file, timezone_=args.timezone) 65 | 66 | 67 | def get_arguments(): 68 | # Configuration options 69 | parser = ArgumentParser(description="change_tz will convert the timezones in an ics file.") 70 | parser.add_argument("-V", "--version", action="version", version=vobject.VERSION) 71 | 72 | parser.add_argument( 73 | "-u", "--only-utc", dest="utc", action="store_true", default=False, help="Only change UTC events." 74 | ) 75 | group = parser.add_mutually_exclusive_group(required=True) 76 | group.add_argument("-l", "--list", dest="list", action="store_true", default=False, help="List available timezones") 77 | group.add_argument("ics_file", nargs="?", help="The ics file to process") 78 | parser.add_argument("timezone", nargs="?", default="UTC", help="The timezone to convert to") 79 | 80 | return parser.parse_args() 81 | 82 | 83 | if __name__ == "__main__": 84 | try: 85 | main() 86 | except KeyboardInterrupt: 87 | print("Aborted") 88 | -------------------------------------------------------------------------------- /vobject/hcalendar.py: -------------------------------------------------------------------------------- 1 | r""" 2 | hCalendar: A microformat for serializing iCalendar data 3 | (http://microformats.org/wiki/hcalendar) 4 | 5 | Here is a sample event in an iCalendar: 6 | 7 | BEGIN:VCALENDAR 8 | PRODID:-//XYZproduct//EN 9 | VERSION:2.0 10 | BEGIN:VEVENT 11 | URL:http://www.web2con.com/ 12 | DTSTART:20051005 13 | DTEND:20051008 14 | SUMMARY:Web 2.0 Conference 15 | LOCATION:Argent Hotel\, San Francisco\, CA 16 | END:VEVENT 17 | END:VCALENDAR 18 | 19 | and an equivalent event in hCalendar format with various elements optimized appropriately. 20 | 21 | 22 | 23 | Web 2.0 Conference: 24 | October 5- 25 | 7, 26 | at the Argent Hotel, San Francisco, CA 27 | 28 | 29 | """ 30 | 31 | import io 32 | from datetime import date, datetime, timedelta 33 | 34 | from .base import CRLF, registerBehavior 35 | from .icalendar import VCalendar2_0 36 | 37 | 38 | class HCalendar(VCalendar2_0): 39 | name = "HCALENDAR" 40 | 41 | @classmethod 42 | def serialize(cls, obj, buf=None, lineLength=None, validate=True, *args, **kwargs): 43 | """ 44 | Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar) 45 | """ 46 | 47 | outbuf = buf or io.StringIO() 48 | level = 0 # holds current indentation level 49 | tabwidth = 3 50 | 51 | def indent(): 52 | return " " * level * tabwidth 53 | 54 | def out(s): 55 | outbuf.write(indent()) 56 | outbuf.write(s) 57 | 58 | # not serializing optional vcalendar wrapper 59 | 60 | vevents = obj.vevent_list 61 | 62 | for event in vevents: 63 | out('' + CRLF) 64 | level += 1 65 | 66 | # URL 67 | url = event.getChildValue("url") 68 | if url: 69 | out('' + CRLF) 70 | level += 1 71 | # SUMMARY 72 | summary = event.getChildValue("summary") 73 | if summary: 74 | out('' + summary + ":" + CRLF) 75 | 76 | # DTSTART 77 | dtstart = event.getChildValue("dtstart") 78 | if dtstart: 79 | machine = timeformat = "" 80 | if type(dtstart) is date: 81 | timeformat = "%A, %B %e" 82 | machine = "%Y%m%d" 83 | elif type(dtstart) is datetime: 84 | timeformat = "%A, %B %e, %H:%M" 85 | machine = "%Y%m%dT%H%M%S%z" 86 | 87 | # TODO: Handle non-datetime formats? 88 | # TODO: Spec says we should handle when dtstart isn't included 89 | 90 | out( 91 | f'{dtstart.strftime(timeformat)}\r\n' 92 | ) 93 | 94 | # DTEND 95 | dtend = event.getChildValue("dtend") 96 | if not dtend: 97 | duration = event.getChildValue("duration") 98 | if duration: 99 | dtend = duration + dtstart 100 | # TODO: If lacking dtend & duration? 101 | 102 | if dtend: 103 | human = dtend 104 | # TODO: Human readable part could be smarter, excluding repeated data 105 | if type(dtend) is date: 106 | human = dtend - timedelta(days=1) 107 | 108 | out( 109 | f'- {human.strftime(timeformat)}\r\n' 110 | ) 111 | 112 | # LOCATION 113 | location = event.getChildValue("location") 114 | if location: 115 | out('at ' + location + "" + CRLF) 116 | 117 | description = event.getChildValue("description") 118 | if description: 119 | out('
' + description + "
" + CRLF) 120 | 121 | if url: 122 | level -= 1 123 | out("
" + CRLF) 124 | 125 | level -= 1 126 | out("
" + CRLF) # close vevent 127 | 128 | return buf or outbuf.getvalue() 129 | 130 | 131 | registerBehavior(HCalendar) 132 | -------------------------------------------------------------------------------- /vobject/ics_diff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compare VTODOs and VEVENTs in two iCalendar sources. 3 | """ 4 | 5 | from argparse import ArgumentParser 6 | 7 | import vobject 8 | 9 | 10 | def getSortKey(component): 11 | def getUID(component): 12 | return component.getChildValue("uid", "") 13 | 14 | # it's not quite as simple as getUID, need to account for recurrenceID and sequence 15 | 16 | def getSequence(component) -> str: 17 | sequence = component.getChildValue("sequence", 0) 18 | return f"{int(sequence):05d}" 19 | 20 | def getRecurrenceID(component): 21 | recurrence_id = component.getChildValue("recurrence_id", None) 22 | if recurrence_id is None: 23 | return "0000-00-00" 24 | else: 25 | return recurrence_id.isoformat() 26 | 27 | return getUID(component) + getSequence(component) + getRecurrenceID(component) 28 | 29 | 30 | def sortByUID(components): 31 | return sorted(components, key=getSortKey) 32 | 33 | 34 | def deleteExtraneous(component, ignore_dtstamp=False): 35 | """ 36 | Recursively walk the component's children, deleting extraneous details like 37 | X-VOBJ-ORIGINAL-TZID. 38 | """ 39 | for comp in component.components(): 40 | deleteExtraneous(comp, ignore_dtstamp) 41 | for line in component.lines(): 42 | if "X-VOBJ-ORIGINAL-TZID" in line.params: 43 | del line.params["X-VOBJ-ORIGINAL-TZID"] 44 | if ignore_dtstamp and hasattr(component, "dtstamp_list"): 45 | del component.dtstamp_list 46 | 47 | 48 | def diff(left, right): 49 | """ 50 | Take two VCALENDAR components, compare VEVENTs and VTODOs in them, 51 | return a list of object pairs containing just UID and the bits 52 | that didn't match, using None for objects that weren't present in one 53 | version or the other. 54 | 55 | When there are multiple ContentLines in one VEVENT, for instance many 56 | DESCRIPTION lines, such lines original order is assumed to be 57 | meaningful. Order is also preserved when comparing (the unlikely case 58 | of) multiple parameters of the same type in a ContentLine 59 | 60 | """ 61 | 62 | def processComponentLists(leftList, rightList): 63 | output = [] 64 | rightIndex = 0 65 | rightListSize = len(rightList) 66 | 67 | for comp in leftList: 68 | if rightIndex >= rightListSize: 69 | output.append((comp, None)) 70 | else: 71 | leftKey = getSortKey(comp) 72 | rightComp = rightList[rightIndex] 73 | rightKey = getSortKey(rightComp) 74 | while leftKey > rightKey: 75 | output.append((None, rightComp)) 76 | rightIndex += 1 77 | if rightIndex >= rightListSize: 78 | output.append((comp, None)) 79 | break 80 | rightComp = rightList[rightIndex] 81 | rightKey = getSortKey(rightComp) 82 | 83 | if leftKey < rightKey: 84 | output.append((comp, None)) 85 | elif leftKey == rightKey: 86 | rightIndex += 1 87 | matchResult = processComponentPair(comp, rightComp) 88 | if matchResult is not None: 89 | output.append(matchResult) 90 | 91 | return output 92 | 93 | def newComponent(name, body): # pylint:disable=unused-variable 94 | if body is None: 95 | return None 96 | c = vobject.base.Component(name) 97 | c.behavior = vobject.base.getBehavior(name) 98 | c.isNative = True 99 | return c 100 | 101 | def processComponentPair(leftComp, rightComp): 102 | """ 103 | Return None if a match, or a pair of components including UIDs and 104 | any differing children. 105 | 106 | """ 107 | leftChildKeys = leftComp.contents.keys() 108 | rightChildKeys = rightComp.contents.keys() 109 | 110 | differentContentLines = [] 111 | differentComponents = {} 112 | 113 | for key in leftChildKeys: 114 | rightList = rightComp.contents.get(key, []) 115 | if isinstance(leftComp.contents[key][0], vobject.base.Component): 116 | compDifference = processComponentLists(leftComp.contents[key], rightList) 117 | if len(compDifference) > 0: 118 | differentComponents[key] = compDifference 119 | 120 | elif leftComp.contents[key] != rightList: 121 | differentContentLines.append((leftComp.contents[key], rightList)) 122 | 123 | for key in rightChildKeys: 124 | if key not in leftChildKeys: 125 | if isinstance(rightComp.contents[key][0], vobject.base.Component): 126 | differentComponents[key] = ([], rightComp.contents[key]) 127 | else: 128 | differentContentLines.append(([], rightComp.contents[key])) 129 | 130 | if not differentContentLines and not differentComponents: 131 | return None 132 | 133 | left = vobject.newFromBehavior(leftComp.name) 134 | right = vobject.newFromBehavior(leftComp.name) 135 | # add a UID, if one existed, despite the fact that they'll always be 136 | # the same 137 | uid = leftComp.getChildValue("uid") 138 | if uid is not None: 139 | left.add("uid").value = uid 140 | right.add("uid").value = uid 141 | 142 | for name, childPairList in differentComponents.items(): 143 | leftComponents, rightComponents = zip(*childPairList) 144 | if len(leftComponents) > 0: 145 | # filter out None 146 | left.contents[name] = filter(None, leftComponents) 147 | if len(rightComponents) > 0: 148 | # filter out None 149 | right.contents[name] = filter(None, rightComponents) 150 | 151 | for leftChildLine, rightChildLine in differentContentLines: 152 | nonEmpty = leftChildLine or rightChildLine 153 | name = nonEmpty[0].name 154 | if leftChildLine is not None: 155 | left.contents[name] = leftChildLine 156 | if rightChildLine is not None: 157 | right.contents[name] = rightChildLine 158 | 159 | return left, right 160 | 161 | vevents = processComponentLists( 162 | sortByUID(getattr(left, "vevent_list", [])), sortByUID(getattr(right, "vevent_list", [])) 163 | ) 164 | 165 | vtodos = processComponentLists( 166 | sortByUID(getattr(left, "vtodo_list", [])), sortByUID(getattr(right, "vtodo_list", [])) 167 | ) 168 | 169 | return vevents + vtodos 170 | 171 | 172 | def prettyDiff(leftObj, rightObj): 173 | for left, right in diff(leftObj, rightObj): 174 | print("<<<<<<<<<<<<<<<") 175 | if left is not None: 176 | left.prettyPrint() 177 | print("===============") 178 | if right is not None: 179 | right.prettyPrint() 180 | print(">>>>>>>>>>>>>>>") 181 | 182 | 183 | def main(): 184 | args = get_arguments() 185 | with open(args.ics_file1) as f, open(args.ics_file2) as g: 186 | cal1 = vobject.readOne(f) 187 | cal2 = vobject.readOne(g) 188 | deleteExtraneous(cal1, ignore_dtstamp=args.ignore) 189 | deleteExtraneous(cal2, ignore_dtstamp=args.ignore) 190 | prettyDiff(cal1, cal2) 191 | 192 | 193 | def get_arguments(): 194 | # Configuration options # 195 | parser = ArgumentParser(description="ics_diff will print a comparison of two iCalendar files") 196 | parser.add_argument("-V", "--version", action="version", version=vobject.VERSION) 197 | parser.add_argument( 198 | "-i", 199 | "--ignore-dtstamp", 200 | dest="ignore", 201 | action="store_true", 202 | default=False, 203 | help="ignore DTSTAMP lines [default: False]", 204 | ) 205 | parser.add_argument("ics_file1", help="The first ics file to compare") 206 | parser.add_argument("ics_file2", help="The second ics file to compare") 207 | 208 | return parser.parse_args() 209 | 210 | 211 | if __name__ == "__main__": 212 | try: 213 | main() 214 | except KeyboardInterrupt: 215 | print("Aborted") 216 | -------------------------------------------------------------------------------- /vobject/vcard.py: -------------------------------------------------------------------------------- 1 | """Definitions and behavior for vCard 3.0""" 2 | 3 | import codecs 4 | 5 | from . import behavior 6 | from .base import ContentLine, backslashEscape, basestring, registerBehavior, str_ 7 | from .icalendar import stringToTextValues 8 | 9 | # ------------------------ vCard structs --------------------------------------- 10 | 11 | 12 | class Name: 13 | def __init__(self, family="", given="", additional="", prefix="", suffix=""): 14 | """ 15 | Each name attribute can be a string or a list of strings. 16 | """ 17 | self.family = family 18 | self.given = given 19 | self.additional = additional 20 | self.prefix = prefix 21 | self.suffix = suffix 22 | 23 | @staticmethod 24 | def toString(val): 25 | """ 26 | Turn a string or array value into a string. 27 | """ 28 | if type(val) in (list, tuple): 29 | return " ".join(val) 30 | return val 31 | 32 | def __str__(self): 33 | eng_order = ("prefix", "given", "additional", "family", "suffix") 34 | out = " ".join(self.toString(getattr(self, val)) for val in eng_order) 35 | return str_(out) 36 | 37 | def __repr__(self): 38 | return f"" 39 | 40 | def __eq__(self, other): 41 | try: 42 | return ( 43 | self.family == other.family 44 | and self.given == other.given 45 | and self.additional == other.additional 46 | and self.prefix == other.prefix 47 | and self.suffix == other.suffix 48 | ) 49 | except AttributeError: 50 | return False 51 | 52 | 53 | class Address: 54 | def __init__(self, street="", city="", region="", code="", country="", box="", extended=""): 55 | """ 56 | Each name attribute can be a string or a list of strings. 57 | """ 58 | self.box = box 59 | self.extended = extended 60 | self.street = street 61 | self.city = city 62 | self.region = region 63 | self.code = code 64 | self.country = country 65 | 66 | @staticmethod 67 | def toString(val, join_char="\n"): 68 | """ 69 | Turn a string or array value into a string. 70 | """ 71 | if type(val) in (list, tuple): 72 | return join_char.join(val) 73 | return val 74 | 75 | lines = ("box", "extended", "street") 76 | one_line = ("city", "region", "code") 77 | 78 | def __str__(self): 79 | lines = "\n".join(self.toString(getattr(self, val)) for val in self.lines if getattr(self, val)) 80 | one_line = tuple(self.toString(getattr(self, val), " ") for val in self.one_line) 81 | lines += f"\n{one_line[0]}, {one_line[1]} {one_line[2]}" 82 | if self.country: 83 | lines += "\n" + self.toString(self.country) 84 | return lines 85 | 86 | def __repr__(self): 87 | return "" 88 | 89 | def __eq__(self, other): 90 | try: 91 | return ( 92 | self.box == other.box 93 | and self.extended == other.extended 94 | and self.street == other.street 95 | and self.city == other.city 96 | and self.region == other.region 97 | and self.code == other.code 98 | and self.country == other.country 99 | ) 100 | except AttributeError: 101 | return False 102 | 103 | 104 | # ------------------------ Registered Behavior subclasses ---------------------- 105 | 106 | 107 | class VCardTextBehavior(behavior.Behavior): 108 | """ 109 | Provide backslash escape encoding/decoding for single valued properties. 110 | 111 | TextBehavior also deals with base64 encoding if the ENCODING parameter is 112 | explicitly set to BASE64. 113 | """ 114 | 115 | allowGroup = True 116 | base64string = "B" 117 | 118 | @classmethod 119 | def decode(cls, line): 120 | """ 121 | Remove backslash escaping from line.valueDecode line, either to remove 122 | backslash espacing, or to decode base64 encoding. The content line should 123 | contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to 124 | export a singleton parameter of 'BASE64', which does not match the 3.0 125 | vCard spec. If we encouter that, then we transform the parameter to 126 | ENCODING=b 127 | """ 128 | if line.encoded: 129 | if "BASE64" in line.singletonparams: 130 | line.singletonparams.remove("BASE64") 131 | line.encoding_param = cls.base64string 132 | encoding = getattr(line, "encoding_param", None) 133 | if encoding: 134 | if isinstance(line.value, bytes): 135 | line.value = codecs.decode(line.value, "base64") 136 | else: 137 | line.value = codecs.decode(line.value.encode("utf-8"), "base64") 138 | else: 139 | line.value = stringToTextValues(line.value)[0] 140 | line.encoded = False 141 | 142 | @classmethod 143 | def encode(cls, line): 144 | """ 145 | Backslash escape line.value. 146 | """ 147 | if not line.encoded: 148 | encoding = getattr(line, "encoding_param", None) 149 | if encoding and encoding.upper() == cls.base64string: 150 | if isinstance(line.value, bytes): 151 | line.value = codecs.encode(line.value, "base64").decode("utf-8").replace("\n", "") 152 | else: 153 | line.value = codecs.encode(line.value.encode(encoding), "base64").decode("utf-8") 154 | else: 155 | line.value = backslashEscape(line.value) 156 | line.encoded = True 157 | 158 | 159 | class VCardBehavior(behavior.Behavior): 160 | allowGroup = True 161 | defaultBehavior = VCardTextBehavior 162 | 163 | 164 | class VCard3_0(VCardBehavior): 165 | """ 166 | vCard 3.0 behavior. 167 | """ 168 | 169 | name = "VCARD" 170 | description = "vCard 3.0, defined in rfc2426" 171 | versionString = "3.0" 172 | isComponent = True 173 | sortFirst = ("version", "prodid", "uid") 174 | knownChildren = { 175 | "N": (0, 1, None), # min, max, behaviorRegistry id 176 | "FN": (1, None, None), 177 | "VERSION": (1, 1, None), # required, auto-generated 178 | "PRODID": (0, 1, None), 179 | "LABEL": (0, None, None), 180 | "UID": (0, None, None), 181 | "ADR": (0, None, None), 182 | "ORG": (0, None, None), 183 | "PHOTO": (0, None, None), 184 | "CATEGORIES": (0, None, None), 185 | "GEO": (0, None, None), 186 | } 187 | 188 | @classmethod 189 | def generateImplicitParameters(cls, obj): 190 | """ 191 | Create PRODID, VERSION, and VTIMEZONEs if needed. 192 | 193 | VTIMEZONEs will need to exist whenever TZID parameters exist or when 194 | datetimes with tzinfo exist. 195 | """ 196 | if not hasattr(obj, "version"): 197 | obj.add(ContentLine("VERSION", [], cls.versionString)) 198 | 199 | 200 | registerBehavior(VCard3_0, default=True) 201 | 202 | 203 | class FN(VCardTextBehavior): 204 | name = "FN" 205 | description = "Formatted name" 206 | 207 | 208 | registerBehavior(FN) 209 | 210 | 211 | class Label(VCardTextBehavior): 212 | name = "Label" 213 | description = "Formatted address" 214 | 215 | 216 | registerBehavior(Label) 217 | 218 | 219 | class GEO(VCardBehavior): 220 | name = "GEO" 221 | description = "Geographical location" 222 | 223 | 224 | registerBehavior(GEO) 225 | 226 | 227 | wacky_apple_photo_serialize = True 228 | REALLY_LARGE = 1e50 229 | 230 | 231 | class Photo(VCardTextBehavior): 232 | name = "Photo" 233 | description = "Photograph" 234 | 235 | @classmethod 236 | def valueRepr(cls, line): 237 | return f" (BINARY PHOTO DATA at 0x{id(line.value)}) " 238 | 239 | @classmethod 240 | def serialize(cls, obj, buf, lineLength, validate, *args, **kwargs): 241 | """ 242 | Apple's Address Book is *really* weird with images, it expects 243 | base64 data to have very specific whitespace. It seems Address Book 244 | can handle PHOTO if it's not wrapped, so don't wrap it. 245 | """ 246 | if wacky_apple_photo_serialize: 247 | lineLength = REALLY_LARGE 248 | VCardTextBehavior.serialize(obj, buf, lineLength, validate, *args, **kwargs) 249 | 250 | 251 | registerBehavior(Photo) 252 | 253 | 254 | def toListOrString(string): 255 | stringList = stringToTextValues(string) 256 | if len(stringList) == 1: 257 | return stringList[0] 258 | else: 259 | return stringList 260 | 261 | 262 | def splitFields(string): 263 | """ 264 | Return a list of strings or lists from a Name or Address. 265 | """ 266 | return [toListOrString(i) for i in stringToTextValues(string, listSeparator=";", charList=";")] 267 | 268 | 269 | def toList(stringOrList): 270 | if isinstance(stringOrList, basestring): 271 | return [stringOrList] 272 | return stringOrList 273 | 274 | 275 | def serializeFields(obj, order=None): 276 | """ 277 | Turn an object's fields into a ';' and ',' seperated string. 278 | 279 | If order is None, obj should be a list, backslash escape each field and 280 | return a ';' separated string. 281 | """ 282 | fields = [] 283 | if order is None: 284 | fields = [backslashEscape(val) for val in obj] 285 | else: 286 | for field in order: 287 | escapedValueList = [backslashEscape(val) for val in toList(getattr(obj, field))] 288 | fields.append(",".join(escapedValueList)) 289 | return ";".join(fields) 290 | 291 | 292 | NAME_ORDER = ("family", "given", "additional", "prefix", "suffix") 293 | ADDRESS_ORDER = ("box", "extended", "street", "city", "region", "code", "country") 294 | 295 | 296 | class NameBehavior(VCardBehavior): 297 | """ 298 | A structured name. 299 | """ 300 | 301 | hasNative = True 302 | 303 | @staticmethod 304 | def transformToNative(obj): 305 | """ 306 | Turn obj.value into a Name. 307 | """ 308 | if obj.isNative: 309 | return obj 310 | obj.isNative = True 311 | obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value)))) 312 | return obj 313 | 314 | @staticmethod 315 | def transformFromNative(obj): 316 | """ 317 | Replace the Name in obj.value with a string. 318 | """ 319 | obj.isNative = False 320 | obj.value = serializeFields(obj.value, NAME_ORDER) 321 | return obj 322 | 323 | 324 | registerBehavior(NameBehavior, "N") 325 | 326 | 327 | class AddressBehavior(VCardBehavior): 328 | """ 329 | A structured address. 330 | """ 331 | 332 | hasNative = True 333 | 334 | @staticmethod 335 | def transformToNative(obj): 336 | """ 337 | Turn obj.value into an Address. 338 | """ 339 | if obj.isNative: 340 | return obj 341 | obj.isNative = True 342 | obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value)))) 343 | return obj 344 | 345 | @staticmethod 346 | def transformFromNative(obj): 347 | """ 348 | Replace the Address in obj.value with a string. 349 | """ 350 | obj.isNative = False 351 | obj.value = serializeFields(obj.value, ADDRESS_ORDER) 352 | return obj 353 | 354 | 355 | registerBehavior(AddressBehavior, "ADR") 356 | 357 | 358 | class OrgBehavior(VCardBehavior): 359 | """ 360 | A list of organization values and sub-organization values. 361 | """ 362 | 363 | hasNative = True 364 | 365 | @staticmethod 366 | def transformToNative(obj): 367 | """ 368 | Turn obj.value into a list. 369 | """ 370 | if obj.isNative: 371 | return obj 372 | obj.isNative = True 373 | obj.value = splitFields(obj.value) 374 | return obj 375 | 376 | @staticmethod 377 | def transformFromNative(obj): 378 | """ 379 | Replace the list in obj.value with a string. 380 | """ 381 | if not obj.isNative: 382 | return obj 383 | obj.isNative = False 384 | obj.value = serializeFields(obj.value) 385 | return obj 386 | 387 | 388 | registerBehavior(OrgBehavior, "ORG") 389 | --------------------------------------------------------------------------------