`_.
61 |
62 | Setting up the connection
63 | `````````````````````````
64 |
65 | To do anything in Exchange, you first need to create the Exchange service object::
66 |
67 | from pyexchange import Exchange2010Service, ExchangeNTLMAuthConnection
68 |
69 | URL = u'https://your.email.server.com.here/EWS/Exchange.asmx'
70 | USERNAME = u'YOURDOMAIN\\yourusername'
71 | PASSWORD = u"12345? That's what I have on my luggage!"
72 |
73 | # Set up the connection to Exchange
74 | connection = ExchangeNTLMAuthConnection(url=URL,
75 | username=USERNAME,
76 | password=PASSWORD)
77 |
78 | service = Exchange2010Service(connection)
79 |
80 | Creating an event
81 | `````````````````
82 | To create an event, use the ``new_event`` method::
83 |
84 | from datetime import datetime
85 | from pytz import timezone
86 |
87 | # You can set event properties when you instantiate the event...
88 | event = service.calendar().new_event(
89 | subject=u"80s Movie Night",
90 | attendees=[u'your_friend@friendme.domain', u'your_other_friend@their.domain'],
91 | location = u"My house",
92 | )
93 |
94 | # ...or afterwards
95 | event.start=timezone("US/Pacific").localize(datetime(2013,1,1,15,0,0))
96 | event.end=timezone("US/Pacific").localize(datetime(2013,1,1,21,0,0))
97 |
98 | event.html_body = u"""
99 |
100 | 80s Movie night
101 | We're watching Spaceballs, Wayne's World, and
102 | Bill and Ted's Excellent Adventure.
103 | PARTY ON DUDES!
104 |
105 | """
106 |
107 | # Connect to Exchange and create the event
108 | event.create()
109 |
110 | For a full list of fields, see the :class:`.Exchange2010CalendarEvent` documentation.
111 |
112 | When you create an event, Exchange creates a unique identifier for it. You need this to get the event later.
113 |
114 | After you create the object, the ``id`` attribute is populated with this identifier::
115 |
116 | print event.id # prints None
117 |
118 | # Create the event
119 | event.create()
120 |
121 | print event.id # prints Exchange key
122 |
123 | If you save this key, be warned they're quite long - easily 130+ characters.
124 |
125 | If we could not create the event, a ``pyexchange.exceptions.FailedExchangeException`` exception is thrown.
126 |
127 | Getting an event
128 | ````````````````
129 |
130 | To work with any existing event, you must know its unique identifier in Exchange. For more information, see the `MSDN Exchange Web Services documentation `_.
131 |
132 | Once you have the id, get the event using the ``get_event`` method::
133 |
134 | EXCHANGE_ID = u'3123132131231231'
135 |
136 | event = service.calendar().get_event(id=EXCHANGE_ID)
137 |
138 | print event.id # the same as EXCHANGE_ID
139 | print event.subject
140 | print event.location
141 |
142 | print event.start # datetime object
143 | print event.end # datetime object
144 |
145 | print event.body
146 |
147 | for person in event.attendees:
148 | print person.name
149 | print person.email
150 | print person.response # Accepted/Declined
151 |
152 | For a full list of fields, see the :class:`.Exchange2010CalendarEvent` documentation.
153 |
154 | If the id doesn't match anything in Exchange, a ``pyexchange.exceptions.ExchangeItemNotFoundException`` exception is thrown.
155 |
156 | For all other errors, we throw a ``pyexchange.exceptions.FailedExchangeException``.
157 |
158 | Modifying an event
159 | ``````````````````
160 |
161 | To modify an event, first get the event::
162 |
163 | EXCHANGE_ID = u'3123132131231231'
164 |
165 | event = service.calendar().get_event(id=EXCHANGE_ID)
166 |
167 | Then simply assign to the properties you want to change and use ``update``::
168 |
169 | event.location = u'New location'
170 | event.attendees = [u'thing1@dr.suess', u'thing2@dr.suess']
171 |
172 | event.update()
173 |
174 | If the id doesn't match anything in Exchange, a ``pyexchange.exceptions.ExchangeItemNotFoundException`` exception is thrown.
175 |
176 | For all other errors, we throw a ``pyexchange.exceptions.FailedExchangeException``.
177 |
178 | Listing events
179 | ``````````````
180 |
181 | To list events between two dates, simply do::
182 |
183 | events = my_calendar.list_events(
184 | start=timezone("US/Eastern").localize(datetime(2014, 10, 1, 11, 0, 0)),
185 | end=timezone("US/Eastern").localize(datetime(2014, 10, 29, 11, 0, 0)),
186 | details=True
187 | )
188 |
189 | 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.)::
190 |
191 | for event in calendar_list.events:
192 | print "{start} {stop} - {subject}".format(
193 | start=event.start,
194 | stop=event.end,
195 | subject=event.subject
196 | )
197 |
198 | 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::
199 |
200 | events = my_calendar.list_events(start, end)
201 | events.load_all_details()
202 |
203 | Cancelling an event
204 | ```````````````````
205 |
206 | To cancel an event, simply do::
207 |
208 | event = my_calendar.get_event(id=EXCHANGE_ID)
209 |
210 | event.cancel()
211 |
212 | If the id doesn't match anything in Exchange, a ``pyexchange.exceptions.ExchangeItemNotFoundException`` exception is thrown.
213 |
214 | For all other errors, we throw a ``pyexchange.exceptions.FailedExchangeException``.
215 |
216 | Resending invitations
217 | `````````````````````
218 |
219 | To resend invitations to all participants, do::
220 |
221 | event = my_calendar.get_event(id=EXCHANGE_ID)
222 |
223 | event.resend_invitations()
224 |
225 | Creating a new calendar
226 | ```````````````````````
227 |
228 | To create a new exchange calendar, do::
229 |
230 | calendar = service.folder().new_folder(
231 | display_name="New Name", # This will be the display name for the new calendar. Can be set to whatever you want.
232 | folder_type="CalendarFolder", # This MUST be set to the value "CalendarFolder". It tells exchange what type of folder to create.
233 | parent_id='calendar', # This does not have to be 'calendar' but is recommended. The value 'calendar' will resolve to the base Calendar folder.
234 | )
235 | calendar.create()
236 |
237 | By creating a folder of the type "CalendarFolder", you are creating a new calendar.
238 |
239 | Other tips and tricks
240 | `````````````````````
241 |
242 | You can pickle events if you need to serialize them. (We do this to send invites asynchronously.) ::
243 |
244 | import pickle
245 |
246 | # create event
247 | event = service.calendar().new_event()
248 |
249 | event.subject = u"80s Movie Night"
250 | event.start=timezone("US/Pacific").localize(datetime(2013,1,1,15,0,0))
251 | event.end=timezone("US/Pacific").localize(datetime(2013,1,1,21,0,0))
252 |
253 | # Pickle event
254 | pickled_event = pickle.dumps(event)
255 |
256 | # Unpickle
257 | rehydrated_event = pickle.loads(pickled_event)
258 | print rehydrated_event.subject # "80s Movie Night"
259 |
260 |
261 | Changelog
262 | ---------
263 |
264 | * :doc:`changelog `
265 |
266 | Support
267 | -------
268 |
269 | To report bugs or get support, please use the `Github issue tracker `_.
270 |
271 | Indices and tables
272 | ==================
273 |
274 | * :ref:`genindex`
275 | * :ref:`modindex`
276 | * :ref:`search`
277 |
278 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. linkcheck to check all external links for integrity
37 | echo. doctest to run all doctests embedded in the documentation if enabled
38 | goto end
39 | )
40 |
41 | if "%1" == "clean" (
42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
43 | del /q /s %BUILDDIR%\*
44 | goto end
45 | )
46 |
47 | if "%1" == "html" (
48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
49 | if errorlevel 1 exit /b 1
50 | echo.
51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
52 | goto end
53 | )
54 |
55 | if "%1" == "dirhtml" (
56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
57 | if errorlevel 1 exit /b 1
58 | echo.
59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
60 | goto end
61 | )
62 |
63 | if "%1" == "singlehtml" (
64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
68 | goto end
69 | )
70 |
71 | if "%1" == "pickle" (
72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished; now you can process the pickle files.
76 | goto end
77 | )
78 |
79 | if "%1" == "json" (
80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished; now you can process the JSON files.
84 | goto end
85 | )
86 |
87 | if "%1" == "htmlhelp" (
88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can run HTML Help Workshop with the ^
92 | .hhp project file in %BUILDDIR%/htmlhelp.
93 | goto end
94 | )
95 |
96 | if "%1" == "qthelp" (
97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
98 | if errorlevel 1 exit /b 1
99 | echo.
100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
101 | .qhcp project file in %BUILDDIR%/qthelp, like this:
102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyExchange.qhcp
103 | echo.To view the help file:
104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyExchange.ghc
105 | goto end
106 | )
107 |
108 | if "%1" == "devhelp" (
109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
110 | if errorlevel 1 exit /b 1
111 | echo.
112 | echo.Build finished.
113 | goto end
114 | )
115 |
116 | if "%1" == "epub" (
117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
118 | if errorlevel 1 exit /b 1
119 | echo.
120 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
121 | goto end
122 | )
123 |
124 | if "%1" == "latex" (
125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
129 | goto end
130 | )
131 |
132 | if "%1" == "text" (
133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The text files are in %BUILDDIR%/text.
137 | goto end
138 | )
139 |
140 | if "%1" == "man" (
141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
145 | goto end
146 | )
147 |
148 | if "%1" == "texinfo" (
149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
150 | if errorlevel 1 exit /b 1
151 | echo.
152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
153 | goto end
154 | )
155 |
156 | if "%1" == "gettext" (
157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
158 | if errorlevel 1 exit /b 1
159 | echo.
160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
161 | goto end
162 | )
163 |
164 | if "%1" == "changes" (
165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
166 | if errorlevel 1 exit /b 1
167 | echo.
168 | echo.The overview file is in %BUILDDIR%/changes.
169 | goto end
170 | )
171 |
172 | if "%1" == "linkcheck" (
173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
174 | if errorlevel 1 exit /b 1
175 | echo.
176 | echo.Link check complete; look for any errors in the above output ^
177 | or in %BUILDDIR%/linkcheck/output.txt.
178 | goto end
179 | )
180 |
181 | if "%1" == "doctest" (
182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
183 | if errorlevel 1 exit /b 1
184 | echo.
185 | echo.Testing of doctests in the sources finished, look at the ^
186 | results in %BUILDDIR%/doctest/output.txt.
187 | goto end
188 | )
189 |
190 | :end
191 |
--------------------------------------------------------------------------------
/pyexchange/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import logging
8 | from .exchange2010 import Exchange2010Service # noqa
9 | from .connection import ExchangeNTLMAuthConnection # noqa
10 |
11 | # Silence notification of no default logging handler
12 | log = logging.getLogger("pyexchange")
13 |
14 |
15 | class NullHandler(logging.Handler):
16 | def emit(self, record):
17 | pass
18 |
19 | log.addHandler(NullHandler())
20 |
--------------------------------------------------------------------------------
/pyexchange/base/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | # placeholder
8 |
--------------------------------------------------------------------------------
/pyexchange/base/calendar.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | from collections import namedtuple
8 |
9 | ExchangeEventOrganizer = namedtuple('ExchangeEventOrganizer', ['name', 'email'])
10 | ExchangeEventAttendee = namedtuple('ExchangeEventAttendee', ['name', 'email', 'required'])
11 | ExchangeEventResponse = namedtuple('ExchangeEventResponse', ['name', 'email', 'response', 'last_response', 'required'])
12 |
13 |
14 | RESPONSE_ACCEPTED = u'Accept'
15 | RESPONSE_DECLINED = u'Decline'
16 | RESPONSE_TENTATIVE = u'Tentative'
17 | RESPONSE_UNKNOWN = u'Unknown'
18 |
19 | RESPONSES = [RESPONSE_ACCEPTED, RESPONSE_DECLINED, RESPONSE_TENTATIVE, RESPONSE_UNKNOWN]
20 |
21 |
22 | class BaseExchangeCalendarService(object):
23 |
24 | def __init__(self, service, calendar_id):
25 | self.service = service
26 | self.calendar_id = calendar_id
27 |
28 | def event(self, id, *args, **kwargs):
29 | raise NotImplementedError
30 |
31 | def get_event(self, id):
32 | raise NotImplementedError
33 |
34 | def new_event(self, **properties):
35 | raise NotImplementedError
36 |
37 |
38 | class BaseExchangeCalendarEvent(object):
39 |
40 | _id = None # Exchange identifier for the event
41 | _change_key = None # Exchange requires a second key when updating/deleting the event
42 |
43 | service = None
44 | calendar_id = None
45 |
46 | subject = u''
47 | start = None
48 | end = None
49 | location = None
50 | html_body = None
51 | text_body = None
52 | attachments = None
53 | organizer = None
54 | reminder_minutes_before_start = None
55 | is_all_day = None
56 |
57 | recurrence = None
58 | recurrence_end_date = None
59 | recurrence_days = None
60 | recurrence_interval = None
61 |
62 | _type = None
63 |
64 | _attendees = {} # people attending
65 | _resources = {} # conference rooms attending
66 |
67 | _conflicting_event_ids = []
68 |
69 | _track_dirty_attributes = False
70 | _dirty_attributes = set() # any attributes that have changed, and we need to update in Exchange
71 |
72 | # these attributes can be pickled, or output as JSON
73 | DATA_ATTRIBUTES = [
74 | u'_id', u'subject', u'start', u'end', u'location', u'html_body', u'text_body', u'organizer',
75 | u'_attendees', u'_resources', u'reminder_minutes_before_start', u'is_all_day',
76 | 'recurrence', 'recurrence_interval', 'recurrence_days', 'recurrence_day',
77 | ]
78 |
79 | RECURRENCE_ATTRIBUTES = [
80 | 'recurrence', 'recurrence_end_date', 'recurrence_days', 'recurrence_interval',
81 | ]
82 |
83 | WEEKLY_DAYS = [u'Sunday', u'Monday', u'Tuesday', u'Wednesday', u'Thursday', u'Friday', u'Saturday']
84 |
85 | def __init__(self, service, id=None, calendar_id=u'calendar', xml=None, **kwargs):
86 | self.service = service
87 | self.calendar_id = calendar_id
88 |
89 | if xml is not None:
90 | self._init_from_xml(xml)
91 | elif id is None:
92 | self._update_properties(kwargs)
93 | else:
94 | self._init_from_service(id)
95 |
96 | self._track_dirty_attributes = True # magically look for changed attributes
97 |
98 | def _init_from_service(self, id):
99 | """ Connect to the Exchange service and grab all the properties out of it. """
100 | raise NotImplementedError
101 |
102 | def _init_from_xml(self, xml):
103 | """ Using already retrieved XML from Exchange, extract properties out of it. """
104 | raise NotImplementedError
105 |
106 | @property
107 | def id(self):
108 | """ **Read-only.** The internal id Exchange uses to refer to this event. """
109 | return self._id
110 |
111 | @property
112 | def conflicting_event_ids(self):
113 | """ **Read-only.** The internal id Exchange uses to refer to conflicting events. """
114 | return self._conflicting_event_ids
115 |
116 | @property
117 | def change_key(self):
118 | """ **Read-only.** When you change an event, Exchange makes you pass a change key to prevent overwriting a previous version. """
119 | return self._change_key
120 |
121 | @property
122 | def body(self):
123 | """ **Read-only.** Returns either the html_body or the text_body property, whichever is set. """
124 | return self.html_body or self.text_body or None
125 |
126 | @property
127 | def type(self):
128 | """ **Read-only.** This is an attribute pulled from an event in the exchange store. """
129 | return self._type
130 |
131 | @property
132 | def attendees(self):
133 | """
134 | All attendees invited to this event.
135 |
136 | Iterating over this property yields a list of :class:`ExchangeEventResponse` objects::
137 |
138 | for person in event.attendees:
139 | print person.name, person.response
140 |
141 |
142 | You can set the attendee list by assigning to this property::
143 |
144 | event.attendees = [u'somebody@somebody.foo',
145 | u'somebodyelse@somebody.foo']
146 |
147 | event.update()
148 |
149 | If you add attendees this way, they will be required for the event.
150 |
151 | To add optional attendees, either use :attr:`optional_attendees` or add people using the :class:`ExchangeEventAttendee` object::
152 |
153 | from pyexchange.base import ExchangeEventAttendee
154 |
155 | attendee = ExchangeEventAttendee(name="Jane Doe",
156 | email="jane@her.email",
157 | required=False)
158 |
159 | event.attendees = attendee
160 |
161 | event.update()
162 |
163 |
164 | Attendees must have an email address defined.
165 |
166 | """
167 |
168 | # this is redundant in python 2, but necessary in python 3 - .values() returns dict_values, not a list
169 | return [attendee for attendee in self._attendees.values()]
170 |
171 | @attendees.setter
172 | def attendees(self, attendees):
173 | self._attendees = self._build_resource_dictionary(attendees)
174 | self._dirty_attributes.add(u'attendees')
175 |
176 | @property
177 | def required_attendees(self):
178 | """
179 | Required attendees for this event.
180 |
181 | This property otherwise behaves like :attr:`attendees`.
182 | """
183 | return [attendee for attendee in self._attendees.values() if attendee.required]
184 |
185 | @required_attendees.setter
186 | def required_attendees(self, attendees):
187 | required = self._build_resource_dictionary(attendees, required=True)
188 |
189 | # TODO rsanders medium. This is clunky - have to get around python 2/3 inconsistences :/
190 | # must be a better way to do it.
191 |
192 | # Diff the list of required people and drop anybody who wasn't included
193 | for attendee in self.required_attendees:
194 | if attendee.email not in required.keys():
195 | del self._attendees[attendee.email]
196 |
197 | # then add everybody to the list
198 | for email in required:
199 | self._attendees[email] = required[email]
200 |
201 | self._dirty_attributes.add(u'attendees')
202 |
203 | @property
204 | def optional_attendees(self):
205 | """
206 | Optional attendees for this event.
207 |
208 | This property otherwise behaves like :attr:`attendees`.
209 | """
210 | return [attendee for attendee in self._attendees.values() if not attendee.required]
211 |
212 | @optional_attendees.setter
213 | def optional_attendees(self, attendees):
214 | optional = self._build_resource_dictionary(attendees, required=False)
215 |
216 | # TODO rsanders medium. This is clunky - have to get around python 2/3 inconsistences :/
217 | # must be a better way to do it.
218 |
219 | # Diff the list of required people and drop anybody who wasn't included
220 | for attendee in self.optional_attendees:
221 | if attendee.email not in optional.keys():
222 | del self._attendees[attendee.email]
223 |
224 | # then add everybody to the list
225 | for email in optional:
226 | self._attendees[email] = optional[email]
227 |
228 | self._dirty_attributes.add(u'attendees')
229 |
230 | def add_attendees(self, attendees, required=True):
231 | """
232 | Adds new attendees to the event.
233 |
234 | *attendees* can be a list of email addresses or :class:`ExchangeEventAttendee` objects.
235 | """
236 |
237 | new_attendees = self._build_resource_dictionary(attendees, required=required)
238 |
239 | for email in new_attendees:
240 | self._attendees[email] = new_attendees[email]
241 |
242 | self._dirty_attributes.add(u'attendees')
243 |
244 | def remove_attendees(self, attendees):
245 | """
246 | Removes attendees from the event.
247 |
248 | *attendees* can be a list of email addresses or :class:`ExchangeEventAttendee` objects.
249 | """
250 |
251 | attendees_to_delete = self._build_resource_dictionary(attendees)
252 | for email in attendees_to_delete.keys():
253 | if email in self._attendees:
254 | del self._attendees[email]
255 |
256 | self._dirty_attributes.add(u'attendees')
257 |
258 | @property
259 | def resources(self):
260 | """
261 | Resources (aka conference rooms) for this event.
262 |
263 | This property otherwise behaves like :attr:`attendees`.
264 | """
265 | # this is redundant in python 2, but necessary in python 3 - .values() returns dict_values, not a list
266 | return [resource for resource in self._resources.values()]
267 |
268 | @resources.setter
269 | def resources(self, resources):
270 | self._resources = self._build_resource_dictionary(resources)
271 | self._dirty_attributes.add(u'resources')
272 |
273 | def add_resources(self, resources):
274 | """
275 | Adds new resources to the event.
276 |
277 | *resources* can be a list of email addresses or :class:`ExchangeEventAttendee` objects.
278 | """
279 | new_resources = self._build_resource_dictionary(resources)
280 |
281 | for key in new_resources:
282 | self._resources[key] = new_resources[key]
283 | self._dirty_attributes.add(u'resources')
284 |
285 | def remove_resources(self, resources):
286 | """
287 | Removes resources from the event.
288 |
289 | *resources* can be a list of email addresses or :class:`ExchangeEventAttendee` objects.
290 | """
291 |
292 | resources_to_delete = self._build_resource_dictionary(resources)
293 | for email in resources_to_delete.keys():
294 | if email in self._resources:
295 | del self._resources[email]
296 |
297 | self._dirty_attributes.add(u'resources')
298 |
299 | @property
300 | def conference_room(self):
301 | """ Alias to resources - Exchange calls 'em resources, but this is clearer"""
302 | if self.resources and len(self.resources) == 1:
303 | return self.resources[0]
304 |
305 | def validate(self):
306 | """ Validates that all required fields are present """
307 | if not self.start:
308 | raise ValueError("Event has no start date")
309 |
310 | if not self.end:
311 | raise ValueError("Event has no end date")
312 |
313 | if self.end < self.start:
314 | raise ValueError("Start date is after end date")
315 |
316 | if self.reminder_minutes_before_start and not isinstance(self.reminder_minutes_before_start, int):
317 | raise TypeError("reminder_minutes_before_start must be of type int")
318 |
319 | if self.is_all_day and not isinstance(self.is_all_day, bool):
320 | raise TypeError("is_all_day must be of type bool")
321 |
322 | def create(self):
323 | raise NotImplementedError
324 |
325 | def update(self, send_only_to_changed_attendees=True):
326 | raise NotImplementedError
327 |
328 | def cancel(self):
329 | raise NotImplementedError
330 |
331 | def resend_invitations(self):
332 | raise NotImplementedError
333 |
334 | def get_master(self):
335 | raise NotImplementedError
336 |
337 | def get_occurrance(self, instance_index):
338 | raise NotImplementedError
339 |
340 | def conflicting_events(self):
341 | raise NotImplementedError
342 |
343 | def as_json(self):
344 | """ Output ourselves as JSON """
345 | raise NotImplementedError
346 |
347 | def __getstate__(self):
348 | """ Implemented so pickle.dumps() and pickle.loads() work """
349 | state = {}
350 | for attribute in self.DATA_ATTRIBUTES:
351 | state[attribute] = getattr(self, attribute, None)
352 | return state
353 |
354 | def _build_resource_dictionary(self, resources, required=True):
355 | result = {}
356 | if resources:
357 | item_list = resources if isinstance(resources, list) else [resources]
358 | for item in item_list:
359 | if isinstance(item, ExchangeEventAttendee):
360 | if item.email is None:
361 | raise ValueError(u"You tried to add a resource or attendee with a blank email: {0}".format(item))
362 |
363 | result[item.email] = ExchangeEventResponse(email=item.email, required=item.required, name=item.name, response=None, last_response=None)
364 | elif isinstance(item, ExchangeEventResponse):
365 | if item.email is None:
366 | raise ValueError(u"You tried to add a resource or attendee with a blank email: {0}".format(item))
367 |
368 | result[item.email] = item
369 | else:
370 | if item is None:
371 | raise ValueError(u"You tried to add a resource or attendee with a blank email.")
372 |
373 | result[item] = ExchangeEventResponse(email=item, required=required, name=None, response=None, last_response=None)
374 |
375 | return result
376 |
377 | def _update_properties(self, properties):
378 | self._track_dirty_attributes = False
379 | for key in properties:
380 | setattr(self, key, properties[key])
381 | self._track_dirty_attributes = True
382 |
383 | def __setattr__(self, key, value):
384 | """ Magically track public attributes, so we can track what we need to flush to the Exchange store """
385 | if self._track_dirty_attributes and not key.startswith(u"_"):
386 | self._dirty_attributes.add(key)
387 |
388 | object.__setattr__(self, key, value)
389 |
390 | def _reset_dirty_attributes(self):
391 | self._dirty_attributes = set()
392 |
--------------------------------------------------------------------------------
/pyexchange/base/folder.py:
--------------------------------------------------------------------------------
1 | class BaseExchangeFolderService(object):
2 |
3 | def __init__(self, service):
4 | self.service = service
5 |
6 | def get_folder(self, id, *args, **kwargs):
7 | raise NotImplementedError
8 |
9 |
10 | class BaseExchangeFolder(object):
11 |
12 | _id = None
13 | _change_key = None
14 | _parent_id = None
15 | _parent_change_key = None
16 | _folder_type = u'Folder'
17 |
18 | _service = None
19 |
20 | _display_name = u''
21 | _child_folder_count = None
22 | _total_count = None
23 |
24 | _track_dirty_attributes = False
25 | _dirty_attributes = set() # any attributes that have changed, and we need to update in Exchange
26 |
27 | FOLDER_TYPES = (u'Folder', u'CalendarFolder', u'ContactsFolder', u'SearchFolder', u'TasksFolder')
28 |
29 | def __init__(self, service, id=None, xml=None, **kwargs):
30 | self.service = service
31 |
32 | if xml is not None:
33 | self._init_from_xml(xml)
34 | elif id is None:
35 | self._update_properties(kwargs)
36 | else:
37 | self._init_from_service(id)
38 |
39 | def _init_from_xml(self, xml):
40 | raise NotImplementedError
41 |
42 | def _init_from_service(self, id):
43 | raise NotImplementedError
44 |
45 | def create(self):
46 | raise NotImplementedError
47 |
48 | def update(self):
49 | raise NotImplementedError
50 |
51 | def delete(self):
52 | raise NotImplementedError
53 |
54 | @property
55 | def id(self):
56 | """ **Read-only.** The internal id Exchange uses to refer to this folder. """
57 | return self._id
58 |
59 | @property
60 | def change_key(self):
61 | """ **Read-only.** When you change an folder, Exchange makes you pass a change key to prevent overwriting a previous version. """
62 | return self._change_key
63 |
64 | @property
65 | def parent_id(self):
66 | """ **Read-only.** The internal id Exchange uses to refer to the parent folder. """
67 | return self._parent_id
68 |
69 | @parent_id.setter
70 | def parent_id(self, value):
71 | self._parent_id = value
72 |
73 | @property
74 | def folder_type(self):
75 | return self._folder_type
76 |
77 | @folder_type.setter
78 | def folder_type(self, value):
79 | if value in self.FOLDER_TYPES:
80 | self._folder_type = value
81 |
82 | def _update_properties(self, properties):
83 | self._track_dirty_attributes = False
84 | for key in properties:
85 | setattr(self, key, properties[key])
86 | self._track_dirty_attributes = True
87 |
88 | def __setattr__(self, key, value):
89 | """ Magically track public attributes, so we can track what we need to flush to the Exchange store """
90 | if self._track_dirty_attributes and not key.startswith(u"_"):
91 | self._dirty_attributes.add(key)
92 |
93 | object.__setattr__(self, key, value)
94 |
95 | def _reset_dirty_attributes(self):
96 | self._dirty_attributes = set()
97 |
98 | def validate(self):
99 | """ Validates that all required fields are present """
100 | if not self.display_name:
101 | raise ValueError("Folder has no display_name")
102 |
103 | if not self.parent_id:
104 | raise ValueError("Folder has no parent_id")
105 |
--------------------------------------------------------------------------------
/pyexchange/base/soap.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import logging
8 |
9 | from lxml import etree
10 | from lxml.builder import ElementMaker
11 | from datetime import datetime
12 | from pytz import utc
13 |
14 | from ..exceptions import FailedExchangeException
15 |
16 | SOAP_NS = u'http://schemas.xmlsoap.org/soap/envelope/'
17 |
18 | SOAP_NAMESPACES = {u's': SOAP_NS}
19 | S = ElementMaker(namespace=SOAP_NS, nsmap=SOAP_NAMESPACES)
20 |
21 | log = logging.getLogger('pyexchange')
22 |
23 |
24 | class ExchangeServiceSOAP(object):
25 |
26 | EXCHANGE_DATE_FORMAT = u"%Y-%m-%dT%H:%M:%SZ"
27 |
28 | def __init__(self, connection):
29 | self.connection = connection
30 |
31 | def send(self, xml, headers=None, retries=4, timeout=30, encoding="utf-8"):
32 | request_xml = self._wrap_soap_xml_request(xml)
33 | log.info(etree.tostring(request_xml, encoding=encoding, pretty_print=True))
34 | response = self._send_soap_request(request_xml, headers=headers, retries=retries, timeout=timeout, encoding=encoding)
35 | return self._parse(response, encoding=encoding)
36 |
37 | def _parse(self, response, encoding="utf-8"):
38 |
39 | try:
40 | tree = etree.XML(response.encode(encoding))
41 | except (etree.XMLSyntaxError, TypeError) as err:
42 | raise FailedExchangeException(u"Unable to parse response from Exchange - check your login information. Error: %s" % err)
43 |
44 | self._check_for_errors(tree)
45 |
46 | log.info(etree.tostring(tree, encoding=encoding, pretty_print=True))
47 | return tree
48 |
49 | def _check_for_errors(self, xml_tree):
50 | self._check_for_SOAP_fault(xml_tree)
51 |
52 | def _check_for_SOAP_fault(self, xml_tree):
53 | # Check for SOAP errors. if is anywhere in the response, flip out
54 |
55 | fault_nodes = xml_tree.xpath(u'//s:Fault', namespaces=SOAP_NAMESPACES)
56 |
57 | if fault_nodes:
58 | fault = fault_nodes[0]
59 | log.debug(etree.tostring(fault, pretty_print=True))
60 | raise FailedExchangeException(u"SOAP Fault from Exchange server", fault.text)
61 |
62 | def _send_soap_request(self, xml, headers=None, retries=2, timeout=30, encoding="utf-8"):
63 | body = etree.tostring(xml, encoding=encoding)
64 |
65 | response = self.connection.send(body, headers, retries, timeout)
66 | return response
67 |
68 | def _wrap_soap_xml_request(self, exchange_xml):
69 | root = S.Envelope(S.Body(exchange_xml))
70 | return root
71 |
72 | def _parse_date(self, date_string):
73 | date = datetime.strptime(date_string, self.EXCHANGE_DATE_FORMAT)
74 | date = date.replace(tzinfo=utc)
75 |
76 | return date
77 |
78 | def _parse_date_only_naive(self, date_string):
79 | date = datetime.strptime(date_string[0:10], self.EXCHANGE_DATE_FORMAT[0:8])
80 |
81 | return date.date()
82 |
83 | def _xpath_to_dict(self, element, property_map, namespace_map):
84 | """
85 | property_map = {
86 | u'name' : { u'xpath' : u't:Mailbox/t:Name'},
87 | u'email' : { u'xpath' : u't:Mailbox/t:EmailAddress'},
88 | u'response' : { u'xpath' : u't:ResponseType'},
89 | u'last_response': { u'xpath' : u't:LastResponseTime', u'cast': u'datetime'},
90 | }
91 |
92 | This runs the given xpath on the node and returns a dictionary
93 |
94 | """
95 |
96 | result = {}
97 |
98 | log.info(etree.tostring(element, pretty_print=True))
99 |
100 | for key in property_map:
101 | item = property_map[key]
102 | log.info(u'Pulling xpath {xpath} into key {key}'.format(key=key, xpath=item[u'xpath']))
103 | nodes = element.xpath(item[u'xpath'], namespaces=namespace_map)
104 |
105 | if nodes:
106 | result_for_node = []
107 |
108 | for node in nodes:
109 | cast_as = item.get(u'cast', None)
110 |
111 | if cast_as == u'datetime':
112 | result_for_node.append(self._parse_date(node.text))
113 | elif cast_as == u'date_only_naive':
114 | result_for_node.append(self._parse_date_only_naive(node.text))
115 | elif cast_as == u'int':
116 | result_for_node.append(int(node.text))
117 | elif cast_as == u'bool':
118 | if node.text.lower() == u'true':
119 | result_for_node.append(True)
120 | else:
121 | result_for_node.append(False)
122 | else:
123 | result_for_node.append(node.text)
124 |
125 | if not result_for_node:
126 | result[key] = None
127 | elif len(result_for_node) == 1:
128 | result[key] = result_for_node[0]
129 | else:
130 | result[key] = result_for_node
131 |
132 | return result
133 |
--------------------------------------------------------------------------------
/pyexchange/compat.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | IS_PYTHON3 = sys.version_info >= (3, 0)
4 |
5 | if IS_PYTHON3:
6 | BASESTRING_TYPES = str
7 | else:
8 | BASESTRING_TYPES = (str, unicode)
9 |
10 | def _unicode(item):
11 | if IS_PYTHON3:
12 | return str(item)
13 | else:
14 | return unicode(item)
--------------------------------------------------------------------------------
/pyexchange/connection.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import requests
8 | from requests_ntlm import HttpNtlmAuth
9 |
10 | import logging
11 |
12 | from .exceptions import FailedExchangeException
13 |
14 | log = logging.getLogger('pyexchange')
15 |
16 |
17 | class ExchangeBaseConnection(object):
18 | """ Base class for Exchange connections."""
19 |
20 | def send(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
21 | raise NotImplementedError
22 |
23 |
24 | class ExchangeNTLMAuthConnection(ExchangeBaseConnection):
25 | """ Connection to Exchange that uses NTLM authentication """
26 |
27 | def __init__(self, url, username, password, verify_certificate=True, **kwargs):
28 | self.url = url
29 | self.username = username
30 | self.password = password
31 | self.verify_certificate = verify_certificate
32 | self.handler = None
33 | self.session = None
34 | self.password_manager = None
35 |
36 | def build_password_manager(self):
37 | if self.password_manager:
38 | return self.password_manager
39 |
40 | log.debug(u'Constructing password manager')
41 |
42 | self.password_manager = HttpNtlmAuth(self.username, self.password)
43 |
44 | return self.password_manager
45 |
46 | def build_session(self):
47 | if self.session:
48 | return self.session
49 |
50 | log.debug(u'Constructing opener')
51 |
52 | self.password_manager = self.build_password_manager()
53 |
54 | self.session = requests.Session()
55 | self.session.auth = self.password_manager
56 |
57 | return self.session
58 |
59 | def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
60 | if not self.session:
61 | self.session = self.build_session()
62 |
63 | try:
64 | response = self.session.post(self.url, data=body, headers=headers, verify = self.verify_certificate)
65 | response.raise_for_status()
66 | except requests.exceptions.RequestException as err:
67 | log.debug(err.response.content)
68 | raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
69 |
70 | log.info(u'Got response: {code}'.format(code=response.status_code))
71 | log.debug(u'Got response headers: {headers}'.format(headers=response.headers))
72 | log.debug(u'Got body: {body}'.format(body=response.text))
73 |
74 | return response.text
75 |
--------------------------------------------------------------------------------
/pyexchange/exceptions.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 |
8 |
9 | class FailedExchangeException(Exception):
10 | """Raised when the Microsoft Exchange Server returns an error via SOAP for a request."""
11 | pass
12 |
13 |
14 | class ExchangeInvalidIdMalformedException(FailedExchangeException):
15 | """Raised when we ask for an event key that doesn't exist."""
16 | pass
17 |
18 |
19 | class ExchangeStaleChangeKeyException(FailedExchangeException):
20 | """Raised when a edit event fails due to a stale change key. Exchange requires a change token for all edit events,
21 | and they change every time an object is updated"""
22 | pass
23 |
24 |
25 | class ExchangeItemNotFoundException(FailedExchangeException):
26 | """
27 | Raised when an item is not found on the Exchange server
28 | """
29 | pass
30 |
31 |
32 | class ExchangeIrresolvableConflictException(FailedExchangeException):
33 | """Raised when attempting to update an item that has changed since the the current change key was obtained."""
34 | pass
35 |
36 |
37 | class ExchangeInternalServerTransientErrorException(FailedExchangeException):
38 | """Raised when an internal server error occurs in Exchange and the request can actually be retried."""
39 | pass
40 |
41 |
42 | class InvalidEventType(Exception):
43 | """Raised when a method for an event gets called on the wrong type of event."""
44 | pass
45 |
--------------------------------------------------------------------------------
/pyexchange/exchange2010/soap_request.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | from lxml.builder import ElementMaker
8 | from ..utils import convert_datetime_to_utc
9 | from ..compat import _unicode
10 |
11 | MSG_NS = u'http://schemas.microsoft.com/exchange/services/2006/messages'
12 | TYPE_NS = u'http://schemas.microsoft.com/exchange/services/2006/types'
13 | SOAP_NS = u'http://schemas.xmlsoap.org/soap/envelope/'
14 |
15 | NAMESPACES = {u'm': MSG_NS, u't': TYPE_NS, u's': SOAP_NS}
16 |
17 | M = ElementMaker(namespace=MSG_NS, nsmap=NAMESPACES)
18 | T = ElementMaker(namespace=TYPE_NS, nsmap=NAMESPACES)
19 |
20 | EXCHANGE_DATETIME_FORMAT = u"%Y-%m-%dT%H:%M:%SZ"
21 | EXCHANGE_DATE_FORMAT = u"%Y-%m-%d"
22 |
23 | DISTINGUISHED_IDS = (
24 | 'calendar', 'contacts', 'deleteditems', 'drafts', 'inbox', 'journal', 'notes', 'outbox', 'sentitems',
25 | 'tasks', 'msgfolderroot', 'root', 'junkemail', 'searchfolders', 'voicemail', 'recoverableitemsroot',
26 | 'recoverableitemsdeletions', 'recoverableitemsversions', 'recoverableitemspurges', 'archiveroot',
27 | 'archivemsgfolderroot', 'archivedeleteditems', 'archiverecoverableitemsroot',
28 | 'Archiverecoverableitemsdeletions', 'Archiverecoverableitemsversions', 'Archiverecoverableitemspurges',
29 | )
30 |
31 |
32 | def exchange_header():
33 |
34 | return T.RequestServerVersion({u'Version': u'Exchange2010'})
35 |
36 |
37 | def resource_node(element, resources):
38 | """
39 | Helper function to generate a person/conference room node from an email address
40 |
41 |
42 |
43 |
44 | {{ attendee_email }}
45 |
46 |
47 |
48 | """
49 |
50 | for attendee in resources:
51 | element.append(
52 | T.Attendee(
53 | T.Mailbox(
54 | T.EmailAddress(attendee.email)
55 | )
56 | )
57 | )
58 |
59 | return element
60 |
61 |
62 | def delete_field(field_uri):
63 | """
64 | Helper function to request deletion of a field. This is necessary when you want to overwrite values instead of
65 | appending.
66 |
67 |
68 |
69 |
70 | """
71 |
72 | root = T.DeleteItemField(
73 | T.FieldURI(FieldURI=field_uri)
74 | )
75 |
76 | return root
77 |
78 |
79 | def get_item(exchange_id, format=u"Default"):
80 | """
81 | Requests a calendar item from the store.
82 |
83 | exchange_id is the id for this event in the Exchange store.
84 |
85 | format controls how much data you get back from Exchange. Full docs are here, but acceptible values
86 | are IdOnly, Default, and AllProperties.
87 |
88 | http://msdn.microsoft.com/en-us/library/aa564509(v=exchg.140).aspx
89 |
90 |
92 |
93 | {format}
94 |
95 |
96 |
97 |
98 |
99 |
100 | """
101 |
102 | elements = list()
103 | if type(exchange_id) == list:
104 | for item in exchange_id:
105 | elements.append(T.ItemId(Id=item))
106 | else:
107 | elements = [T.ItemId(Id=exchange_id)]
108 |
109 | root = M.GetItem(
110 | M.ItemShape(
111 | T.BaseShape(format)
112 | ),
113 | M.ItemIds(
114 | *elements
115 | )
116 | )
117 | return root
118 |
119 | def get_calendar_items(format=u"Default", calendar_id=u'calendar', start=None, end=None, max_entries=999999, delegate_for=None):
120 | start = start.strftime(EXCHANGE_DATETIME_FORMAT)
121 | end = end.strftime(EXCHANGE_DATETIME_FORMAT)
122 |
123 | if calendar_id == u'calendar':
124 | if delegate_for is None:
125 | target = M.ParentFolderIds(T.DistinguishedFolderId(Id=calendar_id))
126 | else:
127 | target = M.ParentFolderIds(
128 | T.DistinguishedFolderId(
129 | {'Id': 'calendar'},
130 | T.Mailbox(T.EmailAddress(delegate_for))
131 | )
132 | )
133 | else:
134 | target = M.ParentFolderIds(T.FolderId(Id=calendar_id))
135 |
136 | root = M.FindItem(
137 | {u'Traversal': u'Shallow'},
138 | M.ItemShape(
139 | T.BaseShape(format)
140 | ),
141 | M.CalendarView({
142 | u'MaxEntriesReturned': _unicode(max_entries),
143 | u'StartDate': start,
144 | u'EndDate': end,
145 | }),
146 | target,
147 | )
148 |
149 | return root
150 |
151 |
152 | def get_master(exchange_id, format=u"Default"):
153 | """
154 | Requests a calendar item from the store.
155 |
156 | exchange_id is the id for this event in the Exchange store.
157 |
158 | format controls how much data you get back from Exchange. Full docs are here, but acceptible values
159 | are IdOnly, Default, and AllProperties.
160 |
161 | http://msdn.microsoft.com/en-us/library/aa564509(v=exchg.140).aspx
162 |
163 |
165 |
166 | {format}
167 |
168 |
169 |
170 |
171 |
172 |
173 | """
174 |
175 | root = M.GetItem(
176 | M.ItemShape(
177 | T.BaseShape(format)
178 | ),
179 | M.ItemIds(
180 | T.RecurringMasterItemId(OccurrenceId=exchange_id)
181 | )
182 | )
183 | return root
184 |
185 |
186 | def get_occurrence(exchange_id, instance_index, format=u"Default"):
187 | """
188 | Requests one or more calendar items from the store matching the master & index.
189 |
190 | exchange_id is the id for the master event in the Exchange store.
191 |
192 | format controls how much data you get back from Exchange. Full docs are here, but acceptible values
193 | are IdOnly, Default, and AllProperties.
194 |
195 | GetItem Doc:
196 | http://msdn.microsoft.com/en-us/library/aa564509(v=exchg.140).aspx
197 | OccurrenceItemId Doc:
198 | http://msdn.microsoft.com/en-us/library/office/aa580744(v=exchg.150).aspx
199 |
200 |
202 |
203 | {format}
204 |
205 |
206 | {% for index in instance_index %}
207 |
208 | {% endfor %}
209 |
210 |
211 | """
212 |
213 | root = M.GetItem(
214 | M.ItemShape(
215 | T.BaseShape(format)
216 | ),
217 | M.ItemIds()
218 | )
219 |
220 | items_node = root.xpath("//m:ItemIds", namespaces=NAMESPACES)[0]
221 | for index in instance_index:
222 | items_node.append(T.OccurrenceItemId(RecurringMasterId=exchange_id, InstanceIndex=str(index)))
223 | return root
224 |
225 |
226 | def get_folder(folder_id, format=u"Default"):
227 |
228 | id = T.DistinguishedFolderId(Id=folder_id) if folder_id in DISTINGUISHED_IDS else T.FolderId(Id=folder_id)
229 |
230 | root = M.GetFolder(
231 | M.FolderShape(
232 | T.BaseShape(format)
233 | ),
234 | M.FolderIds(id)
235 | )
236 | return root
237 |
238 |
239 | def new_folder(folder):
240 |
241 | id = T.DistinguishedFolderId(Id=folder.parent_id) if folder.parent_id in DISTINGUISHED_IDS else T.FolderId(Id=folder.parent_id)
242 |
243 | if folder.folder_type == u'Folder':
244 | folder_node = T.Folder(T.DisplayName(folder.display_name))
245 | elif folder.folder_type == u'CalendarFolder':
246 | folder_node = T.CalendarFolder(T.DisplayName(folder.display_name))
247 |
248 | root = M.CreateFolder(
249 | M.ParentFolderId(id),
250 | M.Folders(folder_node)
251 | )
252 | return root
253 |
254 |
255 | def find_folder(parent_id, format=u"Default"):
256 |
257 | id = T.DistinguishedFolderId(Id=parent_id) if parent_id in DISTINGUISHED_IDS else T.FolderId(Id=parent_id)
258 |
259 | root = M.FindFolder(
260 | {u'Traversal': u'Shallow'},
261 | M.FolderShape(
262 | T.BaseShape(format)
263 | ),
264 | M.ParentFolderIds(id)
265 | )
266 | return root
267 |
268 |
269 | def delete_folder(folder):
270 |
271 | root = M.DeleteFolder(
272 | {u'DeleteType': 'HardDelete'},
273 | M.FolderIds(
274 | T.FolderId(Id=folder.id)
275 | )
276 | )
277 | return root
278 |
279 |
280 | def new_event(event):
281 | """
282 | Requests a new event be created in the store.
283 |
284 | http://msdn.microsoft.com/en-us/library/aa564690(v=exchg.140).aspx
285 |
286 |
289 |
290 |
291 |
292 |
293 |
294 | {event.subject}
295 | {event.subject}
296 |
297 |
298 |
299 |
300 | {% for attendee_email in meeting.required_attendees %}
301 |
302 |
303 | {{ attendee_email }}
304 |
305 |
306 | HTTPretty {% endfor %}
307 |
308 | {% if meeting.optional_attendees %}
309 |
310 | {% for attendee_email in meeting.optional_attendees %}
311 |
312 |
313 | {{ attendee_email }}
314 |
315 |
316 | {% endfor %}
317 |
318 | {% endif %}
319 | {% if meeting.conference_room %}
320 |
321 |
322 |
323 | {{ meeting.conference_room.email }}
324 |
325 |
326 |
327 | {% endif %}
328 |
329 |
330 |
331 | """
332 |
333 | id = T.DistinguishedFolderId(Id=event.calendar_id) if event.calendar_id in DISTINGUISHED_IDS else T.FolderId(Id=event.calendar_id)
334 |
335 | start = convert_datetime_to_utc(event.start)
336 | end = convert_datetime_to_utc(event.end)
337 |
338 | root = M.CreateItem(
339 | M.SavedItemFolderId(id),
340 | M.Items(
341 | T.CalendarItem(
342 | T.Subject(event.subject),
343 | T.Body(event.body or u'', BodyType="HTML"),
344 | )
345 | ),
346 | SendMeetingInvitations="SendToAllAndSaveCopy"
347 | )
348 |
349 | calendar_node = root.xpath(u'/m:CreateItem/m:Items/t:CalendarItem', namespaces=NAMESPACES)[0]
350 |
351 | if event.reminder_minutes_before_start:
352 | calendar_node.append(T.ReminderIsSet('true'))
353 | calendar_node.append(T.ReminderMinutesBeforeStart(str(event.reminder_minutes_before_start)))
354 | else:
355 | calendar_node.append(T.ReminderIsSet('false'))
356 |
357 | calendar_node.append(T.Start(start.strftime(EXCHANGE_DATETIME_FORMAT)))
358 | calendar_node.append(T.End(end.strftime(EXCHANGE_DATETIME_FORMAT)))
359 |
360 | if event.is_all_day:
361 | calendar_node.append(T.IsAllDayEvent('true'))
362 |
363 | calendar_node.append(T.Location(event.location or u''))
364 |
365 | if event.required_attendees:
366 | calendar_node.append(resource_node(element=T.RequiredAttendees(), resources=event.required_attendees))
367 |
368 | if event.optional_attendees:
369 | calendar_node.append(resource_node(element=T.OptionalAttendees(), resources=event.optional_attendees))
370 |
371 | if event.resources:
372 | calendar_node.append(resource_node(element=T.Resources(), resources=event.resources))
373 |
374 | if event.recurrence:
375 |
376 | if event.recurrence == u'daily':
377 | recurrence = T.DailyRecurrence(
378 | T.Interval(str(event.recurrence_interval)),
379 | )
380 | elif event.recurrence == u'weekly':
381 | recurrence = T.WeeklyRecurrence(
382 | T.Interval(str(event.recurrence_interval)),
383 | T.DaysOfWeek(event.recurrence_days),
384 | )
385 | elif event.recurrence == u'monthly':
386 | recurrence = T.AbsoluteMonthlyRecurrence(
387 | T.Interval(str(event.recurrence_interval)),
388 | T.DayOfMonth(str(event.start.day)),
389 | )
390 | elif event.recurrence == u'yearly':
391 | recurrence = T.AbsoluteYearlyRecurrence(
392 | T.DayOfMonth(str(event.start.day)),
393 | T.Month(event.start.strftime("%B")),
394 | )
395 |
396 | calendar_node.append(
397 | T.Recurrence(
398 | recurrence,
399 | T.EndDateRecurrence(
400 | T.StartDate(event.start.strftime(EXCHANGE_DATE_FORMAT)),
401 | T.EndDate(event.recurrence_end_date.strftime(EXCHANGE_DATE_FORMAT)),
402 | )
403 | )
404 | )
405 |
406 | return root
407 |
408 |
409 | def delete_event(event):
410 | """
411 |
412 | Requests an item be deleted from the store.
413 |
414 |
415 |
421 |
422 |
423 |
424 |
425 |
426 | """
427 | root = M.DeleteItem(
428 | M.ItemIds(
429 | T.ItemId(Id=event.id, ChangeKey=event.change_key)
430 | ),
431 | DeleteType="HardDelete",
432 | SendMeetingCancellations="SendToAllAndSaveCopy",
433 | AffectedTaskOccurrences="AllOccurrences"
434 | )
435 |
436 | return root
437 |
438 |
439 | def move_event(event, folder_id):
440 |
441 | id = T.DistinguishedFolderId(Id=folder_id) if folder_id in DISTINGUISHED_IDS else T.FolderId(Id=folder_id)
442 |
443 | root = M.MoveItem(
444 | M.ToFolderId(id),
445 | M.ItemIds(
446 | T.ItemId(Id=event.id, ChangeKey=event.change_key)
447 | )
448 | )
449 | return root
450 |
451 |
452 | def move_folder(folder, folder_id):
453 |
454 | id = T.DistinguishedFolderId(Id=folder_id) if folder_id in DISTINGUISHED_IDS else T.FolderId(Id=folder_id)
455 |
456 | root = M.MoveFolder(
457 | M.ToFolderId(id),
458 | M.FolderIds(
459 | T.FolderId(Id=folder.id)
460 | )
461 | )
462 | return root
463 |
464 |
465 | def update_property_node(node_to_insert, field_uri):
466 | """ Helper function - generates a SetItemField which tells Exchange you want to overwrite the contents of a field."""
467 | root = T.SetItemField(
468 | T.FieldURI(FieldURI=field_uri),
469 | T.CalendarItem(node_to_insert)
470 | )
471 | return root
472 |
473 |
474 | def update_item(event, updated_attributes, calendar_item_update_operation_type):
475 | """ Saves updates to an event in the store. Only request changes for attributes that have actually changed."""
476 |
477 | root = M.UpdateItem(
478 | M.ItemChanges(
479 | T.ItemChange(
480 | T.ItemId(Id=event.id, ChangeKey=event.change_key),
481 | T.Updates()
482 | )
483 | ),
484 | ConflictResolution=u"AlwaysOverwrite",
485 | MessageDisposition=u"SendAndSaveCopy",
486 | SendMeetingInvitationsOrCancellations=calendar_item_update_operation_type
487 | )
488 |
489 | update_node = root.xpath(u'/m:UpdateItem/m:ItemChanges/t:ItemChange/t:Updates', namespaces=NAMESPACES)[0]
490 |
491 | # if not send_only_to_changed_attendees:
492 | # # We want to resend invites, which you do by setting an attribute to the same value it has. Right now, events
493 | # # are always scheduled as Busy time, so we just set that again.
494 | # update_node.append(
495 | # update_property_node(field_uri="calendar:LegacyFreeBusyStatus", node_to_insert=T.LegacyFreeBusyStatus("Busy"))
496 | # )
497 |
498 | if u'html_body' in updated_attributes:
499 | update_node.append(
500 | update_property_node(field_uri="item:Body", node_to_insert=T.Body(event.html_body, BodyType="HTML"))
501 | )
502 |
503 | if u'text_body' in updated_attributes:
504 | update_node.append(
505 | update_property_node(field_uri="item:Body", node_to_insert=T.Body(event.text_body, BodyType="Text"))
506 | )
507 |
508 | if u'subject' in updated_attributes:
509 | update_node.append(
510 | update_property_node(field_uri="item:Subject", node_to_insert=T.Subject(event.subject))
511 | )
512 |
513 | if u'start' in updated_attributes:
514 | start = convert_datetime_to_utc(event.start)
515 |
516 | update_node.append(
517 | update_property_node(field_uri="calendar:Start", node_to_insert=T.Start(start.strftime(EXCHANGE_DATETIME_FORMAT)))
518 | )
519 |
520 | if u'end' in updated_attributes:
521 | end = convert_datetime_to_utc(event.end)
522 |
523 | update_node.append(
524 | update_property_node(field_uri="calendar:End", node_to_insert=T.End(end.strftime(EXCHANGE_DATETIME_FORMAT)))
525 | )
526 |
527 | if u'location' in updated_attributes:
528 | update_node.append(
529 | update_property_node(field_uri="calendar:Location", node_to_insert=T.Location(event.location))
530 | )
531 |
532 | if u'attendees' in updated_attributes:
533 |
534 | if event.required_attendees:
535 | required = resource_node(element=T.RequiredAttendees(), resources=event.required_attendees)
536 |
537 | update_node.append(
538 | update_property_node(field_uri="calendar:RequiredAttendees", node_to_insert=required)
539 | )
540 | else:
541 | update_node.append(delete_field(field_uri="calendar:RequiredAttendees"))
542 |
543 | if event.optional_attendees:
544 | optional = resource_node(element=T.OptionalAttendees(), resources=event.optional_attendees)
545 |
546 | update_node.append(
547 | update_property_node(field_uri="calendar:OptionalAttendees", node_to_insert=optional)
548 | )
549 | else:
550 | update_node.append(delete_field(field_uri="calendar:OptionalAttendees"))
551 |
552 | if u'resources' in updated_attributes:
553 | if event.resources:
554 | resources = resource_node(element=T.Resources(), resources=event.resources)
555 |
556 | update_node.append(
557 | update_property_node(field_uri="calendar:Resources", node_to_insert=resources)
558 | )
559 | else:
560 | update_node.append(delete_field(field_uri="calendar:Resources"))
561 |
562 | if u'reminder_minutes_before_start' in updated_attributes:
563 | if event.reminder_minutes_before_start:
564 | update_node.append(
565 | update_property_node(field_uri="item:ReminderIsSet", node_to_insert=T.ReminderIsSet('true'))
566 | )
567 | update_node.append(
568 | update_property_node(
569 | field_uri="item:ReminderMinutesBeforeStart",
570 | node_to_insert=T.ReminderMinutesBeforeStart(str(event.reminder_minutes_before_start))
571 | )
572 | )
573 | else:
574 | update_node.append(
575 | update_property_node(field_uri="item:ReminderIsSet", node_to_insert=T.ReminderIsSet('false'))
576 | )
577 |
578 | if u'is_all_day' in updated_attributes:
579 | update_node.append(
580 | update_property_node(field_uri="calendar:IsAllDayEvent", node_to_insert=T.IsAllDayEvent(str(event.is_all_day).lower()))
581 | )
582 |
583 | for attr in event.RECURRENCE_ATTRIBUTES:
584 | if attr in updated_attributes:
585 |
586 | recurrence_node = T.Recurrence()
587 |
588 | if event.recurrence == 'daily':
589 | recurrence_node.append(
590 | T.DailyRecurrence(
591 | T.Interval(str(event.recurrence_interval)),
592 | )
593 | )
594 | elif event.recurrence == 'weekly':
595 | recurrence_node.append(
596 | T.WeeklyRecurrence(
597 | T.Interval(str(event.recurrence_interval)),
598 | T.DaysOfWeek(event.recurrence_days),
599 | )
600 | )
601 | elif event.recurrence == 'monthly':
602 | recurrence_node.append(
603 | T.AbsoluteMonthlyRecurrence(
604 | T.Interval(str(event.recurrence_interval)),
605 | T.DayOfMonth(str(event.start.day)),
606 | )
607 | )
608 | elif event.recurrence == 'yearly':
609 | recurrence_node.append(
610 | T.AbsoluteYearlyRecurrence(
611 | T.DayOfMonth(str(event.start.day)),
612 | T.Month(event.start.strftime("%B")),
613 | )
614 | )
615 |
616 | recurrence_node.append(
617 | T.EndDateRecurrence(
618 | T.StartDate(event.start.strftime(EXCHANGE_DATE_FORMAT)),
619 | T.EndDate(event.recurrence_end_date.strftime(EXCHANGE_DATE_FORMAT)),
620 | )
621 | )
622 |
623 | update_node.append(
624 | update_property_node(field_uri="calendar:Recurrence", node_to_insert=recurrence_node)
625 | )
626 |
627 | return root
628 |
--------------------------------------------------------------------------------
/pyexchange/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | from pytz import utc
8 |
9 |
10 | def convert_datetime_to_utc(datetime_to_convert):
11 | if datetime_to_convert is None:
12 | return None
13 |
14 | if datetime_to_convert.tzinfo:
15 | return datetime_to_convert.astimezone(utc)
16 | else:
17 | return utc.localize(datetime_to_convert)
18 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | lxml
2 | pytz
3 | requests
4 | requests-ntlm
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | cover-package=pyexchange
3 | where=tests/
4 | nologcapture=True
5 |
6 | [flake8]
7 | ignore = E111,E121,E126,E501
8 |
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 | here = os.path.abspath(os.path.dirname(__file__))
5 |
6 | try:
7 | README = open(os.path.join(here, 'README.md')).read()
8 | CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
9 | except:
10 | README = ''
11 | CHANGES = ''
12 |
13 | setup(
14 | name='pyexchange',
15 | version='0.7-dev',
16 | url='https://github.com/linkedin/pyexchange',
17 | license='Apache',
18 | author='Rachel Sanders',
19 | author_email='rsanders@linkedin.com',
20 | maintainer='Rachel Sanders',
21 | maintainer_email='rsanders@linkedin.com',
22 | description='A simple library to talk to Microsoft Exchange',
23 | long_description=README + '\n\n' + CHANGES,
24 | zip_safe=False,
25 | test_suite="tests",
26 | platforms='any',
27 | include_package_data=True,
28 | packages=find_packages('.', exclude=['test*']),
29 | install_requires=['lxml', 'pytz', 'requests', 'requests-ntlm'],
30 | classifiers=[
31 | 'Development Status :: 4 - Beta',
32 | 'Intended Audience :: Developers',
33 | 'License :: OSI Approved :: Apache Software License',
34 | 'Operating System :: OS Independent',
35 | 'Programming Language :: Python',
36 | 'Programming Language :: Python',
37 | 'Programming Language :: Python :: 2',
38 | 'Programming Language :: Python :: 2.6',
39 | 'Programming Language :: Python :: 2.7',
40 | 'Programming Language :: Python :: 3',
41 | 'Programming Language :: Python :: 3.3',
42 | 'Programming Language :: Python :: 3.4',
43 | 'Topic :: Software Development :: Libraries :: Python Modules'
44 | ]
45 | )
46 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/exchange2010/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_create_event.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import pickle
8 | import unittest
9 | from httpretty import HTTPretty, httprettified
10 | from pytest import raises
11 | from pyexchange import Exchange2010Service
12 |
13 | from pyexchange.connection import ExchangeNTLMAuthConnection
14 | from pyexchange.base.calendar import ExchangeEventAttendee
15 | from pyexchange.exceptions import * # noqa
16 |
17 | from .fixtures import * # noqa
18 |
19 |
20 | class Test_PopulatingANewEvent(unittest.TestCase):
21 | """ Tests all the attribute setting works when creating a new event """
22 | calendar = None
23 |
24 | @classmethod
25 | def setUpClass(cls):
26 |
27 | cls.calendar = Exchange2010Service(
28 | connection=ExchangeNTLMAuthConnection(
29 | url=FAKE_EXCHANGE_URL,
30 | username=FAKE_EXCHANGE_USERNAME,
31 | password=FAKE_EXCHANGE_PASSWORD,
32 | )
33 | ).calendar()
34 |
35 | def test_canary(self):
36 | event = self.calendar.event()
37 | assert event is not None
38 |
39 | def test_events_created_dont_have_an_id(self):
40 | event = self.calendar.event()
41 | assert event.id is None
42 |
43 | def test_can_add_a_subject(self):
44 | event = self.calendar.event(subject=TEST_EVENT.subject)
45 | assert event.subject == TEST_EVENT.subject
46 |
47 | def test_can_add_a_location(self):
48 | event = self.calendar.event(location=TEST_EVENT.location)
49 | assert event.location == TEST_EVENT.location
50 |
51 | def test_can_add_an_html_body(self):
52 | event = self.calendar.event(html_body=TEST_EVENT.body)
53 | assert event.html_body == TEST_EVENT.body
54 | assert event.text_body is None
55 | assert event.body == TEST_EVENT.body
56 |
57 | def test_can_add_a_text_body(self):
58 | event = self.calendar.event(text_body=TEST_EVENT.body)
59 | assert event.text_body == TEST_EVENT.body
60 | assert event.html_body is None
61 | assert event.body == TEST_EVENT.body
62 |
63 | def test_can_add_a_start_time(self):
64 | event = self.calendar.event(start=TEST_EVENT.start)
65 | assert event.start == TEST_EVENT.start
66 |
67 | def test_can_add_an_end_time(self):
68 | event = self.calendar.event(end=TEST_EVENT.end)
69 | assert event.end == TEST_EVENT.end
70 |
71 | def test_can_add_attendees_via_email(self):
72 | event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
73 | assert len(event.attendees) == 1
74 | assert len(event.required_attendees) == 1
75 | assert len(event.optional_attendees) == 0
76 | assert event.attendees[0].email == PERSON_REQUIRED_ACCEPTED.email
77 |
78 | def test_can_add_multiple_attendees_via_email(self):
79 | event = self.calendar.event(attendees=[PERSON_REQUIRED_ACCEPTED.email, PERSON_REQUIRED_TENTATIVE.email])
80 | assert len(event.attendees) == 2
81 | assert len(event.required_attendees) == 2
82 | assert len(event.optional_attendees) == 0
83 |
84 | def test_can_add_attendees_via_named_tuple(self):
85 |
86 | person = ExchangeEventAttendee(name=PERSON_OPTIONAL_ACCEPTED.name, email=PERSON_OPTIONAL_ACCEPTED.email, required=PERSON_OPTIONAL_ACCEPTED.required)
87 |
88 | event = self.calendar.event(attendees=person)
89 | assert len(event.attendees) == 1
90 | assert len(event.required_attendees) == 0
91 | assert len(event.optional_attendees) == 1
92 | assert event.attendees[0].email == PERSON_OPTIONAL_ACCEPTED.email
93 |
94 | def test_can_assign_to_required_attendees(self):
95 |
96 | event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
97 | event.required_attendees = [PERSON_REQUIRED_ACCEPTED.email, PERSON_OPTIONAL_ACCEPTED.email]
98 |
99 | assert len(event.attendees) == 2
100 | assert len(event.required_attendees) == 2
101 | assert len(event.optional_attendees) == 0
102 |
103 | def test_can_assign_to_optional_attendees(self):
104 |
105 | event = self.calendar.event(attendees=PERSON_REQUIRED_ACCEPTED.email)
106 | event.optional_attendees = PERSON_OPTIONAL_ACCEPTED.email
107 |
108 | assert len(event.attendees) == 2
109 | assert len(event.required_attendees) == 1
110 | assert len(event.optional_attendees) == 1
111 | assert event.required_attendees[0].email == PERSON_REQUIRED_ACCEPTED.email
112 | assert event.optional_attendees[0].email == PERSON_OPTIONAL_ACCEPTED.email
113 |
114 | def test_can_add_resources(self):
115 | event = self.calendar.event(resources=[RESOURCE.email])
116 | assert len(event.resources) == 1
117 | assert event.resources[0].email == RESOURCE.email
118 | assert event.conference_room.email == RESOURCE.email
119 |
120 |
121 | class Test_CreatingANewEvent(unittest.TestCase):
122 | service = None
123 | event = None
124 |
125 | @classmethod
126 | def setUpClass(cls):
127 | cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
128 |
129 | def setUp(self):
130 | self.event = self.service.calendar().event(start=TEST_EVENT.start, end=TEST_EVENT.end)
131 |
132 | def test_events_must_have_a_start_date(self):
133 | self.event.start = None
134 |
135 | with raises(ValueError):
136 | self.event.create()
137 |
138 | def test_events_must_have_an_end_date(self):
139 | self.event.end = None
140 |
141 | with raises(ValueError):
142 | self.event.create()
143 |
144 | def test_event_end_date_must_come_after_start_date(self):
145 | self.event.start, self.event.end = self.event.end, self.event.start
146 |
147 | with raises(ValueError):
148 | self.event.create()
149 |
150 | def test_attendees_must_have_an_email_address_take1(self):
151 |
152 | with raises(ValueError):
153 | self.event.add_attendees(ExchangeEventAttendee(name="Bomb", email=None, required=True))
154 | self.event.create()
155 |
156 | def test_attendees_must_have_an_email_address_take2(self):
157 |
158 | with raises(ValueError):
159 | self.event.add_attendees([None])
160 | self.event.create()
161 |
162 | def test_event_reminder_must_be_int(self):
163 | self.event.reminder_minutes_before_start = "not an integer"
164 |
165 | with raises(TypeError):
166 | self.event.create()
167 |
168 | def test_event_all_day_must_be_bool(self):
169 | self.event.is_all_day = "not a bool"
170 |
171 | with raises(TypeError):
172 | self.event.create()
173 |
174 | def cant_delete_a_newly_created_event(self):
175 |
176 | with raises(ValueError):
177 | self.event.delete()
178 |
179 | def cant_update_a_newly_created_event(self):
180 |
181 | with raises(ValueError):
182 | self.event.update()
183 |
184 | def cant_resend_invites_for_a_newly_created_event(self):
185 |
186 | with raises(ValueError):
187 | self.event.resend_invitations()
188 |
189 | @httprettified
190 | def test_can_set_subject(self):
191 |
192 | HTTPretty.register_uri(
193 | HTTPretty.POST, FAKE_EXCHANGE_URL,
194 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
195 | content_type='text/xml; charset=utf-8',
196 | )
197 |
198 | self.event.subject = TEST_EVENT.subject
199 | self.event.create()
200 |
201 | assert TEST_EVENT.subject in HTTPretty.last_request.body.decode('utf-8')
202 |
203 | @httprettified
204 | def test_can_set_location(self):
205 |
206 | HTTPretty.register_uri(
207 | HTTPretty.POST, FAKE_EXCHANGE_URL,
208 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
209 | content_type='text/xml; charset=utf-8',
210 | )
211 |
212 | self.event.location = TEST_EVENT.location
213 | self.event.create()
214 |
215 | assert TEST_EVENT.location in HTTPretty.last_request.body.decode('utf-8')
216 |
217 | @httprettified
218 | def test_can_set_html_body(self):
219 |
220 | HTTPretty.register_uri(
221 | HTTPretty.POST, FAKE_EXCHANGE_URL,
222 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
223 | content_type='text/xml; charset=utf-8'
224 | )
225 |
226 | self.event.html_body = TEST_EVENT.body
227 | self.event.create()
228 |
229 | assert TEST_EVENT.body in HTTPretty.last_request.body.decode('utf-8')
230 |
231 | @httprettified
232 | def test_can_set_text_body(self):
233 |
234 | HTTPretty.register_uri(
235 | HTTPretty.POST, FAKE_EXCHANGE_URL,
236 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
237 | content_type='text/xml; charset=utf-8',
238 | )
239 |
240 | self.event.text_body = TEST_EVENT.body
241 | self.event.create()
242 |
243 | assert TEST_EVENT.body in HTTPretty.last_request.body.decode('utf-8')
244 |
245 | @httprettified
246 | def test_start_time(self):
247 |
248 | HTTPretty.register_uri(
249 | HTTPretty.POST, FAKE_EXCHANGE_URL,
250 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
251 | content_type='text/xml; charset=utf-8',
252 | )
253 |
254 | self.event.create()
255 |
256 | assert TEST_EVENT.start.strftime(EXCHANGE_DATE_FORMAT) in HTTPretty.last_request.body.decode('utf-8')
257 |
258 | @httprettified
259 | def test_end_time(self):
260 |
261 | HTTPretty.register_uri(
262 | HTTPretty.POST, FAKE_EXCHANGE_URL,
263 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
264 | content_type='text/xml; charset=utf-8',
265 | )
266 |
267 | self.event.create()
268 |
269 | assert TEST_EVENT.end.strftime(EXCHANGE_DATE_FORMAT) in HTTPretty.last_request.body.decode('utf-8')
270 |
271 | @httprettified
272 | def test_attendees(self):
273 |
274 | HTTPretty.register_uri(
275 | HTTPretty.POST, FAKE_EXCHANGE_URL,
276 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
277 | content_type='text/xml; charset=utf-8',
278 | )
279 |
280 | attendees = [PERSON_REQUIRED_ACCEPTED.email, PERSON_REQUIRED_TENTATIVE.email]
281 |
282 | self.event.attendees = attendees
283 | self.event.create()
284 |
285 | for email in attendees:
286 | assert email in HTTPretty.last_request.body.decode('utf-8')
287 |
288 | def test_resources_must_have_an_email_address(self):
289 |
290 | HTTPretty.register_uri(
291 | HTTPretty.POST, FAKE_EXCHANGE_URL,
292 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
293 | content_type='text/xml; charset=utf-8',
294 | )
295 |
296 | attendees = [PERSON_WITH_NO_EMAIL_ADDRESS]
297 |
298 | with raises(ValueError):
299 | self.event.attendees = attendees
300 | self.event.create()
301 |
302 | @httprettified
303 | def test_resources(self):
304 |
305 | HTTPretty.register_uri(
306 | HTTPretty.POST, FAKE_EXCHANGE_URL,
307 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
308 | content_type='text/xml; charset=utf-8',
309 | )
310 |
311 | self.event.resources = [RESOURCE.email]
312 | self.event.create()
313 |
314 | assert RESOURCE.email in HTTPretty.last_request.body.decode('utf-8')
315 |
316 |
317 | def test_events_can_be_pickled(self):
318 |
319 | self.event.subject = "events can be pickled"
320 |
321 | pickled_event = pickle.dumps(self.event)
322 | new_event = pickle.loads(pickled_event)
323 |
324 | assert new_event.subject == "events can be pickled"
325 |
326 |
327 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_create_folder.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | from httpretty import HTTPretty, httprettified
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 |
12 | from pyexchange.connection import ExchangeNTLMAuthConnection
13 | from pyexchange.exceptions import *
14 |
15 | from .fixtures import *
16 |
17 |
18 | class Test_PopulatingANewFolder(unittest.TestCase):
19 | """ Tests all the attribute setting works when creating a new folder """
20 | folder = None
21 |
22 | @classmethod
23 | def setUpClass(cls):
24 |
25 | cls.folder = Exchange2010Service(
26 | connection=ExchangeNTLMAuthConnection(
27 | url=FAKE_EXCHANGE_URL,
28 | username=FAKE_EXCHANGE_USERNAME,
29 | password=FAKE_EXCHANGE_PASSWORD
30 | )
31 | ).folder()
32 |
33 | def test_canary(self):
34 | folder = self.folder.new_folder()
35 | assert folder is not None
36 |
37 | def test_folders_created_dont_have_an_id(self):
38 | folder = self.folder.new_folder()
39 | assert folder.id is None
40 |
41 | def test_folder_has_display_name(self):
42 | folder = self.folder.new_folder(display_name=u'Conference Room')
43 | assert folder.display_name == u'Conference Room'
44 |
45 | def test_folder_has_default_folder_type(self):
46 | folder = self.folder.new_folder()
47 | assert folder.folder_type == u'Folder'
48 |
49 | def test_folder_has_calendar_folder_type(self):
50 | folder = self.folder.new_folder(folder_type=u'CalendarFolder')
51 | assert folder.folder_type == u'CalendarFolder'
52 |
53 |
54 | class Test_CreatingANewFolder(unittest.TestCase):
55 | service = None
56 | folder = None
57 |
58 | @classmethod
59 | def setUpClass(cls):
60 | cls.service = Exchange2010Service(
61 | connection=ExchangeNTLMAuthConnection(
62 | url=FAKE_EXCHANGE_URL,
63 | username=FAKE_EXCHANGE_USERNAME,
64 | password=FAKE_EXCHANGE_PASSWORD,
65 | )
66 | )
67 |
68 | def setUp(self):
69 | self.folder = self.service.folder().new_folder()
70 |
71 | def test_folders_must_have_a_display_name(self):
72 | self.parent_id = u'AQASAGFyMTY2AUB0eHN0YXRlLmVkdQAuAAADXToP9jZJ50ix6mBloAoUtQEAIXy9HV1hQUKHHMQm+PlY6QINNPfbUQAAAA=='
73 |
74 | with raises(AttributeError):
75 | self.folder.create()
76 |
77 |
78 | def test_folders_must_have_a_parent_id(self):
79 | self.folder.display_name = u'Conference Room'
80 | self.parent_id = None
81 |
82 | with raises(ValueError):
83 | self.folder.create()
84 |
85 | def cant_delete_an_uncreated_folder(self):
86 | with raises(TypeError):
87 | self.folder.delete()
88 |
89 | @httprettified
90 | def test_can_set_display_name(self):
91 |
92 | HTTPretty.register_uri(
93 | HTTPretty.POST,
94 | FAKE_EXCHANGE_URL,
95 | body=CREATE_FOLDER_RESPONSE.encode('utf-8'),
96 | content_type='text/xml; charset=utf-8',
97 | )
98 |
99 | self.folder.display_name = TEST_FOLDER.display_name
100 | self.folder.parent_id = TEST_FOLDER.parent_id
101 | self.folder.create()
102 |
103 | assert TEST_FOLDER.display_name in HTTPretty.last_request.body.decode('utf-8')
104 |
105 | @httprettified
106 | def test_can_set_parent_id(self):
107 |
108 | HTTPretty.register_uri(
109 | HTTPretty.POST,
110 | FAKE_EXCHANGE_URL,
111 | body=CREATE_FOLDER_RESPONSE.encode('utf-8'),
112 | content_type='text/xml; charset=utf-8',
113 | )
114 |
115 | self.folder.display_name = TEST_FOLDER.display_name
116 | self.folder.parent_id = TEST_FOLDER.parent_id
117 | self.folder.create()
118 |
119 | assert TEST_FOLDER.display_name in HTTPretty.last_request.body.decode('utf-8')
120 |
121 | @httprettified
122 | def test_can_set_folder_type(self):
123 |
124 | HTTPretty.register_uri(
125 | HTTPretty.POST,
126 | FAKE_EXCHANGE_URL,
127 | body=CREATE_FOLDER_RESPONSE.encode('utf-8'),
128 | content_type='text/xml; charset=utf-8',
129 | )
130 |
131 | self.folder.display_name = TEST_FOLDER.display_name
132 | self.folder.parent_id = TEST_FOLDER.parent_id
133 | self.folder.folder_type = TEST_FOLDER.folder_type
134 | self.folder.create()
135 |
136 | assert TEST_FOLDER.folder_type in HTTPretty.last_request.body.decode('utf-8')
137 |
138 | @httprettified
139 | def test_can_create(self):
140 |
141 | HTTPretty.register_uri(
142 | HTTPretty.POST,
143 | FAKE_EXCHANGE_URL,
144 | body=CREATE_FOLDER_RESPONSE.encode('utf-8'),
145 | content_type='text/html; charset=utf-8',
146 | )
147 |
148 | self.folder.display_name = TEST_FOLDER.display_name
149 | self.folder.parent_id = TEST_FOLDER.parent_id
150 | self.folder.folder_type = TEST_FOLDER.folder_type
151 | self.folder.create()
152 |
153 | assert self.folder.id == TEST_FOLDER.id
154 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_create_recurring_event.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | from pytest import raises
9 | from httpretty import HTTPretty, httprettified
10 | from pyexchange import Exchange2010Service
11 |
12 | from pyexchange.connection import ExchangeNTLMAuthConnection
13 | from pyexchange.exceptions import * # noqa
14 |
15 | from .fixtures import * # noqa
16 |
17 |
18 | class Test_PopulatingANewRecurringDailyEvent(unittest.TestCase):
19 | """ Tests all the attribute setting works when creating a new event """
20 | calendar = None
21 |
22 | @classmethod
23 | def setUpClass(cls):
24 |
25 | cls.calendar = Exchange2010Service(
26 | connection=ExchangeNTLMAuthConnection(
27 | url=FAKE_EXCHANGE_URL,
28 | username=FAKE_EXCHANGE_USERNAME,
29 | password=FAKE_EXCHANGE_PASSWORD,
30 | )
31 | ).calendar()
32 |
33 | def test_can_set_recurring(self):
34 | event = self.calendar.event(
35 | recurrence_interval=TEST_RECURRING_EVENT_DAILY.recurrence_interval,
36 | recurrence_end_date=TEST_RECURRING_EVENT_DAILY.recurrence_end_date,
37 | )
38 | assert event.recurrence_interval == TEST_RECURRING_EVENT_DAILY.recurrence_interval
39 | assert event.recurrence_end_date == TEST_RECURRING_EVENT_DAILY.recurrence_end_date
40 |
41 |
42 | class Test_CreatingANewRecurringDailyEvent(unittest.TestCase):
43 | service = None
44 | event = None
45 |
46 | @classmethod
47 | def setUpClass(cls):
48 | cls.service = Exchange2010Service(
49 | connection=ExchangeNTLMAuthConnection(
50 | url=FAKE_EXCHANGE_URL,
51 | username=FAKE_EXCHANGE_USERNAME,
52 | password=FAKE_EXCHANGE_PASSWORD
53 | )
54 | )
55 |
56 | def setUp(self):
57 | self.event = self.service.calendar().event(
58 | subject=TEST_RECURRING_EVENT_DAILY.subject,
59 | start=TEST_RECURRING_EVENT_DAILY.start,
60 | end=TEST_RECURRING_EVENT_DAILY.end,
61 | recurrence='daily',
62 | recurrence_interval=TEST_RECURRING_EVENT_DAILY.recurrence_interval,
63 | recurrence_end_date=TEST_RECURRING_EVENT_DAILY.recurrence_end_date,
64 | )
65 |
66 | def test_recurrence_must_have_interval(self):
67 | self.event.recurrence_interval = None
68 | with raises(ValueError):
69 | self.event.create()
70 |
71 | def test_recurrence_interval_low_value(self):
72 | self.event.recurrence_interval = 0
73 | with raises(ValueError):
74 | self.event.create()
75 |
76 | def test_recurrence_interval_high_value(self):
77 | self.event.recurrence_interval = 1000
78 | with raises(ValueError):
79 | self.event.create()
80 |
81 | @httprettified
82 | def test_recurrence_interval_min_value(self):
83 | HTTPretty.register_uri(
84 | HTTPretty.POST, FAKE_EXCHANGE_URL,
85 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
86 | content_type='text/xml; charset=utf-8',
87 | )
88 | self.event.recurrence_interval = 1
89 | self.event.create()
90 | assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
91 |
92 | @httprettified
93 | def test_recurrence_interval_max_value(self):
94 | HTTPretty.register_uri(
95 | HTTPretty.POST, FAKE_EXCHANGE_URL,
96 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
97 | content_type='text/xml; charset=utf-8',
98 | )
99 | self.event.recurrence_interval = 999
100 | self.event.create()
101 | assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
102 |
103 | def test_recurrence_must_have_end_date(self):
104 | self.event.recurrence_end_date = None
105 | with raises(ValueError):
106 | self.event.create()
107 |
108 | def test_recurrence_end_before_start(self):
109 | self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
110 | with raises(ValueError):
111 | self.event.create()
112 |
113 | @httprettified
114 | def test_create_recurrence_daily(self):
115 | HTTPretty.register_uri(
116 | HTTPretty.POST, FAKE_EXCHANGE_URL,
117 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
118 | content_type='text/xml; charset=utf-8',
119 | )
120 | self.event.create()
121 | assert self.event.id == TEST_RECURRING_EVENT_DAILY.id
122 |
123 |
124 | class Test_PopulatingANewRecurringWeeklyEvent(unittest.TestCase):
125 | """ Tests all the attribute setting works when creating a new event """
126 | calendar = None
127 |
128 | @classmethod
129 | def setUpClass(cls):
130 |
131 | cls.calendar = Exchange2010Service(
132 | connection=ExchangeNTLMAuthConnection(
133 | url=FAKE_EXCHANGE_URL,
134 | username=FAKE_EXCHANGE_USERNAME,
135 | password=FAKE_EXCHANGE_PASSWORD,
136 | )
137 | ).calendar()
138 |
139 | def test_can_set_recurring(self):
140 | event = self.calendar.event(
141 | recurrence_interval=TEST_RECURRING_EVENT_WEEKLY.recurrence_interval,
142 | recurrence_end_date=TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date,
143 | recurrence_days=TEST_RECURRING_EVENT_WEEKLY.recurrence_days,
144 | )
145 | assert event.recurrence_interval == TEST_RECURRING_EVENT_WEEKLY.recurrence_interval
146 | assert event.recurrence_end_date == TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date
147 | assert event.recurrence_days == TEST_RECURRING_EVENT_WEEKLY.recurrence_days
148 |
149 |
150 | class Test_CreatingANewRecurringWeeklyEvent(unittest.TestCase):
151 | service = None
152 | event = None
153 |
154 | @classmethod
155 | def setUpClass(cls):
156 | cls.service = Exchange2010Service(
157 | connection=ExchangeNTLMAuthConnection(
158 | url=FAKE_EXCHANGE_URL,
159 | username=FAKE_EXCHANGE_USERNAME,
160 | password=FAKE_EXCHANGE_PASSWORD
161 | )
162 | )
163 |
164 | def setUp(self):
165 | self.event = self.service.calendar().event(
166 | subject=TEST_RECURRING_EVENT_WEEKLY.subject,
167 | start=TEST_RECURRING_EVENT_WEEKLY.start,
168 | end=TEST_RECURRING_EVENT_WEEKLY.end,
169 | recurrence='weekly',
170 | recurrence_interval=TEST_RECURRING_EVENT_WEEKLY.recurrence_interval,
171 | recurrence_end_date=TEST_RECURRING_EVENT_WEEKLY.recurrence_end_date,
172 | recurrence_days=TEST_RECURRING_EVENT_WEEKLY.recurrence_days,
173 | )
174 |
175 | def test_recurrence_must_have_interval(self):
176 | self.event.recurrence_interval = None
177 | with raises(ValueError):
178 | self.event.create()
179 |
180 | def test_recurrence_interval_low_value(self):
181 | self.event.recurrence_interval = 0
182 | with raises(ValueError):
183 | self.event.create()
184 |
185 | def test_recurrence_interval_high_value(self):
186 | self.event.recurrence_interval = 100
187 | with raises(ValueError):
188 | self.event.create()
189 |
190 | @httprettified
191 | def test_recurrence_interval_min_value(self):
192 | HTTPretty.register_uri(
193 | HTTPretty.POST, FAKE_EXCHANGE_URL,
194 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
195 | content_type='text/xml; charset=utf-8',
196 | )
197 | self.event.recurrence_interval = 1
198 | self.event.create()
199 | assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
200 |
201 | @httprettified
202 | def test_recurrence_interval_max_value(self):
203 | HTTPretty.register_uri(
204 | HTTPretty.POST, FAKE_EXCHANGE_URL,
205 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
206 | content_type='text/xml; charset=utf-8',
207 | )
208 | self.event.recurrence_interval = 99
209 | self.event.create()
210 | assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
211 |
212 | def test_recurrence_must_have_end_date(self):
213 | self.event.recurrence_end_date = None
214 | with raises(ValueError):
215 | self.event.create()
216 |
217 | def test_recurrence_end_before_start(self):
218 | self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
219 | with raises(ValueError):
220 | self.event.create()
221 |
222 | def test_recurrence_bad_days(self):
223 | self.event.recurrence_days = 'Mondays'
224 | with raises(ValueError):
225 | self.event.create()
226 |
227 | def test_recurrence_no_days(self):
228 | self.event.recurrence_days = None
229 | with raises(ValueError):
230 | self.event.create()
231 |
232 | @httprettified
233 | def test_create_recurrence_weekly(self):
234 | HTTPretty.register_uri(
235 | HTTPretty.POST, FAKE_EXCHANGE_URL,
236 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
237 | content_type='text/xml; charset=utf-8',
238 | )
239 | self.event.create()
240 | assert self.event.id == TEST_RECURRING_EVENT_WEEKLY.id
241 |
242 |
243 | class Test_PopulatingANewRecurringMonthlyEvent(unittest.TestCase):
244 | """ Tests all the attribute setting works when creating a new event """
245 | calendar = None
246 |
247 | @classmethod
248 | def setUpClass(cls):
249 |
250 | cls.calendar = Exchange2010Service(
251 | connection=ExchangeNTLMAuthConnection(
252 | url=FAKE_EXCHANGE_URL,
253 | username=FAKE_EXCHANGE_USERNAME,
254 | password=FAKE_EXCHANGE_PASSWORD,
255 | )
256 | ).calendar()
257 |
258 | def test_can_set_recurring(self):
259 | event = self.calendar.event(
260 | recurrence='monthly',
261 | recurrence_interval=TEST_RECURRING_EVENT_MONTHLY.recurrence_interval,
262 | recurrence_end_date=TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date,
263 | )
264 | assert event.recurrence_interval == TEST_RECURRING_EVENT_MONTHLY.recurrence_interval
265 | assert event.recurrence_end_date == TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date
266 |
267 |
268 | class Test_CreatingANewRecurringMonthlyEvent(unittest.TestCase):
269 | service = None
270 | event = None
271 |
272 | @classmethod
273 | def setUpClass(cls):
274 | cls.service = Exchange2010Service(
275 | connection=ExchangeNTLMAuthConnection(
276 | url=FAKE_EXCHANGE_URL,
277 | username=FAKE_EXCHANGE_USERNAME,
278 | password=FAKE_EXCHANGE_PASSWORD
279 | )
280 | )
281 |
282 | def setUp(self):
283 | self.event = self.service.calendar().event(
284 | subject=TEST_RECURRING_EVENT_MONTHLY.subject,
285 | start=TEST_RECURRING_EVENT_MONTHLY.start,
286 | end=TEST_RECURRING_EVENT_MONTHLY.end,
287 | recurrence='monthly',
288 | recurrence_interval=TEST_RECURRING_EVENT_MONTHLY.recurrence_interval,
289 | recurrence_end_date=TEST_RECURRING_EVENT_MONTHLY.recurrence_end_date,
290 | )
291 |
292 | def test_recurrence_must_have_interval(self):
293 | self.event.recurrence_interval = None
294 | with raises(ValueError):
295 | self.event.create()
296 |
297 | def test_recurrence_interval_low_value(self):
298 | self.event.recurrence_interval = 0
299 | with raises(ValueError):
300 | self.event.create()
301 |
302 | def test_recurrence_interval_high_value(self):
303 | self.event.recurrence_interval = 100
304 | with raises(ValueError):
305 | self.event.create()
306 |
307 | @httprettified
308 | def test_recurrence_interval_min_value(self):
309 | HTTPretty.register_uri(
310 | HTTPretty.POST, FAKE_EXCHANGE_URL,
311 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
312 | content_type='text/xml; charset=utf-8',
313 | )
314 | self.event.recurrence_interval = 1
315 | self.event.create()
316 | assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
317 |
318 | @httprettified
319 | def test_recurrence_interval_max_value(self):
320 | HTTPretty.register_uri(
321 | HTTPretty.POST, FAKE_EXCHANGE_URL,
322 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
323 | content_type='text/xml; charset=utf-8',
324 | )
325 | self.event.recurrence_interval = 99
326 | self.event.create()
327 | assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
328 |
329 | def test_recurrence_must_have_end_date(self):
330 | self.event.recurrence_end_date = None
331 | with raises(ValueError):
332 | self.event.create()
333 |
334 | def test_recurrence_end_before_start(self):
335 | self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
336 | with raises(ValueError):
337 | self.event.create()
338 |
339 | @httprettified
340 | def test_create_recurrence_monthly(self):
341 | HTTPretty.register_uri(
342 | HTTPretty.POST, FAKE_EXCHANGE_URL,
343 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
344 | content_type='text/xml; charset=utf-8',
345 | )
346 | self.event.create()
347 | assert self.event.id == TEST_RECURRING_EVENT_MONTHLY.id
348 |
349 |
350 | class Test_PopulatingANewRecurringYearlyEvent(unittest.TestCase):
351 | """ Tests all the attribute setting works when creating a new event """
352 | calendar = None
353 |
354 | @classmethod
355 | def setUpClass(cls):
356 |
357 | cls.calendar = Exchange2010Service(
358 | connection=ExchangeNTLMAuthConnection(
359 | url=FAKE_EXCHANGE_URL,
360 | username=FAKE_EXCHANGE_USERNAME,
361 | password=FAKE_EXCHANGE_PASSWORD,
362 | )
363 | ).calendar()
364 |
365 | def test_can_set_recurring(self):
366 | event = self.calendar.event(
367 | recurrence='yearly',
368 | recurrence_end_date=TEST_RECURRING_EVENT_YEARLY.recurrence_end_date,
369 | )
370 | event.recurrence_end_date == TEST_RECURRING_EVENT_YEARLY.recurrence_end_date
371 |
372 |
373 | class Test_CreatingANewRecurringYearlyEvent(unittest.TestCase):
374 | service = None
375 | event = None
376 |
377 | @classmethod
378 | def setUpClass(cls):
379 | cls.service = Exchange2010Service(
380 | connection=ExchangeNTLMAuthConnection(
381 | url=FAKE_EXCHANGE_URL,
382 | username=FAKE_EXCHANGE_USERNAME,
383 | password=FAKE_EXCHANGE_PASSWORD
384 | )
385 | )
386 |
387 | def setUp(self):
388 | self.event = self.service.calendar().event(
389 | subject=TEST_RECURRING_EVENT_YEARLY.subject,
390 | start=TEST_RECURRING_EVENT_YEARLY.start,
391 | end=TEST_RECURRING_EVENT_YEARLY.end,
392 | recurrence='yearly',
393 | recurrence_end_date=TEST_RECURRING_EVENT_YEARLY.recurrence_end_date,
394 | )
395 |
396 | def test_recurrence_must_have_end_date(self):
397 | self.event.recurrence_end_date = None
398 | with raises(ValueError):
399 | self.event.create()
400 |
401 | def test_recurrence_end_before_start(self):
402 | self.event.recurrence_end_date = self.event.start.date() - timedelta(1)
403 | with raises(ValueError):
404 | self.event.create()
405 |
406 | @httprettified
407 | def test_create_recurrence_yearly(self):
408 | HTTPretty.register_uri(
409 | HTTPretty.POST, FAKE_EXCHANGE_URL,
410 | body=CREATE_ITEM_RESPONSE.encode('utf-8'),
411 | content_type='text/xml; charset=utf-8',
412 | )
413 | self.event.create()
414 | assert self.event.id == TEST_RECURRING_EVENT_YEARLY.id
415 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_delete_event.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | import httpretty
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 | from pyexchange.connection import ExchangeNTLMAuthConnection
12 |
13 | from .fixtures import *
14 |
15 | class Test_EventDeletion(unittest.TestCase):
16 | event = None
17 |
18 | @classmethod
19 | def setUpClass(cls):
20 | cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
21 |
22 |
23 | 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')
24 | cls.delete_event_response = httpretty.Response(body=DELETE_ITEM_RESPONSE.encode('utf-8'), status=200, content_type='text/xml; charset=utf-8')
25 |
26 | @httpretty.activate
27 | def setUp(self):
28 |
29 | httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
30 | body=GET_ITEM_RESPONSE.encode('utf-8'),
31 | content_type='text/xml; charset=utf-8')
32 |
33 | self.event = self.service.calendar().get_event(id=TEST_EVENT.id)
34 |
35 |
36 | @httpretty.activate
37 | def test_can_cancel_event(self):
38 | httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
39 | responses=[
40 | self.get_change_key_response,
41 | self.delete_event_response,
42 | ])
43 |
44 | response = self.event.cancel()
45 | assert response is None
46 |
47 |
48 | @httpretty.activate
49 | def test_cant_cancel_an_event_with_no_exchange_id(self):
50 | httpretty.register_uri(httpretty.POST, FAKE_EXCHANGE_URL,
51 | responses=[
52 | self.get_change_key_response,
53 | self.delete_event_response,
54 | ])
55 | unsaved_event = self.service.calendar().event()
56 |
57 | with raises(TypeError):
58 | unsaved_event.cancel() #bzzt - can't do this
59 |
60 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_delete_folder.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | import httpretty
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 | from pyexchange.connection import ExchangeNTLMAuthConnection
12 |
13 | from .fixtures import *
14 |
15 |
16 | class Test_FolderDeletion(unittest.TestCase):
17 | folder = None
18 |
19 | @classmethod
20 | def setUpClass(cls):
21 | cls.service = Exchange2010Service(
22 | connection=ExchangeNTLMAuthConnection(
23 | url=FAKE_EXCHANGE_URL,
24 | username=FAKE_EXCHANGE_USERNAME,
25 | password=FAKE_EXCHANGE_PASSWORD,
26 | )
27 | )
28 |
29 | cls.get_change_key_response = httpretty.Response(
30 | body=GET_FOLDER_RESPONSE.encode('utf-8'),
31 | status=200,
32 | content_type='text/xml; charset=utf-8'
33 | )
34 | cls.delete_folder_response = httpretty.Response(
35 | body=DELETE_FOLDER_RESPONSE.encode('utf-8'),
36 | status=200,
37 | content_type='text/xml; charset=utf-8'
38 | )
39 |
40 | @httpretty.activate
41 | def setUp(self):
42 |
43 | httpretty.register_uri(
44 | httpretty.POST,
45 | FAKE_EXCHANGE_URL,
46 | body=GET_FOLDER_RESPONSE.encode('utf-8'),
47 | content_type='text/xml; charset=utf-8',
48 | )
49 |
50 | self.folder = self.service.folder().get_folder(id=TEST_FOLDER.id)
51 |
52 | @httpretty.activate
53 | def test_can_delete_folder(self):
54 | httpretty.register_uri(
55 | httpretty.POST,
56 | FAKE_EXCHANGE_URL,
57 | responses=[
58 | self.get_change_key_response,
59 | self.delete_folder_response,
60 | ]
61 | )
62 |
63 | response = self.folder.delete()
64 | assert response is None
65 |
66 | @httpretty.activate
67 | def test_cant_delete_a_uncreated_folder(self):
68 | httpretty.register_uri(
69 | httpretty.POST,
70 | FAKE_EXCHANGE_URL,
71 | responses=[
72 | self.get_change_key_response,
73 | self.delete_folder_response,
74 | ]
75 | )
76 | unsaved_folder = self.service.folder().new_folder()
77 |
78 | with raises(TypeError):
79 | unsaved_folder.delete() # bzzt - can't do this
80 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_event_actions.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | from httpretty import HTTPretty, httprettified
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 | from pyexchange.connection import ExchangeNTLMAuthConnection
12 | from pyexchange.exceptions import *
13 |
14 | from .fixtures import *
15 |
16 |
17 | class Test_EventActions(unittest.TestCase):
18 | event = None
19 |
20 | @classmethod
21 | def setUpClass(cls):
22 | cls.service = Exchange2010Service(connection=ExchangeNTLMAuthConnection(url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD))
23 | 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')
24 | cls.update_event_response = HTTPretty.Response(body=UPDATE_ITEM_RESPONSE.encode('utf-8'), status=200, content_type='text/xml; charset=utf-8')
25 |
26 |
27 | @httprettified
28 | def setUp(self):
29 | HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
30 | body=GET_ITEM_RESPONSE.encode('utf-8'),
31 | content_type='text/xml; charset=utf-8')
32 |
33 | self.event = self.service.calendar().get_event(id=TEST_EVENT.id)
34 |
35 |
36 | @httprettified
37 | def test_resend_invites(self):
38 | HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
39 | responses=[
40 | self.get_change_key_response,
41 | self.update_event_response,
42 | ])
43 | self.event.resend_invitations()
44 |
45 | assert TEST_EVENT.change_key in HTTPretty.last_request.body.decode('utf-8')
46 | assert TEST_EVENT.subject not in HTTPretty.last_request.body.decode('utf-8')
47 |
48 | @httprettified
49 | def test_cant_resend_invites_on_a_modified_event(self):
50 | HTTPretty.register_uri(HTTPretty.POST, FAKE_EXCHANGE_URL,
51 | responses=[
52 | self.get_change_key_response,
53 | self.update_event_response,
54 | ])
55 |
56 | self.event.subject = u'New event thing'
57 |
58 | with raises(ValueError):
59 | self.event.resend_invitations()
60 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_exchange_service.py:
--------------------------------------------------------------------------------
1 | __author__ = 'rsanders'
2 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_find_folder.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | import unittest
8 | import httpretty
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 | from pyexchange.connection import ExchangeNTLMAuthConnection
12 | from pyexchange.exceptions import *
13 |
14 | from .fixtures import *
15 |
16 |
17 | class Test_ParseFolderResponseData(unittest.TestCase):
18 | folder = None
19 |
20 | @classmethod
21 | def setUpClass(cls):
22 |
23 | @httpretty.activate # this decorator doesn't play nice with @classmethod
24 | def fake_folder_request():
25 |
26 | service = Exchange2010Service(
27 | connection=ExchangeNTLMAuthConnection(
28 | url=FAKE_EXCHANGE_URL,
29 | username=FAKE_EXCHANGE_USERNAME,
30 | password=FAKE_EXCHANGE_PASSWORD,
31 | )
32 | )
33 |
34 | httpretty.register_uri(
35 | httpretty.POST,
36 | FAKE_EXCHANGE_URL,
37 | body=GET_FOLDER_RESPONSE.encode('utf-8'),
38 | content_type='text/xml; charset=utf-8',
39 | )
40 |
41 | return service.folder().find_folder(parent_id=TEST_FOLDER.id)
42 |
43 | cls.folder = fake_folder_request()
44 |
45 | def test_canary(self):
46 | for folder in self.folder:
47 | assert folder is not None
48 |
49 | def test_folder_has_a_name(self):
50 | for folder in self.folder:
51 | assert folder is not None
52 |
53 | def test_folder_has_a_parent(self):
54 | for folder in self.folder:
55 | assert folder.parent_id == TEST_FOLDER.id
56 |
57 | def test_folder_type(self):
58 | for folder in self.folder:
59 | assert folder is not None
60 |
61 |
62 | class Test_FailingToGetFolders(unittest.TestCase):
63 |
64 | service = None
65 |
66 | @classmethod
67 | def setUpClass(cls):
68 |
69 | cls.service = Exchange2010Service(
70 | connection=ExchangeNTLMAuthConnection(
71 | url=FAKE_EXCHANGE_URL,
72 | username=FAKE_EXCHANGE_USERNAME,
73 | password=FAKE_EXCHANGE_PASSWORD
74 | )
75 | )
76 |
77 | @httpretty.activate
78 | def test_requesting_an_folder_id_that_doest_exist_throws_exception(self):
79 |
80 | httpretty.register_uri(
81 | httpretty.POST, FAKE_EXCHANGE_URL,
82 | body=FOLDER_DOES_NOT_EXIST.encode('utf-8'),
83 | content_type='text/xml; charset=utf-8',
84 | )
85 |
86 | with raises(ExchangeItemNotFoundException):
87 | self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
88 |
89 | @httpretty.activate
90 | def test_requesting_an_folder_and_getting_a_500_response_throws_exception(self):
91 |
92 | httpretty.register_uri(
93 | httpretty.POST,
94 | FAKE_EXCHANGE_URL,
95 | body=u"",
96 | status=500,
97 | content_type='text/xml; charset=utf-8',
98 | )
99 |
100 | with raises(FailedExchangeException):
101 | self.service.folder().find_folder(parent_id=TEST_FOLDER.id)
102 |
--------------------------------------------------------------------------------
/tests/exchange2010/test_get_event.py:
--------------------------------------------------------------------------------
1 | """
2 | (c) 2013 LinkedIn Corp. All rights reserved.
3 | 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
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.
6 | """
7 | from httpretty import HTTPretty, httprettified, activate
8 | import unittest
9 | from pytest import raises
10 | from pyexchange import Exchange2010Service
11 | from pyexchange.connection import ExchangeNTLMAuthConnection
12 | from pyexchange.exceptions import * # noqa
13 |
14 | from .fixtures import * # noqa
15 |
16 |
17 | class Test_ParseEventResponseData(unittest.TestCase):
18 | event = None
19 |
20 | @classmethod
21 | def setUpClass(cls):
22 |
23 | @activate # this decorator doesn't play nice with @classmethod
24 | def fake_event_request():
25 |
26 | service = Exchange2010Service(
27 | connection=ExchangeNTLMAuthConnection(
28 | url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD
29 | )
30 | )
31 |
32 | HTTPretty.register_uri(
33 | HTTPretty.POST, FAKE_EXCHANGE_URL,
34 | body=GET_ITEM_RESPONSE.encode('utf-8'),
35 | content_type='text/xml; charset=utf-8',
36 | )
37 |
38 | return service.calendar().get_event(id=TEST_EVENT.id)
39 |
40 | cls.event = fake_event_request()
41 |
42 | def test_canary(self):
43 | assert self.event is not None
44 |
45 | def test_event_id_was_not_changed(self):
46 | assert self.event.id == TEST_EVENT.id
47 |
48 | def test_event_has_a_subject(self):
49 | assert self.event.subject == TEST_EVENT.subject
50 |
51 | def test_event_has_a_location(self):
52 | assert self.event.location == TEST_EVENT.location
53 |
54 | def test_event_has_a_body(self):
55 | assert self.event.html_body == TEST_EVENT.body
56 | assert self.event.text_body == TEST_EVENT.body
57 | assert self.event.body == TEST_EVENT.body
58 |
59 | def test_event_starts_at_the_right_time(self):
60 | assert self.event.start == TEST_EVENT.start
61 |
62 | def test_event_ends_at_the_right_time(self):
63 | assert self.event.end == TEST_EVENT.end
64 |
65 | def test_event_has_an_organizer(self):
66 | assert self.event.organizer is not None
67 | assert self.event.organizer.name == ORGANIZER.name
68 | assert self.event.organizer.email == ORGANIZER.email
69 |
70 | def test_event_has_the_correct_attendees(self):
71 | assert len(self.event.attendees) > 0
72 | assert len(self.event.attendees) == len(ATTENDEE_LIST)
73 |
74 | def _test_person_values_are_correct(self, fixture):
75 |
76 | try:
77 | self.event.attendees.index(fixture)
78 | except ValueError as e:
79 | print(u"An attendee should be in the list but isn't:", fixture)
80 | raise e
81 |
82 | def test_all_attendees_are_present_and_accounted_for(self):
83 |
84 | # this is a nose test generator if you haven't seen one before
85 | # it creates one test for each attendee
86 | for attendee in ATTENDEE_LIST:
87 | yield self._test_person_values_are_correct, attendee
88 |
89 | def test_resources_are_correct(self):
90 | assert self.event.resources == [RESOURCE]
91 |
92 | def test_conference_room_alias(self):
93 | assert self.event.conference_room == RESOURCE
94 |
95 | def test_required_attendees_are_required(self):
96 | assert sorted(self.event.required_attendees) == sorted(REQUIRED_PEOPLE)
97 |
98 | def test_optional_attendees_are_optional(self):
99 | assert sorted(self.event.optional_attendees) == sorted(OPTIONAL_PEOPLE)
100 |
101 | def test_conflicting_event_ids(self):
102 | assert self.event.conflicting_event_ids[0] == TEST_CONFLICT_EVENT.id
103 |
104 | @httprettified
105 | def test_conflicting_events(self):
106 | HTTPretty.register_uri(
107 | HTTPretty.POST, FAKE_EXCHANGE_URL,
108 | body=CONFLICTING_EVENTS_RESPONSE.encode('utf-8'),
109 | content_type='text/xml; charset=utf-8',
110 | )
111 | conflicting_events = self.event.conflicting_events()
112 | assert conflicting_events[0].id == TEST_CONFLICT_EVENT.id
113 | assert conflicting_events[0].calendar_id == TEST_CONFLICT_EVENT.calendar_id
114 | assert conflicting_events[0].subject == TEST_CONFLICT_EVENT.subject
115 | assert conflicting_events[0].location == TEST_CONFLICT_EVENT.location
116 | assert conflicting_events[0].start == TEST_CONFLICT_EVENT.start
117 | assert conflicting_events[0].end == TEST_CONFLICT_EVENT.end
118 | assert conflicting_events[0].body == TEST_CONFLICT_EVENT.body
119 | assert conflicting_events[0].conflicting_event_ids[0] == TEST_EVENT.id
120 |
121 |
122 | class Test_FailingToGetEvents(unittest.TestCase):
123 |
124 | service = None
125 |
126 | @classmethod
127 | def setUpClass(cls):
128 |
129 | cls.service = Exchange2010Service(
130 | connection=ExchangeNTLMAuthConnection(
131 | url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD
132 | )
133 | )
134 |
135 | @activate
136 | def test_requesting_an_event_id_that_doest_exist_throws_exception(self):
137 |
138 | HTTPretty.register_uri(
139 | HTTPretty.POST, FAKE_EXCHANGE_URL,
140 | body=ITEM_DOES_NOT_EXIST.encode('utf-8'),
141 | content_type='text/xml; charset=utf-8',
142 | )
143 |
144 | with raises(ExchangeItemNotFoundException):
145 | self.service.calendar().get_event(id=TEST_EVENT.id)
146 |
147 | @activate
148 | def test_requesting_an_event_and_getting_a_500_response_throws_exception(self):
149 |
150 | HTTPretty.register_uri(
151 | HTTPretty.POST, FAKE_EXCHANGE_URL,
152 | body=u"",
153 | status=500,
154 | content_type='text/xml; charset=utf-8',
155 | )
156 |
157 | with raises(FailedExchangeException):
158 | self.service.calendar().get_event(id=TEST_EVENT.id)
159 |
160 | @activate
161 | def test_requesting_an_event_and_getting_garbage_xml_throws_exception(self):
162 |
163 | HTTPretty.register_uri(
164 | HTTPretty.POST, FAKE_EXCHANGE_URL,
165 | body=u"