diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..59a692a
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,18 @@
+[report]
+# Regexes for lines to exclude from consideration
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ # Don't complain about missing debug-only code:
+ def __repr__
+ if self\.debug
+
+ # Don't complain if tests don't hit defensive assertion code:
+ raise AssertionError
+ raise NotImplementedError
+
+ # Don't complain if non-runnable code isn't run:
+ if 0:
+ if __name__ == .__main__.:
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3b3d627
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: python
+
+python:
+ - "2.6"
+ - "2.7"
+ - "3.3"
+ - "3.4"
+
+# command to install dependencies
+install:
+ - pip install -r requirements.txt
+ - pip install -r dev_requirements.txt
+ - pip install python-coveralls
+
+# command to run tests
+script: py.test --cov pyexchange --cov-report term-missing tests
+
+after_success:
+ - coveralls
diff --git a/CHANGES.rst b/CHANGES.rst
index be896d1..9c1c9c0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -40,3 +40,52 @@ Ben Le (kantas92)
* Fixed unicode vs bytecode encoding madness when sending unicode.
+0.4.1 (June 15, 2014)
+------------------
+
+Turns out I actually didn't release Ben Le's code when I thought I did. Bad release engineer, no biscuit.
+
+0.4.2 (October 3, 2014)
+----------------------
+
+Alejandro Ramirez (got-root):
+
+- Bug fixes around the new folder code.
+- More documentation on how to use folders.
+
+
+0.5 (October 15, 2014)
+----------------------
+
+** This release has a potential backwards incompatible change, see below **
+
+* Pyexchange uses requests under the hood now (@trustrachel)
+
+ Hey did you know that requests can do NTLM? I didn't. The internal connection class now uses requests
+ instead of the clunky urllib2.
+
+ There's a backwards incompatible change if you're subclassing the connection object. Requests doesn't
+ need nearly the crud that urllib2 did, so I changed some of the methods and properties.
+
+ Almost nobody should use this feature, but beware if you do.
+
+* You can get a list of events between two dates. This was a big limitation of the library before, so a huge
+ thank you to Eric Matthews (@ematthews))
+
+* Fixed bug causing retrieved events to not be in UTC. (Thanks to Alejandro Ramirez (@got-root))
+
+* Integrated with travis (finally).
+
+0.5.1 (Nov 17, 2014)
+--------------------
+
+* Bugfix release because we broke stuff :(
+
+
+0.6 (January 20, 2015)
+----------------------
+
+* Python 3 conversion complete! yaaaaaaaaaay
+
+
+
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index ccc1676..429fc3f 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -27,7 +27,7 @@ Running the tests
Make sure you have the development libraries installed, then run::
- nosetests
+ py.test tests
Building documentation
``````````````````````
@@ -52,18 +52,19 @@ The code follows `PEP8
* Line length: use your best judgment. (We all have big monitors now, no need to limit to 80 columns.)
Your code should pass `flake8
-`_ unless readability is hurt, you have a good reason, or we really like you. Configuration is in ``setup.cfg``.
+`_ unless readability is hurt. Configuration is in ``setup.cfg``.
-Python 3
-````````
+Python versions
+```````````````
-If possible, your code should be compatible with Python 3.3.
+Your code should work with all versions of Python 2.6 and 2.7. If possible, your code should be compatible with Python 3.3+.
+Travis will check that for you automatically.
Tests
`````
-Submitted code should have tests covering the code submitted.
+Submitted code should have tests covering the code submitted, and your code should pass the Travis build.
All fixture data should be unicode, following the guidelines in Ned Batchelder's fantastic `Pragmatic Unicode `_.
diff --git a/README.md b/README.md
index c69f4ac..e6f9630 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
PyExchange
===================
+[](https://travis-ci.org/linkedin/pyexchange) [](https://coveralls.io/r/linkedin/pyexchange?branch=master)
+
PyExchange is a library for Microsoft Exchange.
It's incomplete at the moment - it only handles calendar events. We've open sourced it because we found it useful and hope others will, too.
@@ -15,7 +17,7 @@ Go to https://pyexchange.readthedocs.org for the most recent, up-to-date version
Installation
------------
-PyExchange supports Python 2.6 and 2.7. Our code is compatible with 3.3+, but see the notes below on getting it working. Non CPython implementations may work but are not tested.
+PyExchange supports Python 2.6 and 2.7, and as of 0.6, is Python 3 compatible. Non CPython implementations may work but are not tested.
We support Exchange Server version 2010. Others will likely work but are not tested.
@@ -23,22 +25,6 @@ To install, use pip:
pip install pyexchange
-PyExchange requires [lxml](http://lxml.de) for XML handling. This will be installed by pip on most systems. If you run into problems, please see lxml's [installation instructions](http://lxml.de/installation.html).
-
-To install from source, download the source code, then run:
-
- python setup.py install
-
-
-Python 3
---------
-
-We use the library [python-ntlm](https://code.google.com/p/python-ntlm/) for authentication. As of July 2013, they have an experimental Python 3 port but it's not in PyPI for easy download.
-
-To the best of our knowledge, PyExchange works with this port and is Python 3.3 compatible. But since this isn't easily testable, we can't officially support Python 3 at the moment.
-
-Help in this area would be appreciated.
-
About
-----
@@ -58,8 +44,3 @@ And everybody lived happily ever after.
THE END
-
-
-
-
-
diff --git a/dev_requirements.txt b/dev_requirements.txt
index b577ca3..f3fd311 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,5 +1,7 @@
sphinx
-nose
+pytest
+pytest-cov
httpretty
flake8
-
+mock
+requests_ntlm
diff --git a/docs/exchange2010calendarevent.rst b/docs/exchange2010.rst
similarity index 74%
rename from docs/exchange2010calendarevent.rst
rename to docs/exchange2010.rst
index b4fa0c4..d9dd313 100644
--- a/docs/exchange2010calendarevent.rst
+++ b/docs/exchange2010.rst
@@ -1,13 +1,13 @@
-Exchange2010CalendarEvent
-=========================
-
.. automodule:: pyexchange.exchange2010
.. toctree::
:maxdepth: 2
+Exchange2010CalendarEvent
+=========================
+
.. autoclass:: Exchange2010CalendarEvent
- :members: create, update, cancel, resend_invitations
+ :members: create, update, cancel, resend_invitations, move_to, conflicting_events, get_occurrence, get_master
.. attribute:: id
@@ -122,6 +122,29 @@ Exchange2010CalendarEvent
Returns a :class:`ExchangeEventAttendee` object.
+ .. attribute:: recurrence
+
+ A property to set the recurrence type for the event. Possible values are: 'daily', 'weekly', 'monthly', 'yearly'.
+
+ .. attribute:: recurrence_interval
+
+ A property to set the recurrence interval for the event. This should be an int and applies to the following types of recurring events: 'daily', 'weekly', 'monthly'.
+ It should be a value between 1 and 999 for 'daily'.
+ It should be a value between 1 and 99 for 'weekly' and 'monthly'.
+
+ .. attribute:: recurrence_end_date
+
+ Should be a datetime.date() object which specifies the end of the recurrence.
+
+ .. attribute:: recurrence_days
+
+ Used in a weekly recurrence to specify which days of the week to schedule the event. This should be a
+ string of days separated by spaces. ex. "Monday Wednesday"
+
+ .. attribute:: conflicting_event_ids
+
+ **Read-only.** The internal id Exchange uses to refer to conflicting events.
+
.. method:: add_attendee(attendees, required=True)
Adds new attendees to the event.
@@ -173,4 +196,33 @@ Exchange2010CalendarEvent
*resources* can be a list of email addresses or :class:`ExchangeEventAttendee` objects.
+Exchange2010FolderService
+=========================
+
+.. autoclass:: Exchange2010FolderService()
+ :members: get_folder, new_folder, find_folder
+
+
+Exchange2010Folder
+=========================
+
+.. autoclass:: Exchange2010Folder()
+ :members: create, delete, move_to
+
+ .. attribute:: id
+
+ **Read-only.** The internal id Exchange uses to refer to this folder.
+
+ .. attribute:: parent_id
+
+ **Read-only.** The internal id Exchange uses to refer to the parent folder.
+
+ .. attribute:: folder_type
+
+ The type of folder this is. Can be one of the following::
+
+ 'Folder', 'CalendarFolder', 'ContactsFolder', 'SearchFolder', 'TasksFolder'
+
+ .. attribute:: display_name
+ The name of the folder.
diff --git a/docs/index.rst b/docs/index.rst
index 7f86d52..0f6588c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -92,8 +92,8 @@ To create an event, use the ``new_event`` method::
)
# ...or afterwards
- event.start=datetime(2013,1,1,15,0,0, tzinfo=timezone("US/Pacific")),
- event.end=datetime(2013,1,1,21,0,0, tzinfo=timezone("US/Pacific"))
+ event.start=timezone("US/Pacific").localize(datetime(2013,1,1,15,0,0))
+ event.end=timezone("US/Pacific").localize(datetime(2013,1,1,21,0,0))
event.html_body = u"""
@@ -175,6 +175,31 @@ If the id doesn't match anything in Exchange, a ``pyexchange.exceptions.Exchange
For all other errors, we throw a ``pyexchange.exceptions.FailedExchangeException``.
+Listing events
+``````````````
+
+To list events between two dates, simply do::
+
+ events = my_calendar.list_events(
+ start=timezone("US/Eastern").localize(datetime(2014, 10, 1, 11, 0, 0)),
+ end=timezone("US/Eastern").localize(datetime(2014, 10, 29, 11, 0, 0)),
+ details=True
+ )
+
+This will return a list of Event objects that are between start and end. If no results are found, it will return an empty list (it intentionally will not throw an Exception.)::
+
+ for event in calendar_list.events:
+ print "{start} {stop} - {subject}".format(
+ start=event.start,
+ stop=event.end,
+ subject=event.subject
+ )
+
+The third argument, 'details', is optional. By default (if details is not specified, or details=False), it will return most of the fields within an event. The full details for the Organizer or Attendees field are not populated by default by Exchange. If these fields are required in your usage, then pass details=True with the request to make a second lookup for these values. The further details can also be loaded after the fact using the load_all_details() function, as below::
+
+ events = my_calendar.list_events(start, end)
+ events.load_all_details()
+
Cancelling an event
```````````````````
@@ -197,6 +222,20 @@ To resend invitations to all participants, do::
event.resend_invitations()
+Creating a new calendar
+```````````````````````
+
+To create a new exchange calendar, do::
+
+ calendar = service.folder().new_folder(
+ display_name="New Name", # This will be the display name for the new calendar. Can be set to whatever you want.
+ folder_type="CalendarFolder", # This MUST be set to the value "CalendarFolder". It tells exchange what type of folder to create.
+ parent_id='calendar', # This does not have to be 'calendar' but is recommended. The value 'calendar' will resolve to the base Calendar folder.
+ )
+ calendar.create()
+
+By creating a folder of the type "CalendarFolder", you are creating a new calendar.
+
Other tips and tricks
`````````````````````
@@ -208,8 +247,8 @@ You can pickle events if you need to serialize them. (We do this to send invites
event = service.calendar().new_event()
event.subject = u"80s Movie Night"
- event.start=datetime(2013,1,1,15,0,0, tzinfo=timezone("US/Pacific"))
- event.end=datetime(2013,1,1,21,0,0, tzinfo=timezone("US/Pacific"))
+ event.start=timezone("US/Pacific").localize(datetime(2013,1,1,15,0,0))
+ event.end=timezone("US/Pacific").localize(datetime(2013,1,1,21,0,0))
# Pickle event
pickled_event = pickle.dumps(event)
diff --git a/pyexchange/base/calendar.py b/pyexchange/base/calendar.py
index 8e5a143..36bc920 100644
--- a/pyexchange/base/calendar.py
+++ b/pyexchange/base/calendar.py
@@ -6,11 +6,6 @@
"""
from collections import namedtuple
-try:
- import simplejson as json
-except ImportError:
- import json
-
ExchangeEventOrganizer = namedtuple('ExchangeEventOrganizer', ['name', 'email'])
ExchangeEventAttendee = namedtuple('ExchangeEventAttendee', ['name', 'email', 'required'])
ExchangeEventResponse = namedtuple('ExchangeEventResponse', ['name', 'email', 'response', 'last_response', 'required'])
@@ -59,21 +54,41 @@ class BaseExchangeCalendarEvent(object):
reminder_minutes_before_start = None
is_all_day = None
+ recurrence = None
+ recurrence_end_date = None
+ recurrence_days = None
+ recurrence_interval = None
+
+ _type = None
+
_attendees = {} # people attending
_resources = {} # conference rooms attending
+ _conflicting_event_ids = []
+
_track_dirty_attributes = False
_dirty_attributes = set() # any attributes that have changed, and we need to update in Exchange
# these attributes can be pickled, or output as JSON
- DATA_ATTRIBUTES = [u'_id', u'subject', u'start', u'end', u'location', u'html_body', u'text_body', u'organizer',
- u'_attendees', u'_resources', u'reminder_minutes_before_start', u'is_all_day']
+ DATA_ATTRIBUTES = [
+ u'_id', u'subject', u'start', u'end', u'location', u'html_body', u'text_body', u'organizer',
+ u'_attendees', u'_resources', u'reminder_minutes_before_start', u'is_all_day',
+ 'recurrence', 'recurrence_interval', 'recurrence_days', 'recurrence_day',
+ ]
+
+ RECURRENCE_ATTRIBUTES = [
+ 'recurrence', 'recurrence_end_date', 'recurrence_days', 'recurrence_interval',
+ ]
- def __init__(self, service, id=None, calendar_id=u'calendar', **kwargs):
+ WEEKLY_DAYS = [u'Sunday', u'Monday', u'Tuesday', u'Wednesday', u'Thursday', u'Friday', u'Saturday']
+
+ def __init__(self, service, id=None, calendar_id=u'calendar', xml=None, **kwargs):
self.service = service
self.calendar_id = calendar_id
- if id is None:
+ if xml is not None:
+ self._init_from_xml(xml)
+ elif id is None:
self._update_properties(kwargs)
else:
self._init_from_service(id)
@@ -84,11 +99,20 @@ def _init_from_service(self, id):
""" Connect to the Exchange service and grab all the properties out of it. """
raise NotImplementedError
+ def _init_from_xml(self, xml):
+ """ Using already retrieved XML from Exchange, extract properties out of it. """
+ raise NotImplementedError
+
@property
def id(self):
""" **Read-only.** The internal id Exchange uses to refer to this event. """
return self._id
+ @property
+ def conflicting_event_ids(self):
+ """ **Read-only.** The internal id Exchange uses to refer to conflicting events. """
+ return self._conflicting_event_ids
+
@property
def change_key(self):
""" **Read-only.** When you change an event, Exchange makes you pass a change key to prevent overwriting a previous version. """
@@ -99,6 +123,11 @@ def body(self):
""" **Read-only.** Returns either the html_body or the text_body property, whichever is set. """
return self.html_body or self.text_body or None
+ @property
+ def type(self):
+ """ **Read-only.** This is an attribute pulled from an event in the exchange store. """
+ return self._type
+
@property
def attendees(self):
"""
@@ -282,7 +311,7 @@ def validate(self):
raise ValueError("Event has no end date")
if self.end < self.start:
- raise ValueError("End date is after start date")
+ raise ValueError("Start date is after end date")
if self.reminder_minutes_before_start and not isinstance(self.reminder_minutes_before_start, int):
raise TypeError("reminder_minutes_before_start must be of type int")
@@ -302,9 +331,18 @@ def cancel(self):
def resend_invitations(self):
raise NotImplementedError
+ def get_master(self):
+ raise NotImplementedError
+
+ def get_occurrance(self, instance_index):
+ raise NotImplementedError
+
+ def conflicting_events(self):
+ raise NotImplementedError
+
def as_json(self):
""" Output ourselves as JSON """
- return json.dumps(self.__getstate__())
+ raise NotImplementedError
def __getstate__(self):
""" Implemented so pickle.dumps() and pickle.loads() work """
diff --git a/pyexchange/base/soap.py b/pyexchange/base/soap.py
index e071c93..ae27d3c 100644
--- a/pyexchange/base/soap.py
+++ b/pyexchange/base/soap.py
@@ -7,6 +7,7 @@
import logging
from lxml import etree
+from lxml.builder import ElementMaker
from datetime import datetime
from pytz import utc
@@ -15,6 +16,7 @@
SOAP_NS = u'http://schemas.xmlsoap.org/soap/envelope/'
SOAP_NAMESPACES = {u's': SOAP_NS}
+S = ElementMaker(namespace=SOAP_NS, nsmap=SOAP_NAMESPACES)
log = logging.getLogger('pyexchange')
@@ -54,6 +56,7 @@ def _check_for_SOAP_fault(self, xml_tree):
if fault_nodes:
fault = fault_nodes[0]
+ log.debug(etree.tostring(fault, pretty_print=True))
raise FailedExchangeException(u"SOAP Fault from Exchange server", fault.text)
def _send_soap_request(self, xml, headers=None, retries=2, timeout=30, encoding="utf-8"):
@@ -63,18 +66,20 @@ def _send_soap_request(self, xml, headers=None, retries=2, timeout=30, encoding=
return response
def _wrap_soap_xml_request(self, exchange_xml):
- root = etree.Element(u"{%s}Envelope" % SOAP_NS)
- body = etree.SubElement(root, u"{%s}Body" % SOAP_NS)
- body.append(exchange_xml)
-
+ root = S.Envelope(S.Body(exchange_xml))
return root
def _parse_date(self, date_string):
date = datetime.strptime(date_string, self.EXCHANGE_DATE_FORMAT)
- date.replace(tzinfo=utc)
+ date = date.replace(tzinfo=utc)
return date
+ def _parse_date_only_naive(self, date_string):
+ date = datetime.strptime(date_string[0:10], self.EXCHANGE_DATE_FORMAT[0:8])
+
+ return date.date()
+
def _xpath_to_dict(self, element, property_map, namespace_map):
"""
property_map = {
@@ -105,6 +110,15 @@ def _xpath_to_dict(self, element, property_map, namespace_map):
if cast_as == u'datetime':
result_for_node.append(self._parse_date(node.text))
+ elif cast_as == u'date_only_naive':
+ result_for_node.append(self._parse_date_only_naive(node.text))
+ elif cast_as == u'int':
+ result_for_node.append(int(node.text))
+ elif cast_as == u'bool':
+ if node.text.lower() == u'true':
+ result_for_node.append(True)
+ else:
+ result_for_node.append(False)
else:
result_for_node.append(node.text)
diff --git a/pyexchange/compat.py b/pyexchange/compat.py
new file mode 100644
index 0000000..f1ee45d
--- /dev/null
+++ b/pyexchange/compat.py
@@ -0,0 +1,14 @@
+import sys
+
+IS_PYTHON3 = sys.version_info >= (3, 0)
+
+if IS_PYTHON3:
+ BASESTRING_TYPES = str
+else:
+ BASESTRING_TYPES = (str, unicode)
+
+def _unicode(item):
+ if IS_PYTHON3:
+ return str(item)
+ else:
+ return unicode(item)
\ No newline at end of file
diff --git a/pyexchange/connection.py b/pyexchange/connection.py
index 5ea5bc0..51feb5d 100644
--- a/pyexchange/connection.py
+++ b/pyexchange/connection.py
@@ -4,18 +4,10 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-import logging
-from ntlm import HTTPNtlmAuthHandler
-
-try:
- import urllib2
-except ImportError: # Python 3
- import urllib.request as urllib2
+import requests
+from requests_ntlm import HttpNtlmAuth
-try:
- from httplib import HTTPException
-except ImportError: # Python 3
- from http.client import HTTPException
+import logging
from .exceptions import FailedExchangeException
@@ -32,13 +24,13 @@ def send(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
class ExchangeNTLMAuthConnection(ExchangeBaseConnection):
""" Connection to Exchange that uses NTLM authentication """
- def __init__(self, url, username, password, **kwargs):
+ def __init__(self, url, username, password, verify_certificate=True, **kwargs):
self.url = url
self.username = username
self.password = password
-
+ self.verify_certificate = verify_certificate
self.handler = None
- self.opener = None
+ self.session = None
self.password_manager = None
def build_password_manager(self):
@@ -47,74 +39,36 @@ def build_password_manager(self):
log.debug(u'Constructing password manager')
- self.password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
- self.password_manager.add_password(None, self.url, self.username, self.password)
+ self.password_manager = HttpNtlmAuth(self.username, self.password)
return self.password_manager
- def build_handler(self):
- if self.handler:
- return self.handler
+ def build_session(self):
+ if self.session:
+ return self.session
- log.debug(u'Constructing handler')
+ log.debug(u'Constructing opener')
self.password_manager = self.build_password_manager()
- self.handler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(self.password_manager)
- return self.handler
+ self.session = requests.Session()
+ self.session.auth = self.password_manager
- def build_opener(self):
- if self.opener:
- return self.opener
+ return self.session
- log.debug(u'Constructing opener')
+ def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
+ if not self.session:
+ self.session = self.build_session()
- self.handler = self.build_handler()
- self.opener = urllib2.build_opener(self.handler)
+ try:
+ response = self.session.post(self.url, data=body, headers=headers, verify = self.verify_certificate)
+ response.raise_for_status()
+ except requests.exceptions.RequestException as err:
+ log.debug(err.response.content)
+ raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
- return self.opener
+ log.info(u'Got response: {code}'.format(code=response.status_code))
+ log.debug(u'Got response headers: {headers}'.format(headers=response.headers))
+ log.debug(u'Got body: {body}'.format(body=response.text))
- def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
- if not self.opener:
- self.opener = self.build_opener()
-
- # lxml tostring returns str in Python 2, and bytes in python 3
- # if XML is actually unicode, urllib2 will barf.
- # Oddly enough this only seems to be a problem in 2.7. 2.6 doesn't seem to care.
- if isinstance(body, str):
- body = body.decode(encoding)
-
- request = urllib2.Request(self.url, body)
-
- if headers:
- for header in headers:
- log.debug(u'Adding header: {name} - {value}'.format(name=header[0], value=header[1]))
- request.add_header(header[0], header[1])
-
- error = None
- for retry in range(retries + 1):
- log.info(u'Connection attempt #{0} of {1}'.format(retry + 1, retries))
- try:
- # retrieve the result
- log.info(u'Sending request to url: {0}'.format(request.get_full_url()))
- try:
- response = self.opener.open(request, timeout=timeout)
-
- except urllib2.HTTPError as err:
- # Called for 500 errors
- raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
-
- response_code = response.getcode()
- body = response.read().decode(encoding)
-
- log.info(u'Got response: {code}'.format(code=response_code))
- log.debug(u'Got response headers: {headers}'.format(headers=response.info()))
- log.debug(u'Got body: {body}'.format(body=body))
-
- return body
- except HTTPException as err:
- log.error(u'Caught err, retrying: {err}'.format(err=err))
- error = err
-
- # All retries used up, re-throw the exception.
- raise error
+ return response.text
diff --git a/pyexchange/exceptions.py b/pyexchange/exceptions.py
index 94bc4e1..f941827 100644
--- a/pyexchange/exceptions.py
+++ b/pyexchange/exceptions.py
@@ -38,3 +38,7 @@ class ExchangeInternalServerTransientErrorException(FailedExchangeException):
"""Raised when an internal server error occurs in Exchange and the request can actually be retried."""
pass
+
+class InvalidEventType(Exception):
+ """Raised when a method for an event gets called on the wrong type of event."""
+ pass
diff --git a/pyexchange/exchange2010/__init__.py b/pyexchange/exchange2010/__init__.py
index d201560..d27d939 100644
--- a/pyexchange/exchange2010/__init__.py
+++ b/pyexchange/exchange2010/__init__.py
@@ -4,19 +4,19 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-# TODO get flake8 to just ignore the lines I want, dangit
-# flake8: noqa
import logging
from ..base.calendar import BaseExchangeCalendarEvent, BaseExchangeCalendarService, ExchangeEventOrganizer, ExchangeEventResponse
from ..base.folder import BaseExchangeFolder, BaseExchangeFolderService
from ..base.soap import ExchangeServiceSOAP
-from ..exceptions import FailedExchangeException, ExchangeStaleChangeKeyException, ExchangeItemNotFoundException, ExchangeInternalServerTransientErrorException, ExchangeIrresolvableConflictException
+from ..exceptions import FailedExchangeException, ExchangeStaleChangeKeyException, ExchangeItemNotFoundException, ExchangeInternalServerTransientErrorException, ExchangeIrresolvableConflictException, InvalidEventType
+from ..compat import BASESTRING_TYPES
from . import soap_request
from lxml import etree
-
+from copy import deepcopy
+from datetime import date
import warnings
log = logging.getLogger("pyexchange")
@@ -37,7 +37,10 @@ def folder(self):
return Exchange2010FolderService(service=self)
def _send_soap_request(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
- headers = [("Accept", "text/xml"), ("Content-type", "text/xml; charset=%s " % encoding)]
+ headers = {
+ "Accept": "text/xml",
+ "Content-type": "text/xml; charset=%s " % encoding
+ }
return super(Exchange2010Service, self)._send_soap_request(body, headers=headers, retries=retries, timeout=timeout, encoding=encoding)
def _check_for_errors(self, xml_tree):
@@ -70,6 +73,9 @@ def _check_for_exchange_fault(self, xml_tree):
elif code.text == u"ErrorInternalServerTransientError":
# temporary internal server error. throw a special error so we can retry
raise ExchangeInternalServerTransientErrorException(u"Exchange Fault (%s) from Exchange server" % code.text)
+ elif code.text == u"ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange":
+ # just means some or all of the requested instances are out of range
+ pass
elif code.text != u"NoError":
raise FailedExchangeException(u"Exchange Fault (%s) from Exchange server" % code.text)
@@ -85,18 +91,113 @@ def get_event(self, id):
def new_event(self, **properties):
return Exchange2010CalendarEvent(service=self.service, calendar_id=self.calendar_id, **properties)
+ def list_events(self, start=None, end=None, details=False, delegate_for=None):
+ return Exchange2010CalendarEventList(service=self.service, calendar_id=self.calendar_id, start=start, end=end, details=details, delegate_for=delegate_for)
+
+
+class Exchange2010CalendarEventList(object):
+ """
+ Creates & Stores a list of Exchange2010CalendarEvent items in the "self.events" variable.
+ """
+
+ def __init__(self, service=None, calendar_id=u'calendar', start=None, end=None, details=False, delegate_for=None):
+ self.service = service
+ self.count = 0
+ self.start = start
+ self.end = end
+ self.events = list()
+ self.event_ids = list()
+ self.details = details
+ self.delegate_for = delegate_for
+
+ # This request uses a Calendar-specific query between two dates.
+ body = soap_request.get_calendar_items(format=u'AllProperties', calendar_id=calendar_id, start=self.start, end=self.end, delegate_for=self.delegate_for)
+ response_xml = self.service.send(body)
+ self._parse_response_for_all_events(response_xml)
+
+ # Populate the event ID list, for convenience reasons.
+ for event in self.events:
+ self.event_ids.append(event._id)
+
+ # If we have requested all the details, basically repeat the previous 3 steps,
+ # but instead of start/stop, we have a list of ID fields.
+ if self.details:
+ log.debug(u'Received request for all details, retrieving now!')
+ self.load_all_details()
+ return
+
+ def _parse_response_for_all_events(self, response):
+ """
+ This function will retrieve *most* of the event data, excluding Organizer & Attendee details
+ """
+ items = response.xpath(u'//m:FindItemResponseMessage/m:RootFolder/t:Items/t:CalendarItem', namespaces=soap_request.NAMESPACES)
+ if not items:
+ items = response.xpath(u'//m:GetItemResponseMessage/m:Items/t:CalendarItem', namespaces=soap_request.NAMESPACES)
+ if items:
+ self.count = len(items)
+ log.debug(u'Found %s items' % self.count)
+
+ for item in items:
+ self._add_event(xml=soap_request.M.Items(deepcopy(item)))
+ else:
+ log.debug(u'No calendar items found with search parameters.')
+
+ return self
+
+ def _add_event(self, xml=None):
+ log.debug(u'Adding new event to all events list.')
+ event = Exchange2010CalendarEvent(service=self.service, xml=xml)
+ log.debug(u'Subject of new event is %s' % event.subject)
+ self.events.append(event)
+ return self
+
+ def load_all_details(self):
+ """
+ This function will execute all the event lookups for known events.
+
+ This is intended for use when you want to have a completely populated event entry, including
+ Organizer & Attendee details.
+ """
+ log.debug(u"Loading all details")
+ if self.count > 0:
+ # Now, empty out the events to prevent duplicates!
+ del(self.events[:])
+
+ # Send the SOAP request with the list of exchange ID values.
+ log.debug(u"Requesting all event details for events: {event_list}".format(event_list=str(self.event_ids)))
+ body = soap_request.get_item(exchange_id=self.event_ids, format=u'AllProperties')
+ response_xml = self.service.send(body)
+
+ # Re-parse the results for all the details!
+ self._parse_response_for_all_events(response_xml)
+
+ return self
+
class Exchange2010CalendarEvent(BaseExchangeCalendarEvent):
def _init_from_service(self, id):
-
+ log.debug(u'Creating new Exchange2010CalendarEvent object from ID')
body = soap_request.get_item(exchange_id=id, format=u'AllProperties')
response_xml = self.service.send(body)
properties = self._parse_response_for_get_event(response_xml)
self._update_properties(properties)
self._id = id
+ log.debug(u'Created new event object with ID: %s' % self._id)
+
+ self._reset_dirty_attributes()
+
+ return self
+
+ def _init_from_xml(self, xml=None):
+ log.debug(u'Creating new Exchange2010CalendarEvent object from XML')
+
+ properties = self._parse_response_for_get_event(xml)
+ self._update_properties(properties)
+ self._id, self._change_key = self._parse_id_and_change_key_from_response(xml)
+ log.debug(u'Created new event object with ID: %s' % self._id)
self._reset_dirty_attributes()
return self
@@ -104,6 +205,46 @@ def _init_from_service(self, id):
def as_json(self):
raise NotImplementedError
+ def validate(self):
+
+ if self.recurrence is not None:
+
+ if not (isinstance(self.recurrence_end_date, date)):
+ raise ValueError('recurrence_end_date must be of type date')
+ elif (self.recurrence_end_date < self.start.date()):
+ raise ValueError('recurrence_end_date must be after start')
+
+ if self.recurrence == u'daily':
+
+ if not (isinstance(self.recurrence_interval, int) and 1 <= self.recurrence_interval <= 999):
+ raise ValueError('recurrence_interval must be an int in the range from 1 to 999')
+
+ elif self.recurrence == u'weekly':
+
+ if not (isinstance(self.recurrence_interval, int) and 1 <= self.recurrence_interval <= 99):
+ raise ValueError('recurrence_interval must be an int in the range from 1 to 99')
+
+ if self.recurrence_days is None:
+ raise ValueError('recurrence_days is required')
+ for day in self.recurrence_days.split(' '):
+ if day not in self.WEEKLY_DAYS:
+ raise ValueError('recurrence_days received unknown value: %s' % day)
+
+ elif self.recurrence == u'monthly':
+
+ if not (isinstance(self.recurrence_interval, int) and 1 <= self.recurrence_interval <= 99):
+ raise ValueError('recurrence_interval must be an int in the range from 1 to 99')
+
+ elif self.recurrence == u'yearly':
+
+ pass # everything is pulled from start
+
+ else:
+
+ raise ValueError('recurrence received unknown value: %s' % self.recurrence)
+
+ super(Exchange2010CalendarEvent, self).validate()
+
def create(self):
"""
Creates an event in Exchange. ::
@@ -212,10 +353,17 @@ def cancel(self):
return None
def move_to(self, folder_id):
+ """
+ :param str folder_id: The Calendar ID to where you want to move the event to.
+ Moves an event to a different folder (calendar). ::
+
+ event = service.calendar().get_event(id='KEY HERE')
+ event.move_to(folder_id='NEW CALENDAR KEY HERE')
+ """
if not folder_id:
raise TypeError(u"You can't move an event to a non-existant folder")
- if not isinstance(folder_id, basestring):
+ if not isinstance(folder_id, BASESTRING_TYPES):
raise TypeError(u"folder_id must be a string")
if not self.id:
@@ -232,6 +380,100 @@ def move_to(self, folder_id):
self.calendar_id = folder_id
return self
+ def get_master(self):
+ """
+ get_master()
+ :raises InvalidEventType: When this method is called on an event that is not a Occurrence type.
+
+ This will return the master event to the occurrence.
+
+ **Examples**::
+
+ event = service.calendar().get_event(id='')
+ print event.type # If it prints out 'Occurrence' then that means we could get the master.
+
+ master = event.get_master()
+ print master.type # Will print out 'RecurringMaster'.
+
+
+ """
+
+ if self.type != 'Occurrence':
+ raise InvalidEventType("get_master method can only be called on a 'Occurrence' event type")
+
+ body = soap_request.get_master(exchange_id=self._id, format=u"AllProperties")
+ response_xml = self.service.send(body)
+
+ return Exchange2010CalendarEvent(service=self.service, xml=response_xml)
+
+ def get_occurrence(self, instance_index):
+ """
+ get_occurrence(instance_index)
+ :param iterable instance_index: This should be tuple or list of integers which correspond to occurrences.
+ :raises TypeError: When instance_index is not an iterable of ints.
+ :raises InvalidEventType: When this method is called on an event that is not a RecurringMaster type.
+
+ This will return a list of occurrence events.
+
+ **Examples**::
+
+ master = service.calendar().get_event(id='')
+
+ # The following will return the first 20 occurrences in the recurrence.
+ # If there are not 20 occurrences, it will only return what it finds.
+ occurrences = master.get_occurrence(range(1,21))
+ for occurrence in occurrences:
+ print occurrence.start
+
+ """
+
+ if not all([isinstance(i, int) for i in instance_index]):
+ raise TypeError("instance_index must be an interable of type int")
+
+ if self.type != 'RecurringMaster':
+ raise InvalidEventType("get_occurrance method can only be called on a 'RecurringMaster' event type")
+
+ body = soap_request.get_occurrence(exchange_id=self._id, instance_index=instance_index, format=u"AllProperties")
+ response_xml = self.service.send(body)
+
+ items = response_xml.xpath(u'//m:GetItemResponseMessage/m:Items', namespaces=soap_request.NAMESPACES)
+ events = []
+ for item in items:
+ event = Exchange2010CalendarEvent(service=self.service, xml=deepcopy(item))
+ if event.id:
+ events.append(event)
+
+ return events
+
+ def conflicting_events(self):
+ """
+ conflicting_events()
+
+ This will return a list of conflicting events.
+
+ **Example**::
+
+ event = service.calendar().get_event(id='')
+ for conflict in event.conflicting_events():
+ print conflict.subject
+
+ """
+
+ if not self.conflicting_event_ids:
+ return []
+
+ body = soap_request.get_item(exchange_id=self.conflicting_event_ids, format="AllProperties")
+ response_xml = self.service.send(body)
+
+ items = response_xml.xpath(u'//m:GetItemResponseMessage/m:Items', namespaces=soap_request.NAMESPACES)
+ events = []
+ for item in items:
+ event = Exchange2010CalendarEvent(service=self.service, xml=deepcopy(item))
+ if event.id:
+ events.append(event)
+
+ return events
+
def refresh_change_key(self):
body = soap_request.get_item(exchange_id=self._id, format=u"IdOnly")
@@ -255,7 +497,10 @@ def _parse_response_for_get_event(self, response):
result = self._parse_event_properties(response)
organizer_properties = self._parse_event_organizer(response)
- result[u'organizer'] = ExchangeEventOrganizer(**organizer_properties)
+ if organizer_properties is not None:
+ if 'email' not in organizer_properties:
+ organizer_properties['email'] = None
+ result[u'organizer'] = ExchangeEventOrganizer(**organizer_properties)
attendee_properties = self._parse_event_attendees(response)
result[u'_attendees'] = self._build_resource_dictionary([ExchangeEventResponse(**attendee) for attendee in attendee_properties])
@@ -263,29 +508,108 @@ def _parse_response_for_get_event(self, response):
resource_properties = self._parse_event_resources(response)
result[u'_resources'] = self._build_resource_dictionary([ExchangeEventResponse(**resource) for resource in resource_properties])
+ result['_conflicting_event_ids'] = self._parse_event_conflicts(response)
+
return result
def _parse_event_properties(self, response):
property_map = {
- u'subject' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Subject'}, # noqa
- u'location' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Location'}, # noqa
- u'availability' : { u'xpath' : u'//m:Items/t:CalendarItem/t:LegacyFreeBusyStatus'}, # noqa
- u'start' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Start', u'cast': u'datetime'}, # noqa
- u'end' : { u'xpath' : u'//m:Items/t:CalendarItem/t:End', u'cast': u'datetime'}, # noqa
- u'html_body' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Body[@BodyType="HTML"]'}, # noqa
- u'text_body' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Body[@BodyType="Text"]'}, # noqa
+ u'subject': {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Subject',
+ },
+ u'location':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Location',
+ },
+ u'availability':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:LegacyFreeBusyStatus',
+ },
+ u'start':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Start',
+ u'cast': u'datetime',
+ },
+ u'end':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:End',
+ u'cast': u'datetime',
+ },
+ u'html_body':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Body[@BodyType="HTML"]',
+ },
+ u'text_body':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Body[@BodyType="Text"]',
+ },
+ u'_type':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:CalendarItemType',
+ },
+ u'reminder_minutes_before_start':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:ReminderMinutesBeforeStart',
+ u'cast': u'int',
+ },
+ u'is_all_day':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:IsAllDayEvent',
+ u'cast': u'bool',
+ },
+ u'recurrence_end_date':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Recurrence/t:EndDateRecurrence/t:EndDate',
+ u'cast': u'date_only_naive',
+ },
+ u'recurrence_interval':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Recurrence/*/t:Interval',
+ u'cast': u'int',
+ },
+ u'recurrence_days':
+ {
+ u'xpath': u'//m:Items/t:CalendarItem/t:Recurrence/t:WeeklyRecurrence/t:DaysOfWeek',
+ },
}
- return self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)
+ result = self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)
+
+ try:
+ recurrence_node = response.xpath(u'//m:Items/t:CalendarItem/t:Recurrence', namespaces=soap_request.NAMESPACES)[0]
+ except IndexError:
+ recurrence_node = None
+
+ if recurrence_node is not None:
+
+ if recurrence_node.find('t:DailyRecurrence', namespaces=soap_request.NAMESPACES) is not None:
+ result['recurrence'] = 'daily'
+
+ elif recurrence_node.find('t:WeeklyRecurrence', namespaces=soap_request.NAMESPACES) is not None:
+ result['recurrence'] = 'weekly'
+
+ elif recurrence_node.find('t:AbsoluteMonthlyRecurrence', namespaces=soap_request.NAMESPACES) is not None:
+ result['recurrence'] = 'monthly'
+
+ elif recurrence_node.find('t:AbsoluteYearlyRecurrence', namespaces=soap_request.NAMESPACES) is not None:
+ result['recurrence'] = 'yearly'
+
+ return result
def _parse_event_organizer(self, response):
organizer = response.xpath(u'//m:Items/t:CalendarItem/t:Organizer/t:Mailbox', namespaces=soap_request.NAMESPACES)
property_map = {
- u'name' : { u'xpath' : u't:Name'}, # noqa
- u'email' : { u'xpath' : u't:EmailAddress'}, # noqa
+ u'name':
+ {
+ u'xpath': u't:Name'
+ },
+ u'email':
+ {
+ u'xpath': u't:EmailAddress'
+ },
}
if organizer:
@@ -295,10 +619,23 @@ def _parse_event_organizer(self, response):
def _parse_event_resources(self, response):
property_map = {
- u'name' : { u'xpath' : u't:Mailbox/t:Name'}, # noqa
- u'email' : { u'xpath' : u't:Mailbox/t:EmailAddress'}, # noqa
- u'response' : { u'xpath' : u't:ResponseType'}, # noqa
- u'last_response': { u'xpath' : u't:LastResponseTime', u'cast': u'datetime'}, # noqa
+ u'name':
+ {
+ u'xpath': u't:Mailbox/t:Name'
+ },
+ u'email':
+ {
+ u'xpath': u't:Mailbox/t:EmailAddress'
+ },
+ u'response':
+ {
+ u'xpath': u't:ResponseType'
+ },
+ u'last_response':
+ {
+ u'xpath': u't:LastResponseTime',
+ u'cast': u'datetime'
+ },
}
result = []
@@ -312,17 +649,31 @@ def _parse_event_resources(self, response):
if u'last_response' not in attendee_properties:
attendee_properties[u'last_response'] = None
- result.append(attendee_properties)
+ if u'email' in attendee_properties:
+ result.append(attendee_properties)
return result
def _parse_event_attendees(self, response):
property_map = {
- u'name' : { u'xpath' : u't:Mailbox/t:Name'}, # noqa
- u'email' : { u'xpath' : u't:Mailbox/t:EmailAddress'}, # noqa
- u'response' : { u'xpath' : u't:ResponseType'}, # noqa
- u'last_response': { u'xpath' : u't:LastResponseTime', u'cast': u'datetime'}, # noqa
+ u'name':
+ {
+ u'xpath': u't:Mailbox/t:Name'
+ },
+ u'email':
+ {
+ u'xpath': u't:Mailbox/t:EmailAddress'
+ },
+ u'response':
+ {
+ u'xpath': u't:ResponseType'
+ },
+ u'last_response':
+ {
+ u'xpath': u't:LastResponseTime',
+ u'cast': u'datetime'
+ },
}
result = []
@@ -335,7 +686,8 @@ def _parse_event_attendees(self, response):
if u'last_response' not in attendee_properties:
attendee_properties[u'last_response'] = None
- result.append(attendee_properties)
+ if u'email' in attendee_properties:
+ result.append(attendee_properties)
optional_attendees = response.xpath(u'//m:Items/t:CalendarItem/t:OptionalAttendees/t:Attendee', namespaces=soap_request.NAMESPACES)
@@ -346,10 +698,15 @@ def _parse_event_attendees(self, response):
if u'last_response' not in attendee_properties:
attendee_properties[u'last_response'] = None
- result.append(attendee_properties)
+ if u'email' in attendee_properties:
+ result.append(attendee_properties)
return result
+ def _parse_event_conflicts(self, response):
+ conflicting_ids = response.xpath(u'//m:Items/t:CalendarItem/t:ConflictingMeetings/t:CalendarItem/t:ItemId', namespaces=soap_request.NAMESPACES)
+ return [id_element.get(u"Id") for id_element in conflicting_ids]
+
class Exchange2010FolderService(BaseExchangeFolderService):
@@ -357,12 +714,61 @@ def folder(self, id=None, **kwargs):
return Exchange2010Folder(service=self.service, id=id, **kwargs)
def get_folder(self, id):
+ """
+ :param str id: The Exchange ID of the folder to retrieve from the Exchange store.
+
+ Retrieves the folder specified by the id, from the Exchange store.
+
+ **Examples**::
+
+ folder = service.folder().get_folder(id)
+
+ """
+
return Exchange2010Folder(service=self.service, id=id)
def new_folder(self, **properties):
+ """
+ new_folder(display_name=display_name, folder_type=folder_type, parent_id=parent_id)
+ :param str display_name: The display name given to the new folder.
+ :param str folder_type: The type of folder to create. Possible values are 'Folder',
+ 'CalendarFolder', 'ContactsFolder', 'SearchFolder', 'TasksFolder'.
+ :param str parent_id: The parent folder where the new folder will be created.
+
+ Creates a new folder with the given properties. Not saved until you call the create() method.
+
+ **Examples**::
+
+ folder = service.folder().new_folder(
+ display_name=u"New Folder Name",
+ folder_type="CalendarFolder",
+ parent_id='calendar',
+ )
+ folder.create()
+
+ """
+
return Exchange2010Folder(service=self.service, **properties)
def find_folder(self, parent_id):
+ """
+ find_folder(parent_id)
+ :param str parent_id: The parent folder to list.
+
+ This method will return a list of sub-folders to a given parent folder.
+
+ **Examples**::
+
+ # Iterate through folders within the default 'calendar' folder.
+ folders = service.folder().find_folder(parent_id='calendar')
+ for folder in folders:
+ print(folder.display_name)
+
+ # Delete all folders within the 'calendar' folder.
+ folders = service.folder().find_folder(parent_id='calendar')
+ for folder in folders:
+ folder.delete()
+ """
body = soap_request.find_folder(parent_id=parent_id, format=u'AllProperties')
response_xml = self.service.send(body)
@@ -403,6 +809,16 @@ def _init_from_xml(self, xml):
return self
def create(self):
+ """
+ Creates a folder in Exchange. ::
+
+ calendar = service.folder().new_folder(
+ display_name=u"New Folder Name",
+ folder_type="CalendarFolder",
+ parent_id='calendar',
+ )
+ calendar.create()
+ """
self.validate()
body = soap_request.new_folder(self)
@@ -413,6 +829,14 @@ def create(self):
return self
def delete(self):
+ """
+ Deletes a folder from the Exchange store. ::
+
+ folder = service.folder().get_folder(id)
+ print("Deleting folder: %s" % folder.display_name)
+ folder.delete()
+ """
+
if not self.id:
raise TypeError(u"You can't delete a folder that hasn't been created yet.")
@@ -426,10 +850,18 @@ def delete(self):
return None
def move_to(self, folder_id):
+ """
+ :param str folder_id: The Folder ID of what will be the new parent folder, of this folder.
+ Move folder to a different location, specified by folder_id::
+
+ folder = service.folder().get_folder(id)
+ folder.move_to(folder_id="ID of new location's folder")
+ """
+
if not folder_id:
raise TypeError(u"You can't move to a non-existant folder")
- if not isinstance(folder_id, basestring):
+ if not isinstance(folder_id, BASESTRING_TYPES):
raise TypeError(u"folder_id must be a string")
if not self.id:
@@ -454,7 +886,7 @@ def _parse_response_for_get_folder(self, response):
def _parse_folder_properties(self, response):
property_map = {
- u'display_name' : { u'xpath' : u't:DisplayName'},
+ u'display_name': {u'xpath': u't:DisplayName'},
}
self._id, self._change_key = self._parse_id_and_change_key_from_response(response)
diff --git a/pyexchange/exchange2010/soap_request.py b/pyexchange/exchange2010/soap_request.py
index 9c16233..5de16e9 100644
--- a/pyexchange/exchange2010/soap_request.py
+++ b/pyexchange/exchange2010/soap_request.py
@@ -6,6 +6,7 @@
"""
from lxml.builder import ElementMaker
from ..utils import convert_datetime_to_utc
+from ..compat import _unicode
MSG_NS = u'http://schemas.microsoft.com/exchange/services/2006/messages'
TYPE_NS = u'http://schemas.microsoft.com/exchange/services/2006/types'
@@ -16,7 +17,8 @@
M = ElementMaker(namespace=MSG_NS, nsmap=NAMESPACES)
T = ElementMaker(namespace=TYPE_NS, nsmap=NAMESPACES)
-EXCHANGE_DATE_FORMAT = u"%Y-%m-%dT%H:%M:%SZ"
+EXCHANGE_DATETIME_FORMAT = u"%Y-%m-%dT%H:%M:%SZ"
+EXCHANGE_DATE_FORMAT = u"%Y-%m-%d"
DISTINGUISHED_IDS = (
'calendar', 'contacts', 'deleteditems', 'drafts', 'inbox', 'journal', 'notes', 'outbox', 'sentitems',
@@ -97,17 +99,130 @@ def get_item(exchange_id, format=u"Default"):
"""
+ elements = list()
+ if type(exchange_id) == list:
+ for item in exchange_id:
+ elements.append(T.ItemId(Id=item))
+ else:
+ elements = [T.ItemId(Id=exchange_id)]
+
+ root = M.GetItem(
+ M.ItemShape(
+ T.BaseShape(format)
+ ),
+ M.ItemIds(
+ *elements
+ )
+ )
+ return root
+
+def get_calendar_items(format=u"Default", calendar_id=u'calendar', start=None, end=None, max_entries=999999, delegate_for=None):
+ start = start.strftime(EXCHANGE_DATETIME_FORMAT)
+ end = end.strftime(EXCHANGE_DATETIME_FORMAT)
+
+ if calendar_id == u'calendar':
+ if delegate_for is None:
+ target = M.ParentFolderIds(T.DistinguishedFolderId(Id=calendar_id))
+ else:
+ target = M.ParentFolderIds(
+ T.DistinguishedFolderId(
+ {'Id': 'calendar'},
+ T.Mailbox(T.EmailAddress(delegate_for))
+ )
+ )
+ else:
+ target = M.ParentFolderIds(T.FolderId(Id=calendar_id))
+
+ root = M.FindItem(
+ {u'Traversal': u'Shallow'},
+ M.ItemShape(
+ T.BaseShape(format)
+ ),
+ M.CalendarView({
+ u'MaxEntriesReturned': _unicode(max_entries),
+ u'StartDate': start,
+ u'EndDate': end,
+ }),
+ target,
+ )
+
+ return root
+
+
+def get_master(exchange_id, format=u"Default"):
+ """
+ Requests a calendar item from the store.
+
+ exchange_id is the id for this event in the Exchange store.
+
+ format controls how much data you get back from Exchange. Full docs are here, but acceptible values
+ are IdOnly, Default, and AllProperties.
+
+ http://msdn.microsoft.com/en-us/library/aa564509(v=exchg.140).aspx
+
+
+
+ {format}
+
+
+
+
+
+
+ """
+
root = M.GetItem(
M.ItemShape(
T.BaseShape(format)
),
M.ItemIds(
- T.ItemId(Id=exchange_id)
+ T.RecurringMasterItemId(OccurrenceId=exchange_id)
)
)
return root
+def get_occurrence(exchange_id, instance_index, format=u"Default"):
+ """
+ Requests one or more calendar items from the store matching the master & index.
+
+ exchange_id is the id for the master event in the Exchange store.
+
+ format controls how much data you get back from Exchange. Full docs are here, but acceptible values
+ are IdOnly, Default, and AllProperties.
+
+ GetItem Doc:
+ http://msdn.microsoft.com/en-us/library/aa564509(v=exchg.140).aspx
+ OccurrenceItemId Doc:
+ http://msdn.microsoft.com/en-us/library/office/aa580744(v=exchg.150).aspx
+
+
+
+ {format}
+
+
+ {% for index in instance_index %}
+
+ {% endfor %}
+
+
+ """
+
+ root = M.GetItem(
+ M.ItemShape(
+ T.BaseShape(format)
+ ),
+ M.ItemIds()
+ )
+
+ items_node = root.xpath("//m:ItemIds", namespaces=NAMESPACES)[0]
+ for index in instance_index:
+ items_node.append(T.OccurrenceItemId(RecurringMasterId=exchange_id, InstanceIndex=str(index)))
+ return root
+
+
def get_folder(folder_id, format=u"Default"):
id = T.DistinguishedFolderId(Id=folder_id) if folder_id in DISTINGUISHED_IDS else T.FolderId(Id=folder_id)
@@ -239,8 +354,8 @@ def new_event(event):
else:
calendar_node.append(T.ReminderIsSet('false'))
- calendar_node.append(T.Start(start.strftime(EXCHANGE_DATE_FORMAT)))
- calendar_node.append(T.End(end.strftime(EXCHANGE_DATE_FORMAT)))
+ calendar_node.append(T.Start(start.strftime(EXCHANGE_DATETIME_FORMAT)))
+ calendar_node.append(T.End(end.strftime(EXCHANGE_DATETIME_FORMAT)))
if event.is_all_day:
calendar_node.append(T.IsAllDayEvent('true'))
@@ -256,6 +371,38 @@ def new_event(event):
if event.resources:
calendar_node.append(resource_node(element=T.Resources(), resources=event.resources))
+ if event.recurrence:
+
+ if event.recurrence == u'daily':
+ recurrence = T.DailyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ )
+ elif event.recurrence == u'weekly':
+ recurrence = T.WeeklyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ T.DaysOfWeek(event.recurrence_days),
+ )
+ elif event.recurrence == u'monthly':
+ recurrence = T.AbsoluteMonthlyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ T.DayOfMonth(str(event.start.day)),
+ )
+ elif event.recurrence == u'yearly':
+ recurrence = T.AbsoluteYearlyRecurrence(
+ T.DayOfMonth(str(event.start.day)),
+ T.Month(event.start.strftime("%B")),
+ )
+
+ calendar_node.append(
+ T.Recurrence(
+ recurrence,
+ T.EndDateRecurrence(
+ T.StartDate(event.start.strftime(EXCHANGE_DATE_FORMAT)),
+ T.EndDate(event.recurrence_end_date.strftime(EXCHANGE_DATE_FORMAT)),
+ )
+ )
+ )
+
return root
@@ -367,14 +514,14 @@ def update_item(event, updated_attributes, calendar_item_update_operation_type):
start = convert_datetime_to_utc(event.start)
update_node.append(
- update_property_node(field_uri="calendar:Start", node_to_insert=T.Start(start.strftime(EXCHANGE_DATE_FORMAT)))
+ update_property_node(field_uri="calendar:Start", node_to_insert=T.Start(start.strftime(EXCHANGE_DATETIME_FORMAT)))
)
if u'end' in updated_attributes:
end = convert_datetime_to_utc(event.end)
update_node.append(
- update_property_node(field_uri="calendar:End", node_to_insert=T.End(end.strftime(EXCHANGE_DATE_FORMAT)))
+ update_property_node(field_uri="calendar:End", node_to_insert=T.End(end.strftime(EXCHANGE_DATETIME_FORMAT)))
)
if u'location' in updated_attributes:
@@ -412,4 +559,69 @@ def update_item(event, updated_attributes, calendar_item_update_operation_type):
else:
update_node.append(delete_field(field_uri="calendar:Resources"))
+ if u'reminder_minutes_before_start' in updated_attributes:
+ if event.reminder_minutes_before_start:
+ update_node.append(
+ update_property_node(field_uri="item:ReminderIsSet", node_to_insert=T.ReminderIsSet('true'))
+ )
+ update_node.append(
+ update_property_node(
+ field_uri="item:ReminderMinutesBeforeStart",
+ node_to_insert=T.ReminderMinutesBeforeStart(str(event.reminder_minutes_before_start))
+ )
+ )
+ else:
+ update_node.append(
+ update_property_node(field_uri="item:ReminderIsSet", node_to_insert=T.ReminderIsSet('false'))
+ )
+
+ if u'is_all_day' in updated_attributes:
+ update_node.append(
+ update_property_node(field_uri="calendar:IsAllDayEvent", node_to_insert=T.IsAllDayEvent(str(event.is_all_day).lower()))
+ )
+
+ for attr in event.RECURRENCE_ATTRIBUTES:
+ if attr in updated_attributes:
+
+ recurrence_node = T.Recurrence()
+
+ if event.recurrence == 'daily':
+ recurrence_node.append(
+ T.DailyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ )
+ )
+ elif event.recurrence == 'weekly':
+ recurrence_node.append(
+ T.WeeklyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ T.DaysOfWeek(event.recurrence_days),
+ )
+ )
+ elif event.recurrence == 'monthly':
+ recurrence_node.append(
+ T.AbsoluteMonthlyRecurrence(
+ T.Interval(str(event.recurrence_interval)),
+ T.DayOfMonth(str(event.start.day)),
+ )
+ )
+ elif event.recurrence == 'yearly':
+ recurrence_node.append(
+ T.AbsoluteYearlyRecurrence(
+ T.DayOfMonth(str(event.start.day)),
+ T.Month(event.start.strftime("%B")),
+ )
+ )
+
+ recurrence_node.append(
+ T.EndDateRecurrence(
+ T.StartDate(event.start.strftime(EXCHANGE_DATE_FORMAT)),
+ T.EndDate(event.recurrence_end_date.strftime(EXCHANGE_DATE_FORMAT)),
+ )
+ )
+
+ update_node.append(
+ update_property_node(field_uri="calendar:Recurrence", node_to_insert=recurrence_node)
+ )
+
return root
diff --git a/requirements.txt b/requirements.txt
index d86ca00..c620977 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
lxml
-python-ntlm
pytz
+requests
+requests-ntlm
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b9f12bb..973eb33 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@
setup(
name='pyexchange',
- version='0.4',
+ version='0.7-dev',
url='https://github.com/linkedin/pyexchange',
license='Apache',
author='Rachel Sanders',
@@ -26,13 +26,20 @@
platforms='any',
include_package_data=True,
packages=find_packages('.', exclude=['test*']),
- install_requires=['lxml', 'pytz', 'python-ntlm'],
+ install_requires=['lxml', 'pytz', 'requests', 'requests-ntlm'],
classifiers=[
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 749a72d..fdb1242 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,32 +4,5 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-from functools import wraps
-from nose.plugins.attrib import attr
-from nose.plugins.skip import SkipTest
-"""
-Got this from:
-
-http://www.natpryce.com/articles/000788.html
-https://gist.github.com/997195
-
-"""
-
-
-def fail(message):
- raise AssertionError(message)
-
-def wip(f):
- """
- Use this as a decorator to mark tests that are "works in progress"
- """
- @wraps(f)
- def run_test(*args, **kwargs):
- try:
- f(*args, **kwargs)
- except Exception as e:
- raise SkipTest("WIP test failed: " + str(e))
- fail("test passed but marked as work in progress")
- return attr('wip')(run_test)
\ No newline at end of file
diff --git a/tests/exchange2010/__init__.py b/tests/exchange2010/__init__.py
index 9ed0823..c08ca54 100644
--- a/tests/exchange2010/__init__.py
+++ b/tests/exchange2010/__init__.py
@@ -4,4 +4,3 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-__author__ = 'rsanders'
diff --git a/tests/exchange2010/fixtures.py b/tests/exchange2010/fixtures.py
index c684eee..81cdfa1 100644
--- a/tests/exchange2010/fixtures.py
+++ b/tests/exchange2010/fixtures.py
@@ -5,15 +5,44 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, date
+from pytz import utc
from collections import namedtuple
from pyexchange.base.calendar import ExchangeEventOrganizer, ExchangeEventResponse, RESPONSE_ACCEPTED, RESPONSE_DECLINED, RESPONSE_TENTATIVE, RESPONSE_UNKNOWN
-from pyexchange.exchange2010.soap_request import EXCHANGE_DATE_FORMAT
+from pyexchange.exchange2010.soap_request import EXCHANGE_DATE_FORMAT, EXCHANGE_DATETIME_FORMAT # noqa
# don't remove this - a few tests import stuff this way
-from ..fixtures import *
+from ..fixtures import * # noqa
-EventFixture = namedtuple('EventFixture', ['id', 'change_key', 'subject', 'location', 'start', 'end', 'body'])
+EventFixture = namedtuple('EventFixture', ['id', 'change_key', 'calendar_id', 'subject', 'location', 'start', 'end', 'body'])
+RecurringEventDailyFixture = namedtuple(
+ 'RecurringEventDailyFixture',
+ [
+ 'id', 'change_key', 'calendar_id', 'subject', 'location', 'start', 'end', 'body',
+ 'recurrence_end_date', 'recurrence_interval',
+ ]
+)
+RecurringEventWeeklyFixture = namedtuple(
+ 'RecurringEventWeeklyFixture',
+ [
+ 'id', 'change_key', 'calendar_id', 'subject', 'location', 'start', 'end', 'body',
+ 'recurrence_end_date', 'recurrence_interval', 'recurrence_days',
+ ]
+)
+RecurringEventMonthlyFixture = namedtuple(
+ 'RecurringEventMonthlyFixture',
+ [
+ 'id', 'change_key', 'calendar_id', 'subject', 'location', 'start', 'end', 'body',
+ 'recurrence_end_date', 'recurrence_interval',
+ ]
+)
+RecurringEventYearlyFixture = namedtuple(
+ 'RecurringEventYearlyFixture',
+ [
+ 'id', 'change_key', 'calendar_id', 'subject', 'location', 'start', 'end', 'body',
+ 'recurrence_end_date',
+ ]
+)
FolderFixture = namedtuple('FolderFixture', ['id', 'change_key', 'display_name', 'parent_id', 'folder_type'])
TEST_FOLDER = FolderFixture(
@@ -26,29 +55,115 @@
TEST_EVENT = EventFixture(id=u'AABBCCDDEEFF',
change_key=u'GGHHIIJJKKLLMM',
+ calendar_id='calendar',
subject=u'нyвrιd ѕolαr eclιpѕe',
location=u'söüth päċïfïċ (40.1°S 123.7°W)',
- start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50),
- end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51),
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
body=u'rärr ï äm ä dïnösäür')
+TEST_CONFLICT_EVENT = EventFixture(
+ id=u'aabbccddeeff',
+ change_key=u'gghhiijjkkllmm',
+ calendar_id='calendar',
+ subject=u'mч cσnflÃctÃng Ñ”vÑ”nt',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+)
+
+TEST_EVENT_LIST_START = datetime(year=2050, month=4, day=20, hour=20, minute=42, second=50)
+TEST_EVENT_LIST_END = datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51)
+
TEST_EVENT_UPDATED = EventFixture(id=u'AABBCCDDEEFF',
change_key=u'XXXXVVV',
+ calendar_id='calendar',
subject=u'spärklÿ hämstër sümmër bäll',
location=u'häppÿ fröġ länd',
- start=datetime(year=2040, month=4, day=19, hour=19, minute=41, second=49),
- end=datetime(year=2060, month=4, day=19, hour=20, minute=42, second=50),
+ start=datetime(year=2040, month=4, day=19, hour=19, minute=41, second=49, tzinfo=utc),
+ end=datetime(year=2060, month=4, day=19, hour=20, minute=42, second=50, tzinfo=utc),
body=u'śő Å›hÃńý śő véŕý Å›hÃńý')
-TEST_EVENT_MOVED = EventFixture(id=u'AABBCCDDEEFFAABBCCDDEEFF',
- change_key=u'GGHHIIJJKKLLMMGGHHIIJJKKLLMM',
- subject=u'нyвrιd ѕolαr eclιpѕe',
- location=u'söüth päċïfïċ (40.1°S 123.7°W)',
- start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50),
- end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51),
- body=u'rärr ï äm ä dïnösäür')
+TEST_EVENT_MOVED = EventFixture(
+ id=u'AABBCCDDEEFFAABBCCDDEEFF',
+ change_key=u'GGHHIIJJKKLLMMGGHHIIJJKKLLMM',
+ calendar_id='calendar',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+)
+
+TEST_RECURRING_EVENT_DAILY = RecurringEventDailyFixture(
+ id=u'AABBCCDDEEFF',
+ change_key=u'GGHHIIJJKKLLMM',
+ calendar_id='calendar',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+ recurrence_interval=1,
+ recurrence_end_date=date(year=2050, month=5, day=25),
+)
+
+TEST_RECURRING_EVENT_WEEKLY = RecurringEventWeeklyFixture(
+ id=u'AABBCCDDEEFF',
+ change_key=u'GGHHIIJJKKLLMM',
+ calendar_id='calendar',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+ recurrence_interval=1,
+ recurrence_end_date=date(year=2050, month=5, day=31),
+ recurrence_days='Monday Tuesday Friday',
+)
+
+TEST_RECURRING_EVENT_MONTHLY = RecurringEventMonthlyFixture(
+ id=u'AABBCCDDEEFF',
+ change_key=u'GGHHIIJJKKLLMM',
+ calendar_id='calendar',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+ recurrence_interval=1,
+ recurrence_end_date=date(year=2050, month=7, day=31),
+)
+
+TEST_RECURRING_EVENT_YEARLY = RecurringEventYearlyFixture(
+ id=u'AABBCCDDEEFF',
+ change_key=u'GGHHIIJJKKLLMM',
+ calendar_id='calendar',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+ recurrence_end_date=date(year=2055, month=5, day=31),
+)
+
+TEST_EVENT_DAILY_OCCURRENCES = list()
+for day in range(20, 25):
+ TEST_EVENT_DAILY_OCCURRENCES.append(
+ EventFixture(
+ id=str(day) * 10,
+ change_key=u'GGHHIIJJKKLLMM',
+ subject=u'нyвrιd ѕolαr eclιpѕe',
+ location=u'söüth päċïfïċ (40.1°S 123.7°W)',
+ start=datetime(year=2050, month=5, day=day, hour=20, minute=42, second=50, tzinfo=utc),
+ end=datetime(year=2050, month=5, day=day, hour=21, minute=43, second=51, tzinfo=utc),
+ body=u'rärr ï äm ä dïnösäür',
+ calendar_id='calendar',
+ )
+ )
-NOW = datetime.utcnow().replace(microsecond=0) # If you don't remove microseconds, it screws with datetime comparisions :/
+NOW = datetime.utcnow().replace(microsecond=0).replace(tzinfo=utc) # If you don't remove microseconds, it screws with datetime comparisions :/
ORGANIZER = ExchangeEventOrganizer(name=u'émmý ńőéthéŕ', email=u'noether@test.linkedin.com')
@@ -89,45 +204,877 @@
NoError
-
-
- IPM.Appointment
- {event.subject}
+
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 1935
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ true
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ false
+ true
+ Single
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+
+
+
+ {required_accepted.name}
+ {required_accepted.email}
+ SMTP
+
+ {required_accepted.response}
+ {required_accepted.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {required_tentative.name}
+ {required_tentative.email}
+ SMTP
+
+ {required_tentative.response}
+ {required_tentative.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {required_declined.name}
+ {required_declined.email}
+ SMTP
+
+ {required_declined.response}
+ {required_declined.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {required_unknown.name}
+ {required_unknown.email}
+ SMTP
+
+ {required_unknown.response}
+
+
+
+
+
+ {optional_accepted.name}
+ {optional_accepted.email}
+ SMTP
+
+ {optional_accepted.response}
+ {optional_accepted.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {optional_tentative.name}
+ {optional_tentative.email}
+ SMTP
+
+ {optional_tentative.response}
+ {optional_tentative.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {optional_declined.name}
+ {optional_declined.email}
+ SMTP
+
+ {optional_declined.response}
+ {optional_declined.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ {optional_unknown.name}
+ {optional_unknown.email}
+ SMTP
+
+ {optional_unknown.response}
+
+
+
+
+
+ {resource.name}
+ {resource.email}
+ SMTP
+
+ {resource.response}
+ {resource.last_response:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+ 1
+ 1
+
+
+
+ {conflict_event.subject}
+ {conflict_event.start:%Y-%m-%dT%H:%M:%SZ}
+ {conflict_event.end:%Y-%m-%dT%H:%M:%SZ}
+ Busy
+ {conflict_event.location}
+
+
+
+
+
+ my other OTHER awesome event
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ Busy
+ Outside
+
+
+ PT1H
+ (UTC-08:00) Pacific Time (US & Canada)
+ 0
+ 1
+
+
+
+
+
+
+
+""".format(event=TEST_EVENT,
+ organizer=ORGANIZER,
+ required_accepted=PERSON_REQUIRED_ACCEPTED,
+ required_tentative=PERSON_REQUIRED_TENTATIVE,
+ required_declined=PERSON_REQUIRED_DECLINED,
+ required_unknown=PERSON_REQUIRED_UNKNOWN,
+ optional_accepted=PERSON_OPTIONAL_ACCEPTED,
+ optional_tentative=PERSON_OPTIONAL_TENTATIVE,
+ optional_declined=PERSON_OPTIONAL_DECLINED,
+ optional_unknown=PERSON_OPTIONAL_UNKNOWN,
+ resource=RESOURCE,
+ conflict_event=TEST_CONFLICT_EVENT,
+ )
+
+CONFLICTING_EVENTS_RESPONSE = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 1935
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ true
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ false
+ true
+ Single
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 1
+ 1
+
+
+
+ {conflict_event.subject}
+ {conflict_event.start:%Y-%m-%dT%H:%M:%SZ}
+ {conflict_event.end:%Y-%m-%dT%H:%M:%SZ}
+ Busy
+ {conflict_event.location}
+
+
+
+
+
+ my other OTHER awesome event
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ Busy
+ Outside
+
+
+ PT1H
+ (UTC-08:00) Pacific Time (US & Canada)
+ 0
+ 1
+
+
+
+
+
+
+
+""".format(
+ event=TEST_CONFLICT_EVENT,
+ organizer=ORGANIZER,
+ conflict_event=TEST_EVENT,
+)
+
+GET_ITEM_RESPONSE_ID_ONLY = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+
+
+
+
+
+""".format(event=TEST_EVENT)
+
+
+GET_RECURRING_MASTER_DAILY_EVENT = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+^[OB
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 2527
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ true
+ RecurringMaster
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT2H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+ {event.recurrence_interval}
+
+
+ {event.start:%Y-%m-%d}-05:00
+ {event.recurrence_end_date:%Y-%m-%d}-05:00
+
+
+
+
+
+
+
+
+""".format(
+ event=TEST_RECURRING_EVENT_DAILY,
+ organizer=ORGANIZER,
+)
+
+GET_RECURRING_MASTER_WEEKLY_EVENT = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 2598
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ false
+ true
+ RecurringMaster
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT1H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+ {event.recurrence_interval}
+ {event.recurrence_days}
+
+
+ {event.start:%Y-%m-%d}-05:00
+ {event.recurrence_end_date:%Y-%m-%d}-05:00
+
+
+
+ PT360M
+
+ PT0M
+
+ Sunday
+ First
+ November
+
+ 02:00:00
+
+
+ -PT60M
+
+ Sunday
+ Second
+ March
+
+ 02:00:00
+
+
+
+
+
+
+
+
+""".format(
+ event=TEST_RECURRING_EVENT_WEEKLY,
+ organizer=ORGANIZER,
+)
+
+GET_RECURRING_MASTER_MONTHLY_EVENT = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 2588
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ false
+ true
+ RecurringMaster
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT1H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+ {event.recurrence_interval}
+ {event.start:%d}
+
+
+ {event.start:%Y-%m-%d}-05:00
+ {event.recurrence_end_date:%Y-%m-%d}-05:00
+
+
+
+ PT360M
+
+ PT0M
+
+ Sunday
+ First
+ November
+
+ 02:00:00
+
+
+ -PT60M
+
+ Sunday
+ Second
+ March
+
+ 02:00:00
+
+
+
+
+
+
+
+
+""".format(
+ event=TEST_RECURRING_EVENT_MONTHLY,
+ organizer=ORGANIZER,
+)
+
+GET_RECURRING_MASTER_YEARLY_EVENT = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment
+ {event.subject}
+ Normal
+ {event.body}
+ {event.body}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ 2535
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {event.end:%Y-%m-%dT%H:%M:%SZ}
+ false
+ Busy
+ {event.location}
+ true
+ false
+ false
+ false
+ true
+ RecurringMaster
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT2H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+ {event.start:%d}
+ {event.start:%B}
+
+
+ {event.start:%Y-%m-%d}-05:00
+ {event.recurrence_end_date:%Y-%m-%d}-05:00
+
+
+
+ PT360M
+
+ PT0M
+
+ Sunday
+ First
+ November
+
+ 02:00:00
+
+
+ -PT60M
+
+ Sunday
+ Second
+ March
+
+ 02:00:00
+
+
+
+
+
+
+
+
+
+""".format(
+ event=TEST_RECURRING_EVENT_YEARLY,
+ organizer=ORGANIZER,
+)
+
+GET_DAILY_OCCURRENCES = u"""
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment.Occurrence
+ {events[0].subject}
+ Normal
+ {events[0].body}
+ {events[0].body}
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ 2532
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[0].end:%Y-%m-%dT%H:%M:%SZ}
+ 2014-10-15T22:00:00Z
+ false
+ Busy
+ {events[0].location}
+ true
+ false
+ true
+ false
+ true
+ Occurrence
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT1H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment.Occurrence
+ {events[1].subject}
+ Normal
+ {events[1].body}
+ {events[1].body}
+ {events[1].start:%Y-%m-%dT%H:%M:%SZ}
+ 2532
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {events[1].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[1].start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {events[1].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[1].end:%Y-%m-%dT%H:%M:%SZ}
+ 2014-10-15T22:00:00Z
+ false
+ Busy
+ {events[1].location}
+ true
+ false
+ true
+ false
+ true
+ Occurrence
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT1H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment.Occurrence
+ {events[2].subject}
Normal
- {event.body}
- {event.body}
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- 1935
+ {events[2].body}
+ {events[2].body}
+ {events[2].start:%Y-%m-%dT%H:%M:%SZ}
+ 2532
Normal
false
false
false
false
false
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- {event.start:%Y-%m-%dT%H:%M:%SZ}
+ {events[2].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[2].start:%Y-%m-%dT%H:%M:%SZ}
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- true
+ false
15
false
en-US
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- {event.end:%Y-%m-%dT%H:%M:%SZ}
+ {events[2].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[2].end:%Y-%m-%dT%H:%M:%SZ}
+ 2014-10-15T22:00:00Z
false
Busy
- {event.location}
+ {events[2].location}
true
false
- false
+ true
false
true
- Single
+ Occurrence
Organizer
@@ -136,163 +1083,151 @@
SMTP
-
-
-
- {required_accepted.name}
- {required_accepted.email}
- SMTP
-
- {required_accepted.response}
- {required_accepted.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {required_tentative.name}
- {required_tentative.email}
- SMTP
-
- {required_tentative.response}
- {required_tentative.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {required_declined.name}
- {required_declined.email}
- SMTP
-
- {required_declined.response}
- {required_declined.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {required_unknown.name}
- {required_unknown.email}
- SMTP
-
- {required_unknown.response}
-
-
-
-
-
- {optional_accepted.name}
- {optional_accepted.email}
- SMTP
-
- {optional_accepted.response}
- {optional_accepted.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {optional_tentative.name}
- {optional_tentative.email}
- SMTP
-
- {optional_tentative.response}
- {optional_tentative.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {optional_declined.name}
- {optional_declined.email}
- SMTP
-
- {optional_declined.response}
- {optional_declined.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- {optional_unknown.name}
- {optional_unknown.email}
- SMTP
-
- {optional_unknown.response}
-
-
-
-
-
- {resource.name}
- {resource.email}
- SMTP
-
- {resource.response}
- {resource.last_response:%Y-%m-%dT%H:%M:%SZ}
-
-
-
- 1
- 1
-
-
-
- My other awesome event
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- {event.end:%Y-%m-%dT%H:%M:%SZ}
- Busy
- Nowhere special
-
-
-
-
-
- my other OTHER awesome event
- {event.start:%Y-%m-%dT%H:%M:%SZ}
- {event.end:%Y-%m-%dT%H:%M:%SZ}
- Busy
- Outside
-
-
+ 0
+ 0
PT1H
- (UTC-08:00) Pacific Time (US & Canada)
+ (UTC-06:00) Central Time (US & Canada)
0
1
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
-
-""".format(event=TEST_EVENT,
- organizer=ORGANIZER,
- required_accepted=PERSON_REQUIRED_ACCEPTED,
- required_tentative=PERSON_REQUIRED_TENTATIVE,
- required_declined=PERSON_REQUIRED_DECLINED,
- required_unknown=PERSON_REQUIRED_UNKNOWN,
- optional_accepted=PERSON_OPTIONAL_ACCEPTED,
- optional_tentative=PERSON_OPTIONAL_TENTATIVE,
- optional_declined=PERSON_OPTIONAL_DECLINED,
- optional_unknown=PERSON_OPTIONAL_UNKNOWN,
- resource=RESOURCE
- )
+""".format(
+ events=TEST_EVENT_DAILY_OCCURRENCES,
+ organizer=ORGANIZER,
+)
-GET_ITEM_RESPONSE_ID_ONLY = u"""
-
-
-
-
-
+GET_EVENT_OCCURRENCE = u"""
+
+
+
+
+
NoError
-
+
+
+ IPM.Appointment.Occurrence
+ {events[0].subject}
+ Normal
+ {events[0].body}
+ {events[0].body}
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ 2532
+ Normal
+ false
+ false
+ false
+ false
+ false
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+
+
+
+
+ false
+ 15
+
+
+ false
+ en-US
+ {events[0].start:%Y-%m-%dT%H:%M:%SZ}
+ {events[0].end:%Y-%m-%dT%H:%M:%SZ}
+ 2014-10-15T22:00:00Z
+ false
+ Busy
+ {events[0].location}
+ true
+ false
+ true
+ false
+ true
+ Occurrence
+ Organizer
+
+
+ {organizer.name}
+ {organizer.email}
+ SMTP
+
+
+ 0
+ 0
+ PT1H
+ (UTC-06:00) Central Time (US & Canada)
+ 0
+ 1
-
-
-""".format(event=TEST_EVENT)
+
+
+""".format(
+ events=TEST_EVENT_DAILY_OCCURRENCES,
+ organizer=ORGANIZER,
+)
+GET_EMPTY_OCCURRENCES = u"""
+
+
+
+
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+ Occurrence index is out of recurrence range.
+ ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange
+ 0
+
+
+
+
+
+"""
ITEM_DOES_NOT_EXIST = u"""
@@ -626,3 +1561,157 @@
""".format(folder=TEST_FOLDER)
+
+LIST_EVENTS_RESPONSE = u"""
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment.Occurrence
+ Event Subject 1
+ Normal
+ 2050-04-22T01:01:01Z
+ 114026
+ Normal
+ false
+ false
+ false
+ false
+ false
+ 15
+ Roe, Tim
+ false
+ en-US
+ 2050-05-01T14:30:00Z
+ 2050-05-01T16:00:00Z
+ false
+ Busy
+ Location1
+ true
+ false
+ true
+ false
+ true
+ Occurrence
+ Accept
+
+
+ Organizing User 1
+
+
+ PT1H30M
+ (UTC-05:00) Eastern Time (US & Canada)
+ 2050-04-23T16:39:38Z
+ 1
+ 3
+
+
+
+
+ IPM.Appointment.Occurrence
+ Event Subject 2
+ Normal
+ 2050-04-05T15:22:06Z
+ 4761
+ Normal
+ false
+ false
+ false
+ false
+ false
+ 2014-09-05T15:22:06Z
+ 2014-09-05T15:42:54Z
+ 2014-09-09T14:30:00Z
+ true
+ 15
+
+ display1; display2
+ false
+ en-US
+ 2050-05-01T14:30:00Z
+ 2050-05-01T14:45:00Z
+ false
+ Busy
+ Location2
+ true
+ false
+ true
+ false
+ true
+ Occurrence
+ Accept
+
+
+ Organizer 2
+
+
+ PT15M
+ (UTC-05:00) Eastern Time (US & Canada)
+ 2014-09-05T15:42:54Z
+ 0
+ 3
+
+
+
+
+ IPM.Appointment
+ Subject 3
+ Normal
+ 2014-09-30T15:26:27Z
+ 4912
+ Normal
+ false
+ false
+ false
+ false
+ false
+ 2014-09-30T15:26:27Z
+ 2014-09-30T15:37:12Z
+ 2014-10-01T17:00:00Z
+ false
+ 15
+
+ display1; display2; display3
+ false
+ en-US
+ 2050-05-11T17:00:00Z
+ 2050-05-11T18:00:00Z
+ false
+ Busy
+ location 3
+ true
+ false
+ false
+ false
+ true
+ Single
+ Accept
+
+
+ Organizer 3
+
+
+ PT1H
+ UTC
+ 2014-09-30T15:37:11Z
+ 0
+ 3
+
+
+
+
+
+
+
+"""
diff --git a/tests/exchange2010/test_create_event.py b/tests/exchange2010/test_create_event.py
index 01e92aa..be7ca61 100644
--- a/tests/exchange2010/test_create_event.py
+++ b/tests/exchange2010/test_create_event.py
@@ -4,28 +4,33 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import pickle
+import unittest
from httpretty import HTTPretty, httprettified
-from nose.tools import eq_, raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
from pyexchange.base.calendar import ExchangeEventAttendee
-from pyexchange.exceptions import *
+from pyexchange.exceptions import * # noqa
-from .fixtures import *
-from .. import wip
+from .fixtures import * # noqa
-class Test_PopulatingANewEvent():
+
+class Test_PopulatingANewEvent(unittest.TestCase):
""" Tests all the attribute setting works when creating a new event """
calendar = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
- cls.calendar = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL,
- username=FAKE_EXCHANGE_USERNAME,
- password=FAKE_EXCHANGE_PASSWORD)
- ).calendar()
+ cls.calendar = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD,
+ )
+ ).calendar()
def test_canary(self):
event = self.calendar.event()
@@ -33,131 +38,162 @@ def test_canary(self):
def test_events_created_dont_have_an_id(self):
event = self.calendar.event()
- eq_(event.id, None)
+ assert event.id is None
def test_can_add_a_subject(self):
event = self.calendar.event(subject=TEST_EVENT.subject)
- eq_(event.subject, TEST_EVENT.subject)
+ assert event.subject == TEST_EVENT.subject
def test_can_add_a_location(self):
event = self.calendar.event(location=TEST_EVENT.location)
- eq_(event.location, TEST_EVENT.location)
+ assert event.location == TEST_EVENT.location
def test_can_add_an_html_body(self):
event = self.calendar.event(html_body=TEST_EVENT.body)
- eq_(event.html_body, TEST_EVENT.body)
- eq_(event.text_body, None)
- eq_(event.body, TEST_EVENT.body)
+ assert event.html_body == TEST_EVENT.body
+ assert event.text_body is None
+ assert event.body == TEST_EVENT.body
def test_can_add_a_text_body(self):
event = self.calendar.event(text_body=TEST_EVENT.body)
- eq_(event.text_body, TEST_EVENT.body)
- eq_(event.html_body, None)
- eq_(event.body, TEST_EVENT.body)
+ assert event.text_body == TEST_EVENT.body
+ assert event.html_body is None
+ assert event.body == TEST_EVENT.body
def test_can_add_a_start_time(self):
event = self.calendar.event(start=TEST_EVENT.start)
- eq_(event.start, TEST_EVENT.start)
+ assert event.start == TEST_EVENT.start
def test_can_add_an_end_time(self):
event = self.calendar.event(end=TEST_EVENT.end)
- eq_(event.end, TEST_EVENT.end)
+ assert event.end == TEST_EVENT.end
def test_can_add_attendees_via_email(self):
event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
- eq_(len(event.attendees), 1)
- eq_(len(event.required_attendees), 1)
- eq_(len(event.optional_attendees), 0)
- eq_(event.attendees[0].email, PERSON_REQUIRED_ACCEPTED.email)
+ assert len(event.attendees) == 1
+ assert len(event.required_attendees) == 1
+ assert len(event.optional_attendees) == 0
+ assert event.attendees[0].email == PERSON_REQUIRED_ACCEPTED.email
def test_can_add_multiple_attendees_via_email(self):
event = self.calendar.event(attendees=[PERSON_REQUIRED_ACCEPTED.email, PERSON_REQUIRED_TENTATIVE.email])
- eq_(len(event.attendees), 2)
- eq_(len(event.required_attendees), 2)
- eq_(len(event.optional_attendees), 0)
+ assert len(event.attendees) == 2
+ assert len(event.required_attendees) == 2
+ assert len(event.optional_attendees) == 0
def test_can_add_attendees_via_named_tuple(self):
person = ExchangeEventAttendee(name=PERSON_OPTIONAL_ACCEPTED.name, email=PERSON_OPTIONAL_ACCEPTED.email, required=PERSON_OPTIONAL_ACCEPTED.required)
event = self.calendar.event(attendees=person)
- eq_(len(event.attendees), 1)
- eq_(len(event.required_attendees), 0)
- eq_(len(event.optional_attendees), 1)
- eq_(event.attendees[0].email, PERSON_OPTIONAL_ACCEPTED.email)
+ assert len(event.attendees) == 1
+ assert len(event.required_attendees) == 0
+ assert len(event.optional_attendees) == 1
+ assert event.attendees[0].email == PERSON_OPTIONAL_ACCEPTED.email
def test_can_assign_to_required_attendees(self):
event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
event.required_attendees = [PERSON_REQUIRED_ACCEPTED.email, PERSON_OPTIONAL_ACCEPTED.email]
- eq_(len(event.attendees), 2)
- eq_(len(event.required_attendees), 2)
- eq_(len(event.optional_attendees), 0)
+ assert len(event.attendees) == 2
+ assert len(event.required_attendees) == 2
+ assert len(event.optional_attendees) == 0
def test_can_assign_to_optional_attendees(self):
event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
event.optional_attendees = PERSON_OPTIONAL_ACCEPTED.email
- eq_(len(event.attendees), 2)
- eq_(len(event.required_attendees), 1)
- eq_(len(event.optional_attendees), 1)
- eq_(event.required_attendees[0].email, PERSON_REQUIRED_ACCEPTED.email)
- eq_(event.optional_attendees[0].email, PERSON_OPTIONAL_ACCEPTED.email)
-
+ assert len(event.attendees) == 2
+ assert len(event.required_attendees) == 1
+ assert len(event.optional_attendees) == 1
+ assert event.required_attendees[0].email == PERSON_REQUIRED_ACCEPTED.email
+ assert event.optional_attendees[0].email == PERSON_OPTIONAL_ACCEPTED.email
def test_can_add_resources(self):
event = self.calendar.event(resources=[RESOURCE.email])
- eq_(len(event.resources), 1)
- eq_(event.resources[0].email, RESOURCE.email)
- eq_(event.conference_room.email, RESOURCE.email)
+ assert len(event.resources) == 1
+ assert event.resources[0].email == RESOURCE.email
+ assert event.conference_room.email == RESOURCE.email
-class Test_CreatingANewEvent(object):
+class Test_CreatingANewEvent(unittest.TestCase):
service = None
event = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
def setUp(self):
self.event = self.service.calendar().event(start=TEST_EVENT.start, end=TEST_EVENT.end)
- @raises(ValueError)
def test_events_must_have_a_start_date(self):
self.event.start = None
- self.event.create()
- @raises(ValueError)
+ with raises(ValueError):
+ self.event.create()
+
def test_events_must_have_an_end_date(self):
self.event.end = None
- self.event.create()
- @raises(ValueError)
+ with raises(ValueError):
+ self.event.create()
+
def test_event_end_date_must_come_after_start_date(self):
self.event.start, self.event.end = self.event.end, self.event.start
- self.event.create()
- @raises(ValueError)
+ with raises(ValueError):
+ self.event.create()
+
+ def test_attendees_must_have_an_email_address_take1(self):
+
+ with raises(ValueError):
+ self.event.add_attendees(ExchangeEventAttendee(name="Bomb", email=None, required=True))
+ self.event.create()
+
+ def test_attendees_must_have_an_email_address_take2(self):
+
+ with raises(ValueError):
+ self.event.add_attendees([None])
+ self.event.create()
+
+ def test_event_reminder_must_be_int(self):
+ self.event.reminder_minutes_before_start = "not an integer"
+
+ with raises(TypeError):
+ self.event.create()
+
+ def test_event_all_day_must_be_bool(self):
+ self.event.is_all_day = "not a bool"
+
+ with raises(TypeError):
+ self.event.create()
+
def cant_delete_a_newly_created_event(self):
- self.event.delete()
- @raises(ValueError)
+ with raises(ValueError):
+ self.event.delete()
+
def cant_update_a_newly_created_event(self):
- self.event.update()
- @raises(ValueError)
+ with raises(ValueError):
+ self.event.update()
+
def cant_resend_invites_for_a_newly_created_event(self):
- self.event.resend_invitations()
+
+ with raises(ValueError):
+ self.event.resend_invitations()
@httprettified
def test_can_set_subject(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.subject = TEST_EVENT.subject
self.event.create()
@@ -167,9 +203,11 @@ def test_can_set_subject(self):
@httprettified
def test_can_set_location(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.location = TEST_EVENT.location
self.event.create()
@@ -179,9 +217,11 @@ def test_can_set_location(self):
@httprettified
def test_can_set_html_body(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8'
+ )
self.event.html_body = TEST_EVENT.body
self.event.create()
@@ -191,9 +231,11 @@ def test_can_set_html_body(self):
@httprettified
def test_can_set_text_body(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.text_body = TEST_EVENT.body
self.event.create()
@@ -203,9 +245,11 @@ def test_can_set_text_body(self):
@httprettified
def test_start_time(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.create()
@@ -214,9 +258,11 @@ def test_start_time(self):
@httprettified
def test_end_time(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.create()
@@ -225,9 +271,11 @@ def test_end_time(self):
@httprettified
def test_attendees(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
attendees = [PERSON_REQUIRED_ACCEPTED.email, PERSON_REQUIRED_TENTATIVE.email]
@@ -237,25 +285,28 @@ def test_attendees(self):
for email in attendees:
assert email in HTTPretty.last_request.body.decode('utf-8')
- @raises(ValueError)
def test_resources_must_have_an_email_address(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
attendees = [PERSON_WITH_NO_EMAIL_ADDRESS]
- self.event.attendees = attendees
- self.event.create()
+ with raises(ValueError):
+ self.event.attendees = attendees
+ self.event.create()
@httprettified
def test_resources(self):
- HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
- body=CREATE_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
-
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
self.event.resources = [RESOURCE.email]
self.event.create()
@@ -263,3 +314,13 @@ def test_resources(self):
assert RESOURCE.email in HTTPretty.last_request.body.decode('utf-8')
+ def test_events_can_be_pickled(self):
+
+ self.event.subject = "events can be pickled"
+
+ pickled_event = pickle.dumps(self.event)
+ new_event = pickle.loads(pickled_event)
+
+ assert new_event.subject == "events can be pickled"
+
+
diff --git a/tests/exchange2010/test_create_folder.py b/tests/exchange2010/test_create_folder.py
index 006eb49..ca3cb3d 100644
--- a/tests/exchange2010/test_create_folder.py
+++ b/tests/exchange2010/test_create_folder.py
@@ -4,8 +4,9 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import unittest
from httpretty import HTTPretty, httprettified
-from nose.tools import eq_, raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
@@ -14,12 +15,12 @@
from .fixtures import *
-class Test_PopulatingANewFolder():
+class Test_PopulatingANewFolder(unittest.TestCase):
""" Tests all the attribute setting works when creating a new folder """
folder = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.folder = Exchange2010Service(
connection=ExchangeNTLMAuthConnection(
@@ -35,27 +36,27 @@ def test_canary(self):
def test_folders_created_dont_have_an_id(self):
folder = self.folder.new_folder()
- eq_(folder.id, None)
+ assert folder.id is None
def test_folder_has_display_name(self):
folder = self.folder.new_folder(display_name=u'Conference Room')
- eq_(folder.display_name, u'Conference Room')
+ assert folder.display_name == u'Conference Room'
def test_folder_has_default_folder_type(self):
folder = self.folder.new_folder()
- eq_(folder.folder_type, u'Folder')
+ assert folder.folder_type == u'Folder'
def test_folder_has_calendar_folder_type(self):
folder = self.folder.new_folder(folder_type=u'CalendarFolder')
- eq_(folder.folder_type, u'CalendarFolder')
+ assert folder.folder_type == u'CalendarFolder'
-class Test_CreatingANewFolder(object):
+class Test_CreatingANewFolder(unittest.TestCase):
service = None
folder = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(
connection=ExchangeNTLMAuthConnection(
url=FAKE_EXCHANGE_URL,
@@ -67,20 +68,23 @@ def setUpAll(cls):
def setUp(self):
self.folder = self.service.folder().new_folder()
- @raises(AttributeError)
def test_folders_must_have_a_display_name(self):
self.parent_id = u'AQASAGFyMTY2AUB0eHN0YXRlLmVkdQAuAAADXToP9jZJ50ix6mBloAoUtQEAIXy9HV1hQUKHHMQm+PlY6QINNPfbUQAAAA=='
- self.folder.create()
- @raises(ValueError)
+ with raises(AttributeError):
+ self.folder.create()
+
+
def test_folders_must_have_a_parent_id(self):
self.folder.display_name = u'Conference Room'
self.parent_id = None
- self.folder.create()
- @raises(TypeError)
+ with raises(ValueError):
+ self.folder.create()
+
def cant_delete_an_uncreated_folder(self):
- self.folder.delete()
+ with raises(TypeError):
+ self.folder.delete()
@httprettified
def test_can_set_display_name(self):
@@ -146,4 +150,4 @@ def test_can_create(self):
self.folder.folder_type = TEST_FOLDER.folder_type
self.folder.create()
- eq_(self.folder.id, TEST_FOLDER.id)
+ assert self.folder.id == TEST_FOLDER.id
diff --git a/tests/exchange2010/test_create_recurring_event.py b/tests/exchange2010/test_create_recurring_event.py
new file mode 100644
index 0000000..50776b2
--- /dev/null
+++ b/tests/exchange2010/test_create_recurring_event.py
@@ -0,0 +1,414 @@
+"""
+(c) 2013 LinkedIn Corp. All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");?you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+"""
+import unittest
+from pytest import raises
+from httpretty import HTTPretty, httprettified
+from pyexchange import Exchange2010Service
+
+from pyexchange.connection import ExchangeNTLMAuthConnection
+from pyexchange.exceptions import * # noqa
+
+from .fixtures import * # noqa
+
+
+class Test_PopulatingANewRecurringDailyEvent(unittest.TestCase):
+ """ Tests all the attribute setting works when creating a new event """
+ calendar = None
+
+ @classmethod
+ def setUpClass(cls):
+
+ cls.calendar = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD,
+ )
+ ).calendar()
+
+ def test_can_set_recurring(self):
+ event = self.calendar.event(
+ recurrence_interval=TEST_RECURRING_EVENT_DAILY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_DAILY.recurrence_end_date,
+ )
+ assert event.recurrence_interval == TEST_RECURRING_EVENT_DAILY.recurrence_interval
+ assert event.recurrence_end_date == TEST_RECURRING_EVENT_DAILY.recurrence_end_date
+
+
+class Test_CreatingANewRecurringDailyEvent(unittest.TestCase):
+ service = None
+ event = None
+
+ @classmethod
+ def setUpClass(cls):
+ cls.service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
+
+ def setUp(self):
+ self.event = self.service.calendar().event(
+ subject=TEST_RECURRING_EVENT_DAILY.subject,
+ start=TEST_RECURRING_EVENT_DAILY.start,
+ end=TEST_RECURRING_EVENT_DAILY.end,
+ recurrence='daily',
+ recurrence_interval=TEST_RECURRING_EVENT_DAILY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_DAILY.recurrence_end_date,
+ )
+
+ def test_recurrence_must_have_interval(self):
+ self.event.recurrence_interval = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_low_value(self):
+ self.event.recurrence_interval = 0
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_high_value(self):
+ self.event.recurrence_interval = 1000
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_recurrence_interval_min_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 1
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
+
+ @httprettified
+ def test_recurrence_interval_max_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 999
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
+
+ def test_recurrence_must_have_end_date(self):
+ self.event.recurrence_end_date = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_end_before_start(self):
+ self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_create_recurrence_daily(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
+
+
+class Test_PopulatingANewRecurringWeeklyEvent(unittest.TestCase):
+ """ Tests all the attribute setting works when creating a new event """
+ calendar = None
+
+ @classmethod
+ def setUpClass(cls):
+
+ cls.calendar = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD,
+ )
+ ).calendar()
+
+ def test_can_set_recurring(self):
+ event = self.calendar.event(
+ recurrence_interval=TEST_RECURRING_EVENT_WEEKLY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date,
+ recurrence_days=TEST_RECURRING_EVENT_WEEKLY.recurrence_days,
+ )
+ assert event.recurrence_interval == TEST_RECURRING_EVENT_WEEKLY.recurrence_interval
+ assert event.recurrence_end_date == TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date
+ assert event.recurrence_days == TEST_RECURRING_EVENT_WEEKLY.recurrence_days
+
+
+class Test_CreatingANewRecurringWeeklyEvent(unittest.TestCase):
+ service = None
+ event = None
+
+ @classmethod
+ def setUpClass(cls):
+ cls.service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
+
+ def setUp(self):
+ self.event = self.service.calendar().event(
+ subject=TEST_RECURRING_EVENT_WEEKLY.subject,
+ start=TEST_RECURRING_EVENT_WEEKLY.start,
+ end=TEST_RECURRING_EVENT_WEEKLY.end,
+ recurrence='weekly',
+ recurrence_interval=TEST_RECURRING_EVENT_WEEKLY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date,
+ recurrence_days=TEST_RECURRING_EVENT_WEEKLY.recurrence_days,
+ )
+
+ def test_recurrence_must_have_interval(self):
+ self.event.recurrence_interval = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_low_value(self):
+ self.event.recurrence_interval = 0
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_high_value(self):
+ self.event.recurrence_interval = 100
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_recurrence_interval_min_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 1
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
+
+ @httprettified
+ def test_recurrence_interval_max_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 99
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
+
+ def test_recurrence_must_have_end_date(self):
+ self.event.recurrence_end_date = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_end_before_start(self):
+ self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_bad_days(self):
+ self.event.recurrence_days = 'Mondays'
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_no_days(self):
+ self.event.recurrence_days = None
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_create_recurrence_weekly(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
+
+
+class Test_PopulatingANewRecurringMonthlyEvent(unittest.TestCase):
+ """ Tests all the attribute setting works when creating a new event """
+ calendar = None
+
+ @classmethod
+ def setUpClass(cls):
+
+ cls.calendar = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD,
+ )
+ ).calendar()
+
+ def test_can_set_recurring(self):
+ event = self.calendar.event(
+ recurrence='monthly',
+ recurrence_interval=TEST_RECURRING_EVENT_MONTHLY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date,
+ )
+ assert event.recurrence_interval == TEST_RECURRING_EVENT_MONTHLY.recurrence_interval
+ assert event.recurrence_end_date == TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date
+
+
+class Test_CreatingANewRecurringMonthlyEvent(unittest.TestCase):
+ service = None
+ event = None
+
+ @classmethod
+ def setUpClass(cls):
+ cls.service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
+
+ def setUp(self):
+ self.event = self.service.calendar().event(
+ subject=TEST_RECURRING_EVENT_MONTHLY.subject,
+ start=TEST_RECURRING_EVENT_MONTHLY.start,
+ end=TEST_RECURRING_EVENT_MONTHLY.end,
+ recurrence='monthly',
+ recurrence_interval=TEST_RECURRING_EVENT_MONTHLY.recurrence_interval,
+ recurrence_end_date=TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date,
+ )
+
+ def test_recurrence_must_have_interval(self):
+ self.event.recurrence_interval = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_low_value(self):
+ self.event.recurrence_interval = 0
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_interval_high_value(self):
+ self.event.recurrence_interval = 100
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_recurrence_interval_min_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 1
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
+
+ @httprettified
+ def test_recurrence_interval_max_value(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.recurrence_interval = 99
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
+
+ def test_recurrence_must_have_end_date(self):
+ self.event.recurrence_end_date = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_end_before_start(self):
+ self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_create_recurrence_monthly(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
+
+
+class Test_PopulatingANewRecurringYearlyEvent(unittest.TestCase):
+ """ Tests all the attribute setting works when creating a new event """
+ calendar = None
+
+ @classmethod
+ def setUpClass(cls):
+
+ cls.calendar = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD,
+ )
+ ).calendar()
+
+ def test_can_set_recurring(self):
+ event = self.calendar.event(
+ recurrence='yearly',
+ recurrence_end_date=TEST_RECURRING_EVENT_YEARLY.recurrence_end_date,
+ )
+ event.recurrence_end_date == TEST_RECURRING_EVENT_YEARLY.recurrence_end_date
+
+
+class Test_CreatingANewRecurringYearlyEvent(unittest.TestCase):
+ service = None
+ event = None
+
+ @classmethod
+ def setUpClass(cls):
+ cls.service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL,
+ username=FAKE_EXCHANGE_USERNAME,
+ password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
+
+ def setUp(self):
+ self.event = self.service.calendar().event(
+ subject=TEST_RECURRING_EVENT_YEARLY.subject,
+ start=TEST_RECURRING_EVENT_YEARLY.start,
+ end=TEST_RECURRING_EVENT_YEARLY.end,
+ recurrence='yearly',
+ recurrence_end_date=TEST_RECURRING_EVENT_YEARLY.recurrence_end_date,
+ )
+
+ def test_recurrence_must_have_end_date(self):
+ self.event.recurrence_end_date = None
+ with raises(ValueError):
+ self.event.create()
+
+ def test_recurrence_end_before_start(self):
+ self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
+ with raises(ValueError):
+ self.event.create()
+
+ @httprettified
+ def test_create_recurrence_yearly(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CREATE_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ self.event.create()
+ assert self.event.id == TEST_RECURRING_EVENT_YEARLY.id
diff --git a/tests/exchange2010/test_delete_event.py b/tests/exchange2010/test_delete_event.py
index 558f67b..16cebff 100644
--- a/tests/exchange2010/test_delete_event.py
+++ b/tests/exchange2010/test_delete_event.py
@@ -4,20 +4,19 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import unittest
import httpretty
-from nose.tools import eq_, raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
from .fixtures import *
-from .. import wip
-
-class Test_EventDeletion(object):
+class Test_EventDeletion(unittest.TestCase):
event = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
@@ -43,10 +42,9 @@ def test_can_cancel_event(self):
])
response = self.event.cancel()
- eq_(response, None)
+ assert response is None
- @raises(TypeError)
@httpretty.activate
def test_cant_cancel_an_event_with_no_exchange_id(self):
httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
@@ -55,5 +53,7 @@ def test_cant_cancel_an_event_with_no_exchange_id(self):
self.delete_event_response,
])
unsaved_event = self.service.calendar().event()
- unsaved_event.cancel() #bzzt - can't do this
+
+ with raises(TypeError):
+ unsaved_event.cancel() #bzzt - can't do this
diff --git a/tests/exchange2010/test_delete_folder.py b/tests/exchange2010/test_delete_folder.py
index 28456c8..409f726 100644
--- a/tests/exchange2010/test_delete_folder.py
+++ b/tests/exchange2010/test_delete_folder.py
@@ -4,19 +4,20 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import unittest
import httpretty
-from nose.tools import eq_, raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
from .fixtures import *
-class Test_FolderDeletion(object):
+class Test_FolderDeletion(unittest.TestCase):
folder = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(
connection=ExchangeNTLMAuthConnection(
url=FAKE_EXCHANGE_URL,
@@ -60,9 +61,8 @@ def test_can_delete_folder(self):
)
response = self.folder.delete()
- eq_(response, None)
+ assert response is None
- @raises(TypeError)
@httpretty.activate
def test_cant_delete_a_uncreated_folder(self):
httpretty.register_uri(
@@ -74,4 +74,6 @@ def test_cant_delete_a_uncreated_folder(self):
]
)
unsaved_folder = self.service.folder().new_folder()
- unsaved_folder.delete() # bzzt - can't do this
+
+ with raises(TypeError):
+ unsaved_folder.delete() # bzzt - can't do this
diff --git a/tests/exchange2010/test_event_actions.py b/tests/exchange2010/test_event_actions.py
index 97740a9..ed85970 100644
--- a/tests/exchange2010/test_event_actions.py
+++ b/tests/exchange2010/test_event_actions.py
@@ -4,20 +4,21 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import unittest
from httpretty import HTTPretty, httprettified
-from nose.tools import raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
from pyexchange.exceptions import *
from .fixtures import *
-from .. import wip
-class Test_EventActions(object):
+
+class Test_EventActions(unittest.TestCase):
event = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
cls.get_change_key_response = HTTPretty.Response(body=GET_ITEM_RESPONSE_ID_ONLY.encode('utf-8'), status=200, content_type='text/xml; charset=utf-8')
cls.update_event_response = HTTPretty.Response(body=UPDATE_ITEM_RESPONSE.encode('utf-8'), status=200, content_type='text/xml; charset=utf-8')
@@ -44,8 +45,6 @@ def test_resend_invites(self):
assert TEST_EVENT.change_key in HTTPretty.last_request.body.decode('utf-8')
assert TEST_EVENT.subject not in HTTPretty.last_request.body.decode('utf-8')
-
- @raises(ValueError)
@httprettified
def test_cant_resend_invites_on_a_modified_event(self):
HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
@@ -55,4 +54,6 @@ def test_cant_resend_invites_on_a_modified_event(self):
])
self.event.subject = u'New event thing'
- self.event.resend_invitations()
+
+ with raises(ValueError):
+ self.event.resend_invitations()
diff --git a/tests/exchange2010/test_exchange_service.py b/tests/exchange2010/test_exchange_service.py
new file mode 100644
index 0000000..c89f097
--- /dev/null
+++ b/tests/exchange2010/test_exchange_service.py
@@ -0,0 +1 @@
+__author__ = 'rsanders'
diff --git a/tests/exchange2010/test_find_folder.py b/tests/exchange2010/test_find_folder.py
index d552960..02ff712 100644
--- a/tests/exchange2010/test_find_folder.py
+++ b/tests/exchange2010/test_find_folder.py
@@ -4,8 +4,9 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
+import unittest
import httpretty
-from nose.tools import eq_, raises
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
from pyexchange.exceptions import *
@@ -13,11 +14,11 @@
from .fixtures import *
-class Test_ParseFolderResponseData(object):
+class Test_ParseFolderResponseData(unittest.TestCase):
folder = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
@httpretty.activate # this decorator doesn't play nice with @classmethod
def fake_folder_request():
@@ -51,19 +52,19 @@ def test_folder_has_a_name(self):
def test_folder_has_a_parent(self):
for folder in self.folder:
- eq_(folder.parent_id, TEST_FOLDER.id)
+ assert folder.parent_id == TEST_FOLDER.id
def test_folder_type(self):
for folder in self.folder:
assert folder is not None
-class Test_FailingToGetFolders():
+class Test_FailingToGetFolders(unittest.TestCase):
service = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
cls.service = Exchange2010Service(
connection=ExchangeNTLMAuthConnection(
@@ -73,7 +74,6 @@ def setUpAll(cls):
)
)
- @raises(ExchangeItemNotFoundException)
@httpretty.activate
def test_requesting_an_folder_id_that_doest_exist_throws_exception(self):
@@ -83,9 +83,9 @@ def test_requesting_an_folder_id_that_doest_exist_throws_exception(self):
content_type='text/xml; charset=utf-8',
)
- self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
+ with raises(ExchangeItemNotFoundException):
+ self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
- @raises(FailedExchangeException)
@httpretty.activate
def test_requesting_an_folder_and_getting_a_500_response_throws_exception(self):
@@ -97,4 +97,5 @@ def test_requesting_an_folder_and_getting_a_500_response_throws_exception(self):
content_type='text/xml; charset=utf-8',
)
- self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
+ with raises(FailedExchangeException):
+ self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
diff --git a/tests/exchange2010/test_get_event.py b/tests/exchange2010/test_get_event.py
index 6aff7f2..3078bef 100644
--- a/tests/exchange2010/test_get_event.py
+++ b/tests/exchange2010/test_get_event.py
@@ -4,65 +4,72 @@
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
-import httpretty
-from nose.tools import eq_, raises
+from httpretty import HTTPretty, httprettified, activate
+import unittest
+from pytest import raises
from pyexchange import Exchange2010Service
from pyexchange.connection import ExchangeNTLMAuthConnection
-from pyexchange.exceptions import *
+from pyexchange.exceptions import * # noqa
-from .fixtures import *
+from .fixtures import * # noqa
-class Test_ParseEventResponseData(object):
+
+class Test_ParseEventResponseData(unittest.TestCase):
event = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
- @httpretty.activate # this decorator doesn't play nice with @classmethod
+ @activate # this decorator doesn't play nice with @classmethod
def fake_event_request():
- service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
+ service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
- httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
- body=GET_ITEM_RESPONSE.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=GET_ITEM_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
return service.calendar().get_event(id=TEST_EVENT.id)
cls.event = fake_event_request()
-
def test_canary(self):
assert self.event is not None
def test_event_id_was_not_changed(self):
- eq_(self.event.id, TEST_EVENT.id)
+ assert self.event.id == TEST_EVENT.id
def test_event_has_a_subject(self):
- eq_(self.event.subject, TEST_EVENT.subject)
+ assert self.event.subject == TEST_EVENT.subject
def test_event_has_a_location(self):
- eq_(self.event.location, TEST_EVENT.location)
+ assert self.event.location == TEST_EVENT.location
def test_event_has_a_body(self):
- eq_(self.event.html_body, TEST_EVENT.body)
- eq_(self.event.text_body, TEST_EVENT.body)
- eq_(self.event.body, TEST_EVENT.body)
+ assert self.event.html_body == TEST_EVENT.body
+ assert self.event.text_body == TEST_EVENT.body
+ assert self.event.body == TEST_EVENT.body
def test_event_starts_at_the_right_time(self):
- eq_(self.event.start, TEST_EVENT.start)
+ assert self.event.start == TEST_EVENT.start
def test_event_ends_at_the_right_time(self):
- eq_(self.event.end, TEST_EVENT.end)
+ assert self.event.end == TEST_EVENT.end
def test_event_has_an_organizer(self):
assert self.event.organizer is not None
- eq_(self.event.organizer.name, ORGANIZER.name)
- eq_(self.event.organizer.email, ORGANIZER.email)
+ assert self.event.organizer.name == ORGANIZER.name
+ assert self.event.organizer.email == ORGANIZER.email
def test_event_has_the_correct_attendees(self):
assert len(self.event.attendees) > 0
- eq_(len(self.event.attendees), len(ATTENDEE_LIST))
+ assert len(self.event.attendees) == len(ATTENDEE_LIST)
def _test_person_values_are_correct(self, fixture):
@@ -80,52 +87,371 @@ def test_all_attendees_are_present_and_accounted_for(self):
yield self._test_person_values_are_correct, attendee
def test_resources_are_correct(self):
- eq_(self.event.resources, [RESOURCE])
+ assert self.event.resources == [RESOURCE]
def test_conference_room_alias(self):
- eq_(self.event.conference_room, RESOURCE)
+ assert self.event.conference_room == RESOURCE
def test_required_attendees_are_required(self):
- eq_(sorted(self.event.required_attendees), sorted(REQUIRED_PEOPLE))
+ assert sorted(self.event.required_attendees) == sorted(REQUIRED_PEOPLE)
def test_optional_attendees_are_optional(self):
- eq_(sorted(self.event.optional_attendees), sorted(OPTIONAL_PEOPLE))
-
-
-class Test_FailingToGetEvents():
+ assert sorted(self.event.optional_attendees) == sorted(OPTIONAL_PEOPLE)
+
+ def test_conflicting_event_ids(self):
+ assert self.event.conflicting_event_ids[0] == TEST_CONFLICT_EVENT.id
+
+ @httprettified
+ def test_conflicting_events(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=CONFLICTING_EVENTS_RESPONSE.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ conflicting_events = self.event.conflicting_events()
+ assert conflicting_events[0].id == TEST_CONFLICT_EVENT.id
+ assert conflicting_events[0].calendar_id == TEST_CONFLICT_EVENT.calendar_id
+ assert conflicting_events[0].subject == TEST_CONFLICT_EVENT.subject
+ assert conflicting_events[0].location == TEST_CONFLICT_EVENT.location
+ assert conflicting_events[0].start == TEST_CONFLICT_EVENT.start
+ assert conflicting_events[0].end == TEST_CONFLICT_EVENT.end
+ assert conflicting_events[0].body == TEST_CONFLICT_EVENT.body
+ assert conflicting_events[0].conflicting_event_ids[0] == TEST_EVENT.id
+
+
+class Test_FailingToGetEvents(unittest.TestCase):
service = None
@classmethod
- def setUpAll(cls):
+ def setUpClass(cls):
- cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL,
- username=FAKE_EXCHANGE_USERNAME,
- password=FAKE_EXCHANGE_PASSWORD))
+ cls.service = Exchange2010Service(
+ connection=ExchangeNTLMAuthConnection(
+ url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD
+ )
+ )
- @raises(ExchangeItemNotFoundException)
- @httpretty.activate
+ @activate
def test_requesting_an_event_id_that_doest_exist_throws_exception(self):
- httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
- body=ITEM_DOES_NOT_EXIST.encode('utf-8'),
- content_type='text/xml; charset=utf-8')
-
- self.service.calendar().get_event(id=TEST_EVENT.id)
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=ITEM_DOES_NOT_EXIST.encode('utf-8'),
+ content_type='text/xml; charset=utf-8',
+ )
+ with raises(ExchangeItemNotFoundException):
+ self.service.calendar().get_event(id=TEST_EVENT.id)
- @raises(FailedExchangeException)
- @httpretty.activate
+ @activate
def test_requesting_an_event_and_getting_a_500_response_throws_exception(self):
- httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
- body=u"",
- status=500,
- content_type='text/xml; charset=utf-8')
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=u"",
+ status=500,
+ content_type='text/xml; charset=utf-8',
+ )
- self.service.calendar().get_event(id=TEST_EVENT.id)
+ with raises(FailedExchangeException):
+ self.service.calendar().get_event(id=TEST_EVENT.id)
+ @activate
+ def test_requesting_an_event_and_getting_garbage_xml_throws_exception(self):
+ HTTPretty.register_uri(
+ HTTPretty.POST, FAKE_EXCHANGE_URL,
+ body=u"