kopia lustrzana https://github.com/collective/icalendar
Merge branch 'master' into error_handling_2
commit
0080a3942f
|
@ -0,0 +1,51 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '14 7 * * 0' # run once a week on Sunday
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
# [Python version, tox env]
|
||||
- ["3.8", "py38"]
|
||||
- ["3.9", "py39"]
|
||||
- ["3.10", "py310"]
|
||||
- ["pypy-3.9", "pypy3"]
|
||||
- ["3.10", "docs"]
|
||||
- ["3.11.0-rc.1", "py311"]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.config[1] }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.config[0] }}
|
||||
- name: Pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-${{ matrix.config[0] }}-
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- name: Test
|
||||
run: tox -e ${{ matrix.config[1] }}
|
||||
- name: Coverage
|
||||
run: |
|
||||
pip install coveralls coverage-python-version
|
||||
coveralls --service=github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -18,4 +18,5 @@ parts/
|
|||
pip-selfcheck.json
|
||||
src/icalendar.egg-info/
|
||||
!.gitattributes
|
||||
!.github
|
||||
!.gitignore
|
||||
|
|
55
CHANGES.rst
55
CHANGES.rst
|
@ -25,16 +25,58 @@ Bug fixes:
|
|||
- *add item here*
|
||||
|
||||
|
||||
4.0.9 (unreleased)
|
||||
------------------
|
||||
5.0.0a2 (unreleased)
|
||||
--------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- removed deprecated test checks [tuergeist]
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- *add item here*
|
||||
- Require Python 3.8 as minimum Python version. [maurits]
|
||||
- icalenar now takes a ics file directly as an input
|
||||
- icalendar's output is different
|
||||
|
||||
New features:
|
||||
|
||||
- *add item here*
|
||||
- icalendar utility outputs a 'Duration' row
|
||||
- icalendar can take multiple ics files as an input
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Changed tools.UIDGenerator instance methods to static methods
|
||||
Ref: #345
|
||||
[spralja]
|
||||
- proper handling of datetime objects with `tzinfo` generated through zoneinfo.ZoneInfo.
|
||||
Ref: #334
|
||||
Fixes: #333
|
||||
[tobixen]
|
||||
- Timestamps in UTC does not need tzid
|
||||
Ref: #338
|
||||
Fixes: #335
|
||||
[tobixen]
|
||||
|
||||
5.0.0a1 (2022-07-11)
|
||||
--------------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Drop support for Python 3.4, 3.5 and PyPy2. [maurits]
|
||||
|
||||
New features:
|
||||
|
||||
- Document development setup
|
||||
Ref: #358
|
||||
[niccokunzmann]
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Test with GitHub Actions. [maurits]
|
||||
|
||||
|
||||
4.0.9 (2021-10-16)
|
||||
------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
@ -80,7 +122,6 @@ Bug fixes:
|
|||
|
||||
- Fixed a docs issue related to building on Read the Docs [davidfischer]
|
||||
|
||||
|
||||
4.0.4 (2019-11-25)
|
||||
------------------
|
||||
|
||||
|
@ -666,9 +707,9 @@ Fixes:
|
|||
- created sphinx documentation and started documenting development and goals.
|
||||
[garbas]
|
||||
|
||||
- hook out github repository to http://readthedocs.org service so sphinx
|
||||
- hook out github repository to https://readthedocs.org service so sphinx
|
||||
documentation is generated on each commit (for master). Documentation can be
|
||||
visible on: http://readthedocs.org/docs/icalendar/en/latest/
|
||||
visible on: https://icalendar.readthedocs.io/en/latest/
|
||||
[garbas]
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ These are some contribution examples
|
|||
|
||||
- Extending the documentation.
|
||||
|
||||
- Sponsor a Sprint (http://plone.org/events/sprints/whatis).
|
||||
- Sponsor a Sprint (https://plone.org/events/sprints/whatis).
|
||||
|
||||
|
||||
For pull requests, keep this in mind
|
||||
|
@ -21,3 +21,11 @@ For pull requests, keep this in mind
|
|||
- Describe your change in CHANGES.rst
|
||||
|
||||
- Add yourself to the docs/credits.rst
|
||||
|
||||
Development Setup
|
||||
-----------------
|
||||
|
||||
If you would like to setup icalendar to
|
||||
contribute changes, the `Installation Section
|
||||
<https://icalendar.readthedocs.io/en/latest/install.html>`_
|
||||
should help you further.
|
||||
|
|
34
README.rst
34
README.rst
|
@ -16,10 +16,21 @@ files.
|
|||
|
||||
----
|
||||
|
||||
.. image:: https://badge.fury.io/py/icalendar.svg
|
||||
:target: https://pypi.org/project/icalendar/
|
||||
:alt: Python Package Version on PyPI
|
||||
|
||||
.. image:: https://travis-ci.org/collective/icalendar.svg?branch=master
|
||||
:target: https://travis-ci.org/collective/icalendar
|
||||
.. image:: https://img.shields.io/pypi/dm/icalendar.svg
|
||||
:target: https://pypi.org/project/icalendar/#files
|
||||
:alt: Downloads from PyPI
|
||||
|
||||
.. image:: https://img.shields.io/github/workflow/status/collective/icalendar/tests/master?label=master&logo=github
|
||||
:target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3Amaster
|
||||
:alt: GitHub Actions build status for master
|
||||
|
||||
.. image:: https://img.shields.io/github/workflow/status/collective/icalendar/tests/4.x?label=4.x&logo=github
|
||||
:target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3A4.x++
|
||||
:alt: GitHub Actions build status for 4.x
|
||||
|
||||
.. _`icalendar`: https://pypi.org/project/icalendar/
|
||||
.. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt
|
||||
|
@ -27,9 +38,26 @@ files.
|
|||
.. _`pytz`: https://pypi.org/project/pytz/
|
||||
.. _`BSD`: https://github.com/collective/icalendar/issues/2
|
||||
|
||||
Versions and Compatibility
|
||||
--------------------------
|
||||
|
||||
``icalendar`` is a critical project used by many. It has been there for a long time and maintaining
|
||||
long-term compatibility with projects conflicts partially with providing and using the features that
|
||||
the latest Python versions bring.
|
||||
|
||||
Since we pour more `effort into maintaining and developing icalendar <https://github.com/collective/icalendar/discussions/360>`__,
|
||||
we split the project into two:
|
||||
|
||||
- `Branch 4.x <https://github.com/collective/icalendar/tree/4.x>`__ with maximum compatibility to Python versions ``2.7`` and ``3.4+``, ``PyPy2`` and ``PyPy3``.
|
||||
- `Branch master <https://github.com/collective/icalendar/>`__ with the compatibility to Python versions ``3.7+`` and ``PyPy3``.
|
||||
|
||||
We expect the ``master`` branch with versions ``5+`` receive the latest updates and features,
|
||||
and the ``4.x`` branch the subset of security and bug fixes only.
|
||||
We recomment migrating to later Python versions and also providing feedback if you depend on the ``4.x`` features.
|
||||
|
||||
Related projects
|
||||
================
|
||||
|
||||
* `icalevents <https://github.com/irgangla/icalevents>`_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well.
|
||||
|
||||
* `recurring-ical-events <https://pypi.org/project/recurring-ical-events/>`_. Library to query an ``ICalendar`` object for events happening at a certain date or within a certain time.
|
||||
* `x-wr-timezone <https://pypi.org/project/x-wr-timezone/>`_. Library to make ``ICalendar`` objects and files using the non-standard ``X-WR-TIMEZONE`` compliant with the standard (RFC 5545).
|
||||
|
|
|
@ -111,7 +111,7 @@ cmd = [sys.executable, '-c',
|
|||
find_links = os.environ.get(
|
||||
'bootstrap-testing-find-links',
|
||||
options.find_links or
|
||||
('http://downloads.buildout.org/'
|
||||
('https://downloads.buildout.org/'
|
||||
if options.accept_buildout_test_releases else None)
|
||||
)
|
||||
if find_links:
|
||||
|
|
|
@ -45,7 +45,8 @@ clean:
|
|||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
@echo "Build finished. The HTML pages are in docs/$(BUILDDIR)/html."
|
||||
rm -rf build
|
||||
|
||||
manual: *.rst
|
||||
$(SPHINXBUILD) -b html -t manual . manual
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# icalendar documentation build configuration file
|
||||
import pkg_resources
|
||||
import datetime
|
||||
|
@ -26,9 +25,9 @@ extensions = [
|
|||
source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
project = u'icalendar'
|
||||
project = 'icalendar'
|
||||
this_year = datetime.date.today().year
|
||||
copyright = u'{}, Plone Foundation'.format(this_year)
|
||||
copyright = f'{this_year}, Plone Foundation'
|
||||
version = pkg_resources.get_distribution('icalendar').version
|
||||
release = version
|
||||
|
||||
|
@ -38,6 +37,6 @@ pygments_style = 'sphinx'
|
|||
htmlhelp_basename = 'icalendardoc'
|
||||
|
||||
man_pages = [
|
||||
('index', 'icalendar', u'icalendar Documentation',
|
||||
[u'Plone Foundation'], 1)
|
||||
('index', 'icalendar', 'icalendar Documentation',
|
||||
['Plone Foundation'], 1)
|
||||
]
|
||||
|
|
|
@ -56,6 +56,11 @@ icalendar contributors
|
|||
- Clive Stevens <clivest2@gmail.com>
|
||||
- Dalton Durst <github@daltondur.st>
|
||||
- Kamil Mańkowski <kam193@wp.pl>
|
||||
- Tobias Brox <tobias@redpill-linpro.com>
|
||||
- `Nicco Kunzmann <https://github.com/niccokunzmann>`_
|
||||
- Robert Spralja <robert.spralja@gmail.com>
|
||||
- Maurits van Rees <maurits@vanrees.org>
|
||||
- jacadzaca <vitouejj@gmail.com>
|
||||
|
||||
Find out who contributed::
|
||||
|
||||
|
|
115
docs/install.rst
115
docs/install.rst
|
@ -10,6 +10,109 @@ package, like this::
|
|||
|
||||
>>> import icalendar
|
||||
|
||||
Development Setup
|
||||
-----------------
|
||||
|
||||
To start contributing changes to icalendar,
|
||||
you can clone the project to your file system
|
||||
using Git.
|
||||
You can `fork <https://github.com/collective/icalendar/fork>`_
|
||||
the project first and clone your fork, too.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/collective/icalendar.git
|
||||
cd icalendar
|
||||
|
||||
Installing Python
|
||||
-----------------
|
||||
|
||||
You will need a version of Python installed to run the tests
|
||||
and execute the code.
|
||||
The latest version of Python 3 should work and will be enough
|
||||
to get you started.
|
||||
If you like to run the tests with different Python versions,
|
||||
the following setup proecess should work the same.
|
||||
|
||||
Install Tox
|
||||
-----------
|
||||
|
||||
First, install `tox <https://pypi.org/project/tox/>`_..
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install tox
|
||||
|
||||
From now on, tox will manage Python versions and
|
||||
test commands for you.
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
|
||||
``tox`` manages all test environments in all Python versions.
|
||||
|
||||
To run all tests in all environments, simply run ``tox``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox
|
||||
|
||||
You may not have all Python versions installed or
|
||||
you may want to run a specific one.
|
||||
Have a look at the `documentation
|
||||
<https://tox.wiki/en/latest/example/general.html#selecting-one-or-more-environments-to-run-tests-against>`__.
|
||||
This is how you can run ``tox`` with Python 3.9:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e py39
|
||||
|
||||
Accessing a ``tox`` environment
|
||||
-------------------------------
|
||||
|
||||
If you like to enter a specific tox environment,
|
||||
you can do this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
source .tox/py39/bin/activate
|
||||
|
||||
Install ``icalendar`` Manually
|
||||
-------------------------------
|
||||
|
||||
The best way to test the package is to use ``tox`` as
|
||||
described above.
|
||||
If for some reason you cannot install ``tox``, you can
|
||||
go ahead with the following section using your
|
||||
installed version of Python and ``pip``.
|
||||
|
||||
If for example, you would like to use your local copy of
|
||||
icalendar in another Python environment,
|
||||
this section explains how to do it.
|
||||
|
||||
You can install the local copy of ``icalendar`` with ``pip``
|
||||
like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd icalendar
|
||||
python -m pip install -e .
|
||||
|
||||
This installs the module and dependencies in your
|
||||
Python environment so that you can access local changes.
|
||||
If tox fails to install ``icalendar`` during its first run,
|
||||
you can activate the environment in the ``.tox`` folder and
|
||||
manually setup ``icalendar`` like this.
|
||||
|
||||
Try it out:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Python 3.9.5 (default, Nov 23 2021, 15:27:38)
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> import icalendar
|
||||
>>> icalendar.__version__
|
||||
'4.0.10.dev0'
|
||||
|
||||
Building the documentation locally
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -18,10 +121,7 @@ To build the documentation follow these steps:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/collective/icalendar.git
|
||||
$ cd icalendar
|
||||
$ virtualenv-2.7 .
|
||||
$ source bin/activate
|
||||
$ source .tox/py39/bin/activate
|
||||
$ pip install -r requirements_docs.txt
|
||||
$ cd docs
|
||||
$ make html
|
||||
|
@ -29,3 +129,10 @@ To build the documentation follow these steps:
|
|||
You can now open the output from ``_build/html/index.html``. To build the
|
||||
presentation-version use ``make presentation`` instead of ``make html``. You
|
||||
can open the presentation at ``presentation/index.html``.
|
||||
|
||||
You can also use ``tox`` to build the documentation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd icalendar
|
||||
tox -e docs
|
||||
|
|
10
setup.py
10
setup.py
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import setuptools
|
||||
import re
|
||||
|
@ -36,13 +35,6 @@ setuptools.setup(
|
|||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
|
@ -59,7 +51,7 @@ setuptools.setup(
|
|||
package_dir={'': 'src'},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
python_requires=">=3.8",
|
||||
install_requires=install_requires,
|
||||
entry_points = {'console_scripts': ['icalendar = icalendar.cli:main']},
|
||||
extras_require={
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
__version__ = '4.0.9.dev0'
|
||||
__version__ = '5.0.0a2.dev0'
|
||||
|
||||
from icalendar.cal import (
|
||||
Calendar,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Calendar is a dictionary like Python object that can render itself as VCAL
|
||||
files according to rfc2445.
|
||||
|
||||
|
@ -20,7 +19,6 @@ import pytz
|
|||
import dateutil.rrule, dateutil.tz
|
||||
from pytz.tzinfo import DstTzInfo
|
||||
|
||||
from icalendar.compat import unicode_type
|
||||
|
||||
|
||||
######################################
|
||||
|
@ -34,7 +32,7 @@ class ComponentFactory(CaselessDict):
|
|||
def __init__(self, *args, **kwargs):
|
||||
"""Set keys to upper for initial dict.
|
||||
"""
|
||||
super(ComponentFactory, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self['VEVENT'] = Event
|
||||
self['VTODO'] = Todo
|
||||
self['VJOURNAL'] = Journal
|
||||
|
@ -60,7 +58,7 @@ _marker = []
|
|||
class Component(CaselessDict):
|
||||
"""Component is the base object for calendar, Event and the other
|
||||
components defined in RFC 2445. normally you will not use this class
|
||||
directy, but rather one of the subclasses.
|
||||
directly, but rather one of the subclasses.
|
||||
"""
|
||||
|
||||
name = None # should be defined in each component
|
||||
|
@ -79,7 +77,7 @@ class Component(CaselessDict):
|
|||
def __init__(self, *args, **kwargs):
|
||||
"""Set keys to upper for initial dict.
|
||||
"""
|
||||
super(Component, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
# set parameters here for properties that use non-default values
|
||||
self.subcomponents = [] # Components can be nested.
|
||||
self.errors = [] # If we ignored exception(s) while
|
||||
|
@ -91,7 +89,7 @@ class Component(CaselessDict):
|
|||
#
|
||||
# If the parser is too strict it might prevent parsing erroneous but
|
||||
# otherwise compliant properties. So the parser is pretty lax, but it is
|
||||
# possible to test for non-complience by calling this method.
|
||||
# possible to test for non-compliance by calling this method.
|
||||
# """
|
||||
# return name in not_compliant
|
||||
|
||||
|
@ -345,7 +343,7 @@ class Component(CaselessDict):
|
|||
component = stack[-1] if stack else None
|
||||
if not component or not component.ignore_exceptions:
|
||||
raise
|
||||
component.errors.append((None, unicode_type(e)))
|
||||
component.errors.append((None, str(e)))
|
||||
continue
|
||||
|
||||
uname = name.upper()
|
||||
|
@ -410,22 +408,27 @@ class Component(CaselessDict):
|
|||
except ValueError as e:
|
||||
if not component.ignore_exceptions:
|
||||
raise
|
||||
component.errors.append((uname, unicode_type(e)))
|
||||
# fallback to vText and store the original value
|
||||
vals = types_factory['text'](vals)
|
||||
|
||||
vals.params = params
|
||||
component.add(name, vals, encode=0)
|
||||
# component.errors.append((uname, unicode_type(e)))
|
||||
# # fallback to vText and store the original value
|
||||
# vals = types_factory['text'](vals)
|
||||
#
|
||||
# vals.params = params
|
||||
# component.add(name, vals, encode=0)
|
||||
component.errors.append((uname, str(e)))
|
||||
component.add(name, None, encode=0)
|
||||
else:
|
||||
vals.params = params
|
||||
component.add(name, vals, encode=0)
|
||||
|
||||
if multiple:
|
||||
return comps
|
||||
if len(comps) > 1:
|
||||
raise ValueError('Found multiple components where '
|
||||
'only one is allowed: {st!r}'.format(**locals()))
|
||||
raise ValueError(f'Found multiple components where '
|
||||
f'only one is allowed: {st!r}')
|
||||
if len(comps) < 1:
|
||||
raise ValueError('Found no components where '
|
||||
'exactly one is required: '
|
||||
'{st!r}'.format(**locals()))
|
||||
raise ValueError(f'Found no components where '
|
||||
f'exactly one is required: '
|
||||
f'{st!r}')
|
||||
return comps[0]
|
||||
|
||||
def content_line(self, name, value, sorted=True):
|
||||
|
@ -457,7 +460,7 @@ class Component(CaselessDict):
|
|||
"""String representation of class with all of it's subcomponents.
|
||||
"""
|
||||
subs = ', '.join([str(it) for it in self.subcomponents])
|
||||
return '%s(%s%s)' % (
|
||||
return '{}({}{})'.format(
|
||||
self.name or type(self).__name__,
|
||||
dict(self),
|
||||
', %s' % subs if subs else ''
|
||||
|
@ -634,7 +637,7 @@ class Timezone(Component):
|
|||
tzname = component['TZNAME'].encode('ascii', 'replace')
|
||||
tzname = self._make_unique_tzname(tzname, tznames)
|
||||
except KeyError:
|
||||
tzname = '{0}_{1}_{2}_{3}'.format(
|
||||
tzname = '{}_{}_{}_{}'.format(
|
||||
zone,
|
||||
component['DTSTART'].to_ical().decode('utf-8'),
|
||||
component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from icalendar.compat import iteritems
|
||||
from icalendar.parser_tools import to_unicode
|
||||
|
||||
from collections import OrderedDict
|
||||
|
@ -30,62 +28,62 @@ class CaselessDict(OrderedDict):
|
|||
def __init__(self, *args, **kwargs):
|
||||
"""Set keys to upper for initial dict.
|
||||
"""
|
||||
super(CaselessDict, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
for key, value in self.items():
|
||||
key_upper = to_unicode(key).upper()
|
||||
if key != key_upper:
|
||||
super(CaselessDict, self).__delitem__(key)
|
||||
super().__delitem__(key)
|
||||
self[key_upper] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).__getitem__(key.upper())
|
||||
return super().__getitem__(key.upper())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
key = to_unicode(key)
|
||||
super(CaselessDict, self).__setitem__(key.upper(), value)
|
||||
super().__setitem__(key.upper(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
key = to_unicode(key)
|
||||
super(CaselessDict, self).__delitem__(key.upper())
|
||||
super().__delitem__(key.upper())
|
||||
|
||||
def __contains__(self, key):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).__contains__(key.upper())
|
||||
return super().__contains__(key.upper())
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).get(key.upper(), default)
|
||||
return super().get(key.upper(), default)
|
||||
|
||||
def setdefault(self, key, value=None):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).setdefault(key.upper(), value)
|
||||
return super().setdefault(key.upper(), value)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).pop(key.upper(), default)
|
||||
return super().pop(key.upper(), default)
|
||||
|
||||
def popitem(self):
|
||||
return super(CaselessDict, self).popitem()
|
||||
return super().popitem()
|
||||
|
||||
def has_key(self, key):
|
||||
key = to_unicode(key)
|
||||
return super(CaselessDict, self).__contains__(key.upper())
|
||||
return super().__contains__(key.upper())
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# Multiple keys where key1.upper() == key2.upper() will be lost.
|
||||
mappings = list(args) + [kwargs]
|
||||
for mapping in mappings:
|
||||
if hasattr(mapping, 'items'):
|
||||
mapping = iteritems(mapping)
|
||||
mapping = iter(mapping.items())
|
||||
for key, value in mapping:
|
||||
self[key] = value
|
||||
|
||||
def copy(self):
|
||||
return type(self)(super(CaselessDict, self).copy())
|
||||
return type(self)(super().copy())
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (type(self).__name__, dict(self))
|
||||
return f'{type(self).__name__}({dict(self)})'
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other or dict(self.items()) == dict(other.items())
|
||||
|
|
|
@ -1,43 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""iCalendar utility"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
#!/usr/bin/env python3
|
||||
"""utility program that allows user to preview calendar's events"""
|
||||
import sys
|
||||
import pathlib
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
from . import Calendar, __version__
|
||||
|
||||
|
||||
_template = """Organiser: {organiser}
|
||||
Attendees:
|
||||
{attendees}
|
||||
Summary: {summary}
|
||||
When: {time_from}-{time_to}
|
||||
Location: {location}
|
||||
Comment: {comment}
|
||||
Description:
|
||||
|
||||
{description}
|
||||
|
||||
"""
|
||||
|
||||
from icalendar import Calendar, __version__
|
||||
|
||||
def _format_name(address):
|
||||
"""Retrieve the e-mail and optionally the name from an address.
|
||||
"""Retrieve the e-mail and the name from an address.
|
||||
|
||||
:arg vCalAddress address: An address object.
|
||||
:arg an address object, e.g. mailto:test@test.test
|
||||
|
||||
:returns str: The name and optionally the e-mail address.
|
||||
:returns str: The name and the e-mail address.
|
||||
"""
|
||||
if not address:
|
||||
email = address.split(':')[-1]
|
||||
name = email.split('@')[0]
|
||||
if not email:
|
||||
return ''
|
||||
|
||||
email = address.title().split(':')[1]
|
||||
if 'cn' in address.params:
|
||||
return '{} <{}>'.format(address.params['cn'], email)
|
||||
|
||||
return email
|
||||
return f"{name} <{email}>"
|
||||
|
||||
|
||||
def _format_attendees(attendees):
|
||||
|
@ -47,65 +28,54 @@ def _format_attendees(attendees):
|
|||
|
||||
:returns str: Formatted list of attendees.
|
||||
"""
|
||||
if type(attendees) == list:
|
||||
return '\n '.join(map(_format_name, attendees))
|
||||
if isinstance(attendees, list):
|
||||
return '\n'.join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
|
||||
return _format_name(attendees)
|
||||
|
||||
|
||||
def view(input_handle, output_handle):
|
||||
def view(event):
|
||||
"""Make a human readable summary of an iCalendar file.
|
||||
|
||||
:arg stream handle: Open readable handle to an iCalendar file.
|
||||
|
||||
:returns str: Human readable summary.
|
||||
"""
|
||||
cal = Calendar.from_ical(input_handle.read())
|
||||
summary = event.get('summary', default='')
|
||||
organizer = _format_name(event.get('organizer', default=''))
|
||||
attendees = _format_attendees(event.get('attendee', default=[]))
|
||||
location = event.get('location', default='')
|
||||
comment = event.get('comment', '')
|
||||
description = event.get('description', '').split('\n')
|
||||
description = '\n'.join(map(lambda s: s.rjust(len(s) + 5), description))
|
||||
|
||||
for event in cal.walk('vevent'):
|
||||
output_handle.write(_template.format(
|
||||
organiser=_format_name(event.get('organizer', '')),
|
||||
attendees=_format_attendees(event.get('attendee')),
|
||||
summary=event.get('summary', ''),
|
||||
time_from=datetime.strftime(
|
||||
event.get('dtstart').dt, '%a %d %b %Y %H:%M'),
|
||||
time_to=datetime.strftime(event.get('dtend').dt, '%H:%M'),
|
||||
location=event.get('location', ''),
|
||||
comment=event.get('comment', ''),
|
||||
description=event.get('description', '')).encode('utf-8'))
|
||||
start = event.decoded('dtstart')
|
||||
end = event.decoded('dtend', default=start)
|
||||
duration = end - start
|
||||
start = start.astimezone(start.tzinfo).strftime('%c')
|
||||
end = end.astimezone(end.tzinfo).strftime('%c')
|
||||
|
||||
return f""" Organizer: {organizer}
|
||||
Attendees:
|
||||
{attendees}
|
||||
Summary : {summary}
|
||||
Starts : {start}
|
||||
End : {end}
|
||||
Duration : {duration}
|
||||
Location : {location}
|
||||
Comment : {comment}
|
||||
Description:
|
||||
{description}"""
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=__doc__)
|
||||
parser.add_argument(
|
||||
'-v', '--version', action='version',
|
||||
version='{} version {}'.format(parser.prog, __version__))
|
||||
|
||||
# This seems a bit of an overkill now, but we will probably add more
|
||||
# functionality later, e.g., iCalendar to JSON / YAML and vice versa.
|
||||
subparsers = parser.add_subparsers(dest='subcommand')
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
'view', description=view.__doc__.split('\n\n')[0])
|
||||
subparser.add_argument(
|
||||
'input_handle', metavar='INPUT', type=argparse.FileType('r'),
|
||||
help='iCalendar file')
|
||||
subparser.add_argument(
|
||||
'-o', dest='output_handle', metavar='OUTPUT',
|
||||
type=argparse.FileType('w'), default=sys.stdout,
|
||||
help='output file (default=<stdout>)')
|
||||
subparser.set_defaults(func=view)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
args.func(**{k: v for k, v in vars(args).items()
|
||||
if k not in ('func', 'subcommand')})
|
||||
except ValueError as error:
|
||||
parser.error(error)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('calendar_files', nargs='+', type=pathlib.Path)
|
||||
parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='output file')
|
||||
parser.add_argument('-v', '--version', action='version', version=f'{parser.prog} version {__version__}')
|
||||
argv = parser.parse_args()
|
||||
|
||||
for calendar_file in argv.calendar_files:
|
||||
with open(calendar_file) as f:
|
||||
calendar = Calendar.from_ical(f.read())
|
||||
for event in calendar.walk('vevent'):
|
||||
argv.output.write(view(event) + '\n\n')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info[0] == 2: # pragma: no cover
|
||||
unicode_type = unicode
|
||||
bytes_type = str
|
||||
iteritems = lambda d, *args, **kwargs: iter(d.iteritems(*args, **kwargs))
|
||||
else: # pragma: no cover
|
||||
unicode_type = str
|
||||
bytes_type = bytes
|
||||
iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs))
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""This module parses and generates contentlines as defined in RFC 2445
|
||||
(iCalendar), but will probably work for other MIME types with similar syntax.
|
||||
Eg. RFC 2426 (vCard)
|
||||
|
@ -6,9 +5,7 @@ Eg. RFC 2426 (vCard)
|
|||
It is stupid in the sense that it treats the content purely as strings. No type
|
||||
conversion is attempted.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from icalendar import compat
|
||||
from icalendar.caselessdict import CaselessDict
|
||||
from icalendar.parser_tools import DEFAULT_ENCODING
|
||||
from icalendar.parser_tools import SEQUENCE_TYPES
|
||||
|
@ -20,7 +17,7 @@ import re
|
|||
def escape_char(text):
|
||||
"""Format value according to iCalendar TEXT escaping rules.
|
||||
"""
|
||||
assert isinstance(text, (compat.unicode_type, compat.bytes_type))
|
||||
assert isinstance(text, (str, bytes))
|
||||
# NOTE: ORDER MATTERS!
|
||||
return text.replace(r'\N', '\n')\
|
||||
.replace('\\', '\\\\')\
|
||||
|
@ -31,16 +28,16 @@ def escape_char(text):
|
|||
|
||||
|
||||
def unescape_char(text):
|
||||
assert isinstance(text, (compat.unicode_type, compat.bytes_type))
|
||||
assert isinstance(text, (str, bytes))
|
||||
# NOTE: ORDER MATTERS!
|
||||
if isinstance(text, compat.unicode_type):
|
||||
if isinstance(text, str):
|
||||
return text.replace('\\N', '\\n')\
|
||||
.replace('\r\n', '\n')\
|
||||
.replace('\\n', '\n')\
|
||||
.replace('\\,', ',')\
|
||||
.replace('\\;', ';')\
|
||||
.replace('\\\\', '\\')
|
||||
elif isinstance(text, compat.bytes_type):
|
||||
elif isinstance(text, bytes):
|
||||
return text.replace(b'\\N', b'\\n')\
|
||||
.replace(b'\r\n', b'\n')\
|
||||
.replace(b'\n', b'\n')\
|
||||
|
@ -53,15 +50,14 @@ def tzid_from_dt(dt):
|
|||
tzid = None
|
||||
if hasattr(dt.tzinfo, 'zone'):
|
||||
tzid = dt.tzinfo.zone # pytz implementation
|
||||
elif hasattr(dt.tzinfo, 'key'):
|
||||
tzid = dt.tzinfo.key # ZoneInfo implementation
|
||||
elif hasattr(dt.tzinfo, 'tzname'):
|
||||
try:
|
||||
tzid = dt.tzinfo.tzname(dt) # dateutil implementation
|
||||
except AttributeError:
|
||||
# No tzid available
|
||||
pass
|
||||
# dateutil implementation, but this is broken
|
||||
# See https://github.com/collective/icalendar/issues/333 for details
|
||||
tzid = dt.tzinfo.tzname(dt)
|
||||
return tzid
|
||||
|
||||
|
||||
def foldline(line, limit=75, fold_sep='\r\n '):
|
||||
"""Make a string folded as defined in RFC5545
|
||||
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
|
||||
|
@ -71,7 +67,7 @@ def foldline(line, limit=75, fold_sep='\r\n '):
|
|||
immediately followed by a single linear white-space character (i.e.,
|
||||
SPACE or HTAB).
|
||||
"""
|
||||
assert isinstance(line, compat.unicode_type)
|
||||
assert isinstance(line, str)
|
||||
assert '\n' not in line
|
||||
|
||||
# Use a fast and simple variant for the common case that line is all ASCII.
|
||||
|
@ -220,7 +216,7 @@ class Parameters(CaselessDict):
|
|||
|
||||
for key, value in items:
|
||||
value = param_value(value)
|
||||
if isinstance(value, compat.unicode_type):
|
||||
if isinstance(value, str):
|
||||
value = value.encode(DEFAULT_ENCODING)
|
||||
# CaselessDict keys are always unicode
|
||||
key = key.upper().encode(DEFAULT_ENCODING)
|
||||
|
@ -285,7 +281,7 @@ def unescape_list_or_string(val):
|
|||
#########################################
|
||||
# parsing and generation of content lines
|
||||
|
||||
class Contentline(compat.unicode_type):
|
||||
class Contentline(str):
|
||||
"""A content line is basically a string that can be folded and parsed into
|
||||
parts.
|
||||
"""
|
||||
|
@ -293,7 +289,7 @@ class Contentline(compat.unicode_type):
|
|||
value = to_unicode(value, encoding=encoding)
|
||||
assert '\n' not in value, ('Content line can not contain unescaped '
|
||||
'new line characters.')
|
||||
self = super(Contentline, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
self.strict = strict
|
||||
return self
|
||||
|
||||
|
@ -315,8 +311,8 @@ class Contentline(compat.unicode_type):
|
|||
values = to_unicode(values)
|
||||
if params:
|
||||
params = to_unicode(params.to_ical(sorted=sorted))
|
||||
return cls('%s;%s:%s' % (name, params, values))
|
||||
return cls('%s:%s' % (name, values))
|
||||
return cls(f'{name};{params}:{values}')
|
||||
return cls(f'{name}:{values}')
|
||||
|
||||
def parts(self):
|
||||
"""Split the content line up into (name, parameters, values) parts.
|
||||
|
@ -344,7 +340,7 @@ class Contentline(compat.unicode_type):
|
|||
strict=self.strict)
|
||||
params = Parameters(
|
||||
(unescape_string(key), unescape_list_or_string(value))
|
||||
for key, value in compat.iteritems(params)
|
||||
for key, value in iter(params.items())
|
||||
)
|
||||
values = unescape_string(st[value_split + 1:])
|
||||
return (name, params, values)
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from icalendar import compat
|
||||
|
||||
|
||||
SEQUENCE_TYPES = (list, tuple)
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
|
@ -9,9 +5,9 @@ DEFAULT_ENCODING = 'utf-8'
|
|||
def to_unicode(value, encoding='utf-8'):
|
||||
"""Converts a value to unicode, even if it is already a unicode string.
|
||||
"""
|
||||
if isinstance(value, compat.unicode_type):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, compat.bytes_type):
|
||||
elif isinstance(value, bytes):
|
||||
try:
|
||||
value = value.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
|
@ -23,11 +19,11 @@ def data_encode(data, encoding=DEFAULT_ENCODING):
|
|||
"""Encode all datastructures to the given encoding.
|
||||
Currently unicode strings, dicts and lists are supported.
|
||||
"""
|
||||
# http://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str
|
||||
if isinstance(data, compat.unicode_type):
|
||||
# https://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str
|
||||
if isinstance(data, str):
|
||||
return data.encode(encoding)
|
||||
elif isinstance(data, dict):
|
||||
return dict(map(data_encode, compat.iteritems(data)))
|
||||
return dict(map(data_encode, iter(data.items())))
|
||||
elif isinstance(data, list) or isinstance(data, tuple):
|
||||
return list(map(data_encode, data))
|
||||
else:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the parser/generators (or coders/encoders if you
|
||||
prefer) for the classes/datatypes that are used in iCalendar:
|
||||
|
||||
|
@ -46,7 +45,6 @@ try:
|
|||
except ImportError:
|
||||
tzutc = None
|
||||
|
||||
from icalendar import compat
|
||||
from icalendar.caselessdict import CaselessDict
|
||||
from icalendar.parser import Parameters
|
||||
from icalendar.parser import escape_char
|
||||
|
@ -67,7 +65,7 @@ import time as _time
|
|||
|
||||
DATE_PART = r'(\d+)D'
|
||||
TIME_PART = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?'
|
||||
DATETIME_PART = '(?:%s)?(?:%s)?' % (DATE_PART, TIME_PART)
|
||||
DATETIME_PART = f'(?:{DATE_PART})?(?:{TIME_PART})?'
|
||||
WEEKS_PART = r'(\d+)W'
|
||||
DURATION_REGEX = re.compile(r'([-+]?)P(?:%s|%s)$'
|
||||
% (WEEKS_PART, DATETIME_PART))
|
||||
|
@ -133,7 +131,7 @@ class LocalTimezone(tzinfo):
|
|||
return tt.tm_isdst > 0
|
||||
|
||||
|
||||
class vBinary(object):
|
||||
class vBinary:
|
||||
"""Binary property values are base 64 encoded.
|
||||
"""
|
||||
|
||||
|
@ -161,7 +159,7 @@ class vBoolean(int):
|
|||
BOOL_MAP = CaselessDict({'true': True, 'false': False})
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(vBoolean, cls).__new__(cls, *args, **kwargs)
|
||||
self = super().__new__(cls, *args, **kwargs)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -178,12 +176,12 @@ class vBoolean(int):
|
|||
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)
|
||||
|
||||
|
||||
class vCalAddress(compat.unicode_type):
|
||||
class vCalAddress(str):
|
||||
"""This just returns an unquoted string.
|
||||
"""
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vCalAddress, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -202,12 +200,12 @@ class vFloat(float):
|
|||
"""Just a float.
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(vFloat, cls).__new__(cls, *args, **kwargs)
|
||||
self = super().__new__(cls, *args, **kwargs)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
def to_ical(self):
|
||||
return compat.unicode_type(self).encode('utf-8')
|
||||
return str(self).encode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical):
|
||||
|
@ -221,12 +219,12 @@ class vInt(int):
|
|||
"""Just an int.
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(vInt, cls).__new__(cls, *args, **kwargs)
|
||||
self = super().__new__(cls, *args, **kwargs)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
def to_ical(self):
|
||||
return compat.unicode_type(self).encode('utf-8')
|
||||
return str(self).encode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical):
|
||||
|
@ -236,7 +234,7 @@ class vInt(int):
|
|||
raise ValueError('Expected int, got: %s' % ical)
|
||||
|
||||
|
||||
class vDDDLists(object):
|
||||
class vDDDLists:
|
||||
"""A list of vDDDTypes values.
|
||||
"""
|
||||
def __init__(self, dt_list, type_class=None):
|
||||
|
@ -280,7 +278,7 @@ class vDDDLists(object):
|
|||
out.append(unit_type.from_ical(ical_dt, timezone=timezone))
|
||||
return out
|
||||
|
||||
class vCategory(object):
|
||||
class vCategory:
|
||||
|
||||
def __init__(self, c_list):
|
||||
if not hasattr(c_list, '__iter__'):
|
||||
|
@ -298,7 +296,7 @@ class vCategory(object):
|
|||
return out
|
||||
|
||||
|
||||
class vDDDTypes(object):
|
||||
class vDDDTypes:
|
||||
"""A combined Datetime, Date or Duration parser/generator. Their format
|
||||
cannot be confused, and often values can be of either types.
|
||||
So this is practical.
|
||||
|
@ -318,12 +316,9 @@ class vDDDTypes(object):
|
|||
|
||||
if type(dt) in (datetime, time) and hasattr(dt, 'tzinfo'):
|
||||
tzinfo = dt.tzinfo
|
||||
if tzinfo is not pytz.utc and\
|
||||
(tzutc is None or not isinstance(tzinfo, tzutc)):
|
||||
# set the timezone as a parameter to the property
|
||||
tzid = tzid_from_dt(dt)
|
||||
if tzid:
|
||||
self.params.update({'TZID': tzid})
|
||||
tzid = tzid_from_dt(dt)
|
||||
if tzid != 'UTC':
|
||||
self.params.update({'TZID': tzid})
|
||||
self.dt = dt
|
||||
|
||||
def to_ical(self):
|
||||
|
@ -339,7 +334,7 @@ class vDDDTypes(object):
|
|||
elif type(dt) is tuple and len(dt) == 2:
|
||||
return vPeriod(dt).to_ical()
|
||||
else:
|
||||
raise ValueError('Unknown date type: {}'.format(type(dt)))
|
||||
raise ValueError(f'Unknown date type: {type(dt)}')
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical, timezone=None):
|
||||
|
@ -363,7 +358,7 @@ class vDDDTypes(object):
|
|||
)
|
||||
|
||||
|
||||
class vDate(object):
|
||||
class vDate:
|
||||
"""Render and generates iCalendar date format.
|
||||
"""
|
||||
def __init__(self, dt):
|
||||
|
@ -394,7 +389,7 @@ class vDate(object):
|
|||
raise ValueError("Wrong date format '%s'" % ical)
|
||||
|
||||
|
||||
class vDatetime(object):
|
||||
class vDatetime:
|
||||
"""Render and generates icalendar datetime format.
|
||||
|
||||
vDatetime is timezone aware and uses the pytz library, an implementation of
|
||||
|
@ -474,7 +469,7 @@ class vDatetime(object):
|
|||
raise ValueError("Wrong datetime format '%s'" % ical)
|
||||
|
||||
|
||||
class vDuration(object):
|
||||
class vDuration:
|
||||
"""Subclass of timedelta that renders itself in the iCalendar DURATION
|
||||
format.
|
||||
"""
|
||||
|
@ -510,12 +505,12 @@ class vDuration(object):
|
|||
if seconds:
|
||||
timepart += "%dS" % seconds
|
||||
if td.days == 0 and timepart:
|
||||
return (compat.unicode_type(sign).encode('utf-8') + b'P' +
|
||||
compat.unicode_type(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(timepart).encode('utf-8'))
|
||||
else:
|
||||
return (compat.unicode_type(sign).encode('utf-8') + b'P' +
|
||||
compat.unicode_type(abs(td.days)).encode('utf-8') +
|
||||
b'D' + compat.unicode_type(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(abs(td.days)).encode('utf-8') +
|
||||
b'D' + str(timepart).encode('utf-8'))
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical):
|
||||
|
@ -538,7 +533,7 @@ class vDuration(object):
|
|||
raise ValueError('Invalid iCalendar duration: %s' % ical)
|
||||
|
||||
|
||||
class vPeriod(object):
|
||||
class vPeriod:
|
||||
"""A precise period of time.
|
||||
"""
|
||||
def __init__(self, per):
|
||||
|
@ -607,10 +602,10 @@ class vPeriod(object):
|
|||
p = (self.start, self.duration)
|
||||
else:
|
||||
p = (self.start, self.end)
|
||||
return 'vPeriod(%r)' % (p, )
|
||||
return f'vPeriod({p!r})'
|
||||
|
||||
|
||||
class vWeekday(compat.unicode_type):
|
||||
class vWeekday(str):
|
||||
"""This returns an unquoted weekday abbrevation.
|
||||
"""
|
||||
week_days = CaselessDict({
|
||||
|
@ -619,7 +614,7 @@ class vWeekday(compat.unicode_type):
|
|||
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vWeekday, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
match = WEEKDAY_RULE.match(self)
|
||||
if match is None:
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % self)
|
||||
|
@ -644,7 +639,7 @@ class vWeekday(compat.unicode_type):
|
|||
raise ValueError('Expected weekday abbrevation, got: %s' % ical)
|
||||
|
||||
|
||||
class vFrequency(compat.unicode_type):
|
||||
class vFrequency(str):
|
||||
"""A simple class that catches illegal values.
|
||||
"""
|
||||
|
||||
|
@ -660,7 +655,7 @@ class vFrequency(compat.unicode_type):
|
|||
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vFrequency, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
if self not in vFrequency.frequencies:
|
||||
raise ValueError('Expected frequency, got: %s' % self)
|
||||
self.params = Parameters()
|
||||
|
@ -709,7 +704,7 @@ class vRecur(CaselessDict):
|
|||
})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(vRecur, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.params = Parameters()
|
||||
|
||||
def to_ical(self):
|
||||
|
@ -751,13 +746,13 @@ class vRecur(CaselessDict):
|
|||
raise ValueError('Error in recurrence rule: %s' % ical)
|
||||
|
||||
|
||||
class vText(compat.unicode_type):
|
||||
class vText(str):
|
||||
"""Simple text.
|
||||
"""
|
||||
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vText, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
self.encoding = encoding
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
@ -774,7 +769,7 @@ class vText(compat.unicode_type):
|
|||
return cls(ical_unesc)
|
||||
|
||||
|
||||
class vTime(object):
|
||||
class vTime:
|
||||
"""Render and generates iCalendar time format.
|
||||
"""
|
||||
|
||||
|
@ -810,13 +805,13 @@ class vTime(object):
|
|||
raise ValueError("Expected time, got: '%s'" % ical)
|
||||
|
||||
|
||||
class vUri(compat.unicode_type):
|
||||
class vUri(str):
|
||||
"""Uniform resource identifier is basically just an unquoted string.
|
||||
"""
|
||||
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vUri, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -831,7 +826,7 @@ class vUri(compat.unicode_type):
|
|||
raise ValueError('Expected , got: %s' % ical)
|
||||
|
||||
|
||||
class vGeo(object):
|
||||
class vGeo:
|
||||
"""A special type that is only indirectly defined in the rfc.
|
||||
"""
|
||||
|
||||
|
@ -848,7 +843,7 @@ class vGeo(object):
|
|||
self.params = Parameters()
|
||||
|
||||
def to_ical(self):
|
||||
return '%s;%s' % (self.latitude, self.longitude)
|
||||
return f'{self.latitude};{self.longitude}'
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
|
@ -859,7 +854,7 @@ class vGeo(object):
|
|||
raise ValueError("Expected 'float;float' , got: %s" % ical)
|
||||
|
||||
|
||||
class vUTCOffset(object):
|
||||
class vUTCOffset:
|
||||
"""Renders itself as a utc offset.
|
||||
"""
|
||||
|
||||
|
@ -915,14 +910,14 @@ class vUTCOffset(object):
|
|||
return offset
|
||||
|
||||
|
||||
class vInline(compat.unicode_type):
|
||||
class vInline(str):
|
||||
"""This is an especially dumb class that just holds raw unparsed text and
|
||||
has parameters. Conversion of inline values are handled by the Component
|
||||
class, so no further processing is needed.
|
||||
"""
|
||||
def __new__(cls, value, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
self = super(vInline, cls).__new__(cls, value)
|
||||
self = super().__new__(cls, value)
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -944,7 +939,7 @@ class TypesFactory(CaselessDict):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
super(TypesFactory, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.all_types = (
|
||||
vBinary,
|
||||
vBoolean,
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import unittest
|
||||
|
||||
from icalendar import Calendar, cli
|
||||
|
||||
INPUT = '''
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Test Summary
|
||||
ORGANIZER:organizer@test.test
|
||||
ATTENDEE:attendee1@example.com
|
||||
ATTENDEE:attendee2@test.test
|
||||
COMMENT:Comment
|
||||
DTSTART;TZID=Europe/Warsaw:20220820T103400
|
||||
DTEND;TZID=Europe/Warsaw:20220820T113400
|
||||
LOCATION:New Amsterdam, 1000 Sunrise Test Street
|
||||
DESCRIPTION: Test Description
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
ORGANIZER:organizer@test.test
|
||||
ATTENDEE:attendee1@example.com
|
||||
ATTENDEE:attendee2@test.test
|
||||
SUMMARY:Test summury
|
||||
DTSTART;TZID=Europe/Warsaw:20220820T200000
|
||||
DTEND;TZID=Europe/Warsaw:20220820T203000
|
||||
LOCATION:New Amsterdam, 1010 Test Street
|
||||
DESCRIPTION:Test Description\\nThis one is multiline
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
'''
|
||||
|
||||
PROPER_OUTPUT = ''' Organizer: organizer <organizer@test.test>
|
||||
Attendees:
|
||||
attendee1 <attendee1@example.com>
|
||||
attendee2 <attendee2@test.test>
|
||||
Summary : Test Summary
|
||||
Starts : Sat Aug 20 10:34:00 2022
|
||||
End : Sat Aug 20 11:34:00 2022
|
||||
Duration : 1:00:00
|
||||
Location : New Amsterdam, 1000 Sunrise Test Street
|
||||
Comment : Comment
|
||||
Description:
|
||||
Test Description
|
||||
|
||||
Organizer: organizer <organizer@test.test>
|
||||
Attendees:
|
||||
attendee1 <attendee1@example.com>
|
||||
attendee2 <attendee2@test.test>
|
||||
Summary : Test summury
|
||||
Starts : Sat Aug 20 20:00:00 2022
|
||||
End : Sat Aug 20 20:30:00 2022
|
||||
Duration : 0:30:00
|
||||
Location : New Amsterdam, 1010 Test Street
|
||||
Comment :
|
||||
Description:
|
||||
Test Description
|
||||
This one is multiline
|
||||
|
||||
'''
|
||||
|
||||
class CLIToolTest(unittest.TestCase):
|
||||
def test_output_is_proper(self):
|
||||
self.maxDiff = None
|
||||
calendar = Calendar.from_ical(INPUT)
|
||||
output = ''
|
||||
for event in calendar.walk('vevent'):
|
||||
output += cli.view(event) + '\n\n'
|
||||
self.assertEqual(PROPER_OUTPUT, output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import datetime
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from icalendar.parser_tools import to_unicode
|
||||
import unittest
|
||||
|
||||
|
@ -153,7 +150,7 @@ END:VEVENT"""
|
|||
icalendar.Event.from_ical(ical_content).to_ical()
|
||||
|
||||
def test_issue_101(self):
|
||||
"""Issue #101 - icalender is choking on umlauts in ORGANIZER
|
||||
"""Issue #101 - icalendar is choking on umlauts in ORGANIZER
|
||||
|
||||
https://github.com/collective/icalendar/issues/101
|
||||
"""
|
||||
|
@ -471,8 +468,8 @@ END:VCALENDAR"""
|
|||
self.assertEqual(dtstart, expected)
|
||||
|
||||
try:
|
||||
expected_zone = str('(UTC-03:00) Brasília')
|
||||
expected_tzname = str('Brasília standard')
|
||||
expected_zone = '(UTC-03:00) Brasília'
|
||||
expected_tzname = 'Brasília standard'
|
||||
except UnicodeEncodeError:
|
||||
expected_zone = '(UTC-03:00) Brasília'.encode('ascii', 'replace')
|
||||
expected_tzname = 'Brasília standard'.encode('ascii', 'replace')
|
||||
|
@ -565,3 +562,16 @@ END:VCALENDAR"""
|
|||
event.add('DURATION', datetime.timedelta(hours=2))
|
||||
self.assertEqual(event["DURATION"].td, datetime.timedelta(seconds=7200)) # Official API
|
||||
self.assertEqual(event["DURATION"].dt, datetime.timedelta(seconds=7200)) # Backwards compatibility
|
||||
|
||||
def test_issue_345(self):
|
||||
"""Issue #345 - Why is tools.UIDGenerator a class (that must be instantiated) instead of a module? """
|
||||
uid1 = icalendar.tools.UIDGenerator.uid()
|
||||
uid2 = icalendar.tools.UIDGenerator.uid('test.test')
|
||||
uid3 = icalendar.tools.UIDGenerator.uid(unique='123')
|
||||
uid4 = icalendar.tools.UIDGenerator.uid('test.test', '123')
|
||||
|
||||
self.assertEqual(uid1.split('@')[1], 'example.com')
|
||||
self.assertEqual(uid2.split('@')[1], 'test.test')
|
||||
self.assertEqual(uid3.split('-')[1], '123@example.com')
|
||||
self.assertEqual(uid4.split('-')[1], '123@test.test')
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import icalendar
|
||||
import os
|
||||
import textwrap
|
||||
|
@ -59,13 +56,13 @@ class IcalendarTestCase (unittest.TestCase):
|
|||
'123456789 123456789 123456789 123456789 ')
|
||||
)
|
||||
|
||||
# http://tools.ietf.org/html/rfc5545#section-3.3.11
|
||||
# https://tools.ietf.org/html/rfc5545#section-3.3.11
|
||||
# An intentional formatted text line break MUST only be included in
|
||||
# a "TEXT" property value by representing the line break with the
|
||||
# character sequence of BACKSLASH, followed by a LATIN SMALL LETTER
|
||||
# N or a LATIN CAPITAL LETTER N, that is "\n" or "\N".
|
||||
|
||||
# Newlines are not allwoed in content lines
|
||||
# Newlines are not allowed in content lines
|
||||
self.assertRaises(AssertionError, Contentline, b'1234\r\n\r\n1234')
|
||||
|
||||
self.assertEqual(
|
||||
|
@ -165,14 +162,14 @@ class IcalendarTestCase (unittest.TestCase):
|
|||
)
|
||||
|
||||
# And the traditional failure
|
||||
with self.assertRaisesRegexp(
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Content line could not be parsed into parts'
|
||||
):
|
||||
Contentline('ATTENDEE;maxm@example.com').parts()
|
||||
|
||||
# Another failure:
|
||||
with self.assertRaisesRegexp(
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Content line could not be parsed into parts'
|
||||
):
|
||||
|
@ -189,7 +186,7 @@ class IcalendarTestCase (unittest.TestCase):
|
|||
)
|
||||
|
||||
# Should bomb on missing param:
|
||||
with self.assertRaisesRegexp(
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Content line could not be parsed into parts'
|
||||
):
|
||||
|
@ -214,12 +211,12 @@ class IcalendarTestCase (unittest.TestCase):
|
|||
)
|
||||
|
||||
contains_base64 = (
|
||||
'X-APPLE-STRUCTURED-LOCATION;'
|
||||
'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";'
|
||||
'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;'
|
||||
'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;'
|
||||
'X-TITLE=Heldenplatz:geo:48.206686,16.363235'
|
||||
).encode('utf-8')
|
||||
b'X-APPLE-STRUCTURED-LOCATION;'
|
||||
b'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";'
|
||||
b'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;'
|
||||
b'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;'
|
||||
b'X-TITLE=Heldenplatz:geo:48.206686,16.363235'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
Contentline(contains_base64, strict=True).parts(),
|
||||
|
@ -252,7 +249,7 @@ class IcalendarTestCase (unittest.TestCase):
|
|||
# at least just but bytes in there
|
||||
# porting it to "run" under python 2 & 3 makes it not much better
|
||||
with self.assertRaises(AssertionError):
|
||||
foldline('привет'.encode('utf-8'), limit=3)
|
||||
foldline('привет'.encode(), limit=3)
|
||||
|
||||
self.assertEqual(foldline('foobar', limit=4), 'foo\r\n bar')
|
||||
self.assertEqual(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from icalendar import Calendar
|
||||
from icalendar.prop import vText
|
||||
import unittest
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from icalendar import Calendar
|
||||
from icalendar import Event
|
||||
from icalendar import Parameters
|
||||
|
@ -115,11 +112,11 @@ class TestPropertyParams(unittest.TestCase):
|
|||
self.assertEqual(p['parameter1'], 'Value1')
|
||||
self.assertEqual(p['PARAMETER1'], 'Value1')
|
||||
|
||||
# Parameter with list of values must be seperated by comma
|
||||
# Parameter with list of values must be separated by comma
|
||||
p = Parameters({'parameter1': ['Value1', 'Value2']})
|
||||
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1,Value2')
|
||||
|
||||
# Multiple parameters must be seperated by a semicolon
|
||||
# Multiple parameters must be separated by a semicolon
|
||||
p = Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'})
|
||||
self.assertEqual(p.to_ical(), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE')
|
||||
|
||||
|
@ -127,7 +124,7 @@ class TestPropertyParams(unittest.TestCase):
|
|||
p = Parameters({'ALTREP': 'http://www.wiz.org'})
|
||||
self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"')
|
||||
|
||||
# list items must be quoted seperately
|
||||
# list items must be quoted separately
|
||||
p = Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']})
|
||||
self.assertEqual(
|
||||
|
@ -175,7 +172,7 @@ class TestPropertyParams(unittest.TestCase):
|
|||
|
||||
def test_parse_and_access_property_params(self):
|
||||
"""Parse an ics string and access some property parameters then.
|
||||
This is a follow-up of a question recieved per email.
|
||||
This is a follow-up of a question received per email.
|
||||
|
||||
"""
|
||||
ics = """BEGIN:VCALENDAR
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from icalendar.caselessdict import CaselessDict
|
||||
import unittest
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import datetime
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import datetime
|
||||
|
@ -8,11 +5,53 @@ import dateutil.parser
|
|||
import icalendar
|
||||
import os
|
||||
import pytz
|
||||
|
||||
try:
|
||||
import zoneinfo
|
||||
except:
|
||||
try:
|
||||
from backports import zoneinfo
|
||||
except:
|
||||
zoneinfo = None
|
||||
|
||||
class TestTimezoned(unittest.TestCase):
|
||||
|
||||
def test_create_from_ical(self):
|
||||
def test_create_from_ical_zoneinfo(self):
|
||||
if zoneinfo is None:
|
||||
self.skipTest("zoneinfo library not found for this python version")
|
||||
|
||||
directory = os.path.dirname(__file__)
|
||||
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
|
||||
data = fp.read()
|
||||
cal = icalendar.Calendar.from_ical(data)
|
||||
|
||||
self.assertEqual(
|
||||
cal['prodid'].to_ical(),
|
||||
b"-//Plone.org//NONSGML plone.app.event//EN"
|
||||
)
|
||||
|
||||
timezones = cal.walk('VTIMEZONE')
|
||||
self.assertEqual(len(timezones), 1)
|
||||
|
||||
tz = timezones[0]
|
||||
self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna")
|
||||
|
||||
std = tz.walk('STANDARD')[0]
|
||||
self.assertEqual(
|
||||
std.decoded('TZOFFSETFROM'),
|
||||
datetime.timedelta(0, 7200)
|
||||
)
|
||||
|
||||
ev1 = cal.walk('VEVENT')[0]
|
||||
self.assertEqual(
|
||||
ev1.decoded('DTSTART'),
|
||||
datetime.datetime(2012, 2, 13, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Vienna'))
|
||||
)
|
||||
self.assertEqual(
|
||||
ev1.decoded('DTSTAMP'),
|
||||
datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=zoneinfo.ZoneInfo('UTC'))
|
||||
)
|
||||
|
||||
def test_create_from_ical_pytz(self):
|
||||
directory = os.path.dirname(__file__)
|
||||
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
|
||||
data = fp.read()
|
||||
|
@ -49,7 +88,7 @@ class TestTimezoned(unittest.TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_create_to_ical(self):
|
||||
def test_create_to_ical_pytz(self):
|
||||
cal = icalendar.Calendar()
|
||||
|
||||
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
|
||||
|
@ -109,6 +148,92 @@ class TestTimezoned(unittest.TestCase):
|
|||
event.add('attendee', 'franz')
|
||||
event.add('attendee', 'sepp')
|
||||
event.add('contact', 'Max Mustermann, 1010 Wien')
|
||||
event.add('url', 'https://plone.org')
|
||||
cal.add_component(event)
|
||||
|
||||
test_out = b'|'.join(cal.to_ical().splitlines())
|
||||
test_out = test_out.decode('utf-8')
|
||||
|
||||
vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:"
|
||||
"Europe/Vienna|BEGIN:STANDARD|DTSTART;VALUE=DATE-TIME:19701025T03"
|
||||
"0000|RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10|RRULE:FREQ=YEARLY;B"
|
||||
"YDAY=-1SU;BYMONTH=3|TZNAME:CET|TZOFFSETFROM:+0200|TZOFFSETTO:+01"
|
||||
"00|END:STANDARD|BEGIN:DAYLIGHT|DTSTART;VALUE=DATE-TIME:19700329T"
|
||||
"020000|TZNAME:CEST|TZOFFSETFROM:+0100|TZOFFSETTO:+0200|END:DAYLI"
|
||||
"GHT|END:VTIMEZONE"
|
||||
self.assertTrue(vtimezone_lines in test_out)
|
||||
|
||||
test_str = "DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20120213T100000"
|
||||
self.assertTrue(test_str in test_out)
|
||||
self.assertTrue("ATTENDEE:sepp" in test_out)
|
||||
|
||||
# ical standard expects DTSTAMP and CREATED in UTC
|
||||
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
|
||||
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
|
||||
|
||||
def test_create_to_ical_zoneinfo(self):
|
||||
if zoneinfo is None:
|
||||
self.skipTest("zoneinfo library not found for this python version")
|
||||
|
||||
cal = icalendar.Calendar()
|
||||
|
||||
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
|
||||
cal.add('version', "2.0")
|
||||
cal.add('x-wr-calname', "test create calendar")
|
||||
cal.add('x-wr-caldesc', "icalendar tests")
|
||||
cal.add('x-wr-relcalid', "12345")
|
||||
cal.add('x-wr-timezone', "Europe/Vienna")
|
||||
|
||||
tzc = icalendar.Timezone()
|
||||
tzc.add('tzid', 'Europe/Vienna')
|
||||
tzc.add('x-lic-location', 'Europe/Vienna')
|
||||
|
||||
tzs = icalendar.TimezoneStandard()
|
||||
tzs.add('tzname', 'CET')
|
||||
tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0))
|
||||
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'})
|
||||
tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2))
|
||||
tzs.add('TZOFFSETTO', datetime.timedelta(hours=1))
|
||||
|
||||
tzd = icalendar.TimezoneDaylight()
|
||||
tzd.add('tzname', 'CEST')
|
||||
tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0))
|
||||
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'})
|
||||
tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1))
|
||||
tzd.add('TZOFFSETTO', datetime.timedelta(hours=2))
|
||||
|
||||
tzc.add_component(tzs)
|
||||
tzc.add_component(tzd)
|
||||
cal.add_component(tzc)
|
||||
|
||||
event = icalendar.Event()
|
||||
tz = zoneinfo.ZoneInfo("Europe/Vienna")
|
||||
event.add(
|
||||
'dtstart',
|
||||
datetime.datetime(2012, 2, 13, 10, 00, 00, tzinfo=tz))
|
||||
event.add(
|
||||
'dtend',
|
||||
datetime.datetime(2012, 2, 17, 18, 00, 00, tzinfo=tz))
|
||||
event.add(
|
||||
'dtstamp',
|
||||
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
|
||||
event.add(
|
||||
'created',
|
||||
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
|
||||
event.add('uid', '123456')
|
||||
event.add(
|
||||
'last-modified',
|
||||
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
|
||||
event.add('summary', 'artsprint 2012')
|
||||
# event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10')
|
||||
event.add('description', 'sprinting at the artsprint')
|
||||
event.add('location', 'aka bild, wien')
|
||||
event.add('categories', 'first subject')
|
||||
event.add('categories', 'second subject')
|
||||
event.add('attendee', 'häns')
|
||||
event.add('attendee', 'franz')
|
||||
event.add('attendee', 'sepp')
|
||||
event.add('contact', 'Max Mustermann, 1010 Wien')
|
||||
event.add('url', 'http://plone.org')
|
||||
cal.add_component(event)
|
||||
|
||||
|
@ -132,6 +257,7 @@ class TestTimezoned(unittest.TestCase):
|
|||
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
|
||||
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
|
||||
|
||||
|
||||
def test_tzinfo_dateutil(self):
|
||||
# Test for issues #77, #63
|
||||
# references: #73,7430b66862346fe3a6a100ab25e35a8711446717
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import unittest
|
||||
|
@ -96,7 +93,7 @@ class TestCalComponent(unittest.TestCase):
|
|||
b'BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n'
|
||||
)
|
||||
|
||||
# Components can be nested, so You can add a subcompont. Eg a calendar
|
||||
# Components can be nested, so You can add a subcomponent. Eg a calendar
|
||||
# holds events.
|
||||
e = Component(summary='A brief history of time')
|
||||
e.name = 'VEVENT'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import icalendar
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from icalendar.parser_tools import data_encode
|
||||
from icalendar.parser_tools import to_unicode
|
||||
import unittest
|
||||
|
@ -12,9 +9,9 @@ class TestParserTools(unittest.TestCase):
|
|||
|
||||
self.assertEqual(to_unicode(b'spam'), 'spam')
|
||||
self.assertEqual(to_unicode('spam'), 'spam')
|
||||
self.assertEqual(to_unicode('spam'.encode('utf-8')), 'spam')
|
||||
self.assertEqual(to_unicode(b'spam'), 'spam')
|
||||
self.assertEqual(to_unicode(b'\xc6\xb5'), '\u01b5')
|
||||
self.assertEqual(to_unicode('\xc6\xb5'.encode('iso-8859-1')),
|
||||
self.assertEqual(to_unicode(b'\xc6\xb5'),
|
||||
'\u01b5')
|
||||
self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), '\u01b5')
|
||||
self.assertEqual(to_unicode(1), 1)
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from icalendar.tools import UIDGenerator
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# we save all timezone with TZIDs unknow to the TZDB in here
|
||||
# we save all timezone with TZIDs unknown to the TZDB in here
|
||||
_timezone_cache = {}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from icalendar.parser_tools import to_unicode
|
||||
from icalendar.prop import vDatetime
|
||||
|
@ -9,26 +8,28 @@ from string import digits
|
|||
import random
|
||||
|
||||
|
||||
class UIDGenerator(object):
|
||||
class UIDGenerator:
|
||||
"""If you are too lazy to create real uid's.
|
||||
|
||||
"""
|
||||
chars = list(ascii_letters + digits)
|
||||
|
||||
def rnd_string(self, length=16):
|
||||
@staticmethod
|
||||
def rnd_string(length=16):
|
||||
"""Generates a string with random characters of length.
|
||||
"""
|
||||
return ''.join([random.choice(self.chars) for _ in range(length)])
|
||||
return ''.join([random.choice(UIDGenerator.chars) for _ in range(length)])
|
||||
|
||||
def uid(self, host_name='example.com', unique=''):
|
||||
@staticmethod
|
||||
def uid(host_name='example.com', unique=''):
|
||||
"""Generates a unique id consisting of:
|
||||
datetime-uniquevalue@host.
|
||||
Like:
|
||||
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
|
||||
"""
|
||||
host_name = to_unicode(host_name)
|
||||
unique = unique or self.rnd_string()
|
||||
unique = unique or UIDGenerator.rnd_string()
|
||||
today = to_unicode(vDatetime(datetime.today()).to_ical())
|
||||
return vText('%s-%s@%s' % (today,
|
||||
return vText('{}-{}@{}'.format(today,
|
||||
unique,
|
||||
host_name))
|
||||
|
|
|
@ -5,8 +5,8 @@ The data is taken from the unicode consortium [0], the proposal and rationale
|
|||
for this mapping is also available at the unicode consortium [1].
|
||||
|
||||
|
||||
[0] http://www.unicode.org/cldr/charts/29/supplemental/zone_tzid.html
|
||||
[1] http://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa
|
||||
[0] https://www.unicode.org/cldr/cldr-aux/charts/29/supplemental/zone_tzid.html
|
||||
[1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa
|
||||
"""
|
||||
|
||||
WINDOWS_TO_OLSON = {
|
||||
|
|
17
tox.ini
17
tox.ini
|
@ -1,15 +1,24 @@
|
|||
# to run for a specific environment, use ``tox -e ENVNAME``
|
||||
[tox]
|
||||
envlist = py27,py34,py35,py36,py37,py38,py39,py310,pypy,pypy3
|
||||
envlist = py38,py39,py310,pypy3,docs
|
||||
# Note: the 'docs' env creates a 'build' directory which may interfere in strange ways
|
||||
# with the other environments. You might see this when you run the tests in parallel.
|
||||
# See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269
|
||||
|
||||
[testenv]
|
||||
usedevelop=True
|
||||
deps =
|
||||
pytest
|
||||
coverage
|
||||
py{27,34,35,36}: hypothesis>=3.0
|
||||
.[test]
|
||||
commands =
|
||||
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
|
||||
py{27,34,35,36}: coverage run --append --source=src/icalendar --omit=*/tests/* --module pytest [] src/icalendar/tests/hypothesis/
|
||||
coverage report
|
||||
coverage html
|
||||
|
||||
[testenv:docs]
|
||||
deps =
|
||||
-r {toxinidir}/requirements_docs.txt
|
||||
changedir = docs
|
||||
allowlist_externals = make
|
||||
commands =
|
||||
make html
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.10.6
|
Ładowanie…
Reference in New Issue