{ "info": { "author": "Janscas", "author_email": "janscas@users.noreply.github.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Topic :: Office/Business :: Office Suites", "Topic :: Software Development :: Libraries" ], "description": "# pyo365 - Microsoft Graph and Office 365 API made easy\n\nThis project aims is to make it easy to interact with Microsoft Graph and Office 365 Email, Contacts, Calendar, OneDrive, etc.\n\nThis project is inspired on the super work done by [Toben Archer](https://github.com/Narcolapser) [Python-O365](https://github.com/Narcolapser/python-o365).\nThe oauth part is based on the work done by [Royce Melborn](https://github.com/roycem90) which is now integrated with the original project.\n\nI just want to make this project different in almost every sense, and make it also more pythonic.\nSo I ended up rewriting the whole project from scratch.\n\nThe result is a package that provides a lot of the Microsoft Graph and Office 365 API capabilities.\n\nThis is for example how you send a message:\n\n```python\nfrom pyo365 import Account\n\ncredentials = ('client_id', 'client_secret')\n\naccount = Account(credentials)\nm = account.new_message()\nm.to.add('to_example@example.com')\nm.subject = 'Testing!'\nm.body = \"George Best quote: I've stopped drinking, but only while I'm asleep.\"\nm.send()\n```\n\n\n**Python 3.4 is the minimum required**... I was very tempted to just go for 3.6 and use f-strings. Those are fantastic!\n\nThis project was also a learning resource for me. This is a list of not so common python characteristics used in this project:\n- New unpacking technics: `def method(argument, *, with_name=None, **other_params):`\n- Enums: `from enum import Enum`\n- Factory paradigm\n- Package organization\n- Timezone conversion and timezone aware datetimes\n- Etc. (see the code!)\n\n> **This project is in early development.** Changes that can break your code may be commited. If you want to help please feel free to fork and make pull requests.\n\n\nWhat follows is kind of a wiki... but you will get more insights by looking at the code.\n\n## Table of contents\n\n- [Install](#install)\n- [Protocols](#protocols)\n- [Authentication](#authentication)\n- [Account Class and Modularity](#account)\n- [MailBox](#mailbox)\n- [AddressBook](#addressbook)\n- [Calendar](#calendar)\n- [OneDrive](#onedrive)\n- [Sharepoint](#sharepoint)\n- [Utils](#utils)\n\n\n## Install\npyo365 is available on pypi.org. Simply run `pip install pyo365` to install it.\n\nProject dependencies installed by pip:\n - requests\n - requests-oauthlib\n - beatifulsoup4\n - stringcase\n - python-dateutil\n - tzlocal\n - pytz\n\nThe first step to be able to work with this library is to register an application and retrieve the auth token. See [Authentication](#authentication).\n\n## Protocols\nProtocols handles the aspects of comunications between different APIs.\nThis project uses by default either the Office 365 APIs or Microsoft Graph APIs.\nBut, you can use many other Microsoft APIs as long as you implement the protocol needed.\n\nYou can use one or the other:\n\n- `MSGraphProtocol` to use the [Microsoft Graph API](https://developer.microsoft.com/en-us/graph/docs/concepts/overview)\n- `MSOffice365Protocol` to use the [Office 365 API](https://msdn.microsoft.com/en-us/office/office365/api/api-catalog)\n\nBoth protocols are similar but the Graph one has access to more resources (for example OneDrive). It also depends on the api version used.\n\nThe default protocol used by the `Account` Class is `MSGraphProtocol`.\n\nYou can implement your own protocols by inheriting from `Protocol` to communicate with other Microsoft APIs.\n\nYou can instantiate protocols like this:\n```python\nfrom pyo365 import MSGraphProtocol\n\n# try the api version beta of the Microsoft Graph endpoint.\nprotocol = MSGraphProtocol(api_version='beta') # MSGraphProtocol defaults to v1.0 api version\n```\n\n##### Resources:\nEach API endpoint requires a resource. This usually defines the owner of the data.\nEvery protocol defaults to resource 'ME'. 'ME' is the user which has given consent, but you can change this behaviour by providing a different default resource to the protocol constructor.\n\nFor example when accesing a shared mailbox:\n\n\n```python\n# ...\naccount = Account(credentials=my_credentials, main_resource='shared_mailbox@example.com')\n# Any instance created using account will inherit the resource defined for account.\n```\n\nThis can be done however at any point. For example at the protocol level:\n```python\n# ...\nmy_protocol = MSGraphProtocol(default_resource='shared_mailbox@example.com')\n\naccount = Account(credentials=my_credentials, protocol=my_protocol)\n\n# now account is accesing the shared_mailbox@example.com in every api call.\nshared_mailbox_messages = account.mailbox().get_messages()\n```\n\n\n\nInstead of defining the resource used at the account or protocol level, you can provide it per use case as follows:\n```python\n# ...\naccount = Account(credentials=my_credentials) # account defaults to 'ME' resource\n\nmailbox = account.mailbox('shared_mailbox@example.com') # mailbox is using 'shared_mailbox@example.com' resource instead of 'ME'\n\n# or:\n\nmessage = Message(parent=account, main_resource='shared_mailbox@example.com') # message is using 'shared_mailbox@example.com' resource\n```\n\nUsually you will work with the default 'ME' resuorce, but you can also use one of the following:\n\n- **'me'**: the user which has given consent. the default for every protocol.\n- **'user:user@domain.com'**: a shared mailbox or a user account for which you have permissions. If you don't provide 'user:' will be infered anyways.\n- **'sharepoint:sharepoint-site-id'**: a sharepoint site id.\n- **'group:group-site-id'**: a office365 group id. \n\n## Authentication\nYou can only authenticate using oauth athentication as Microsoft deprecated basic oauth on November 1st 2018.\n\n- Oauth authentication: using an authentication token provided after user consent.\n\nThe `Connection` Class handles the authentication.\n\n#### Oauth Authentication\nThis section is explained using Microsoft Graph Protocol, almost the same applies to the Office 365 REST API.\n\n\n##### Permissions and Scopes:\nWhen using oauth you create an application and allow some resources to be accesed and used by it's users.\nThen the user can request access to one or more of this resources by providing scopes to the oauth provider.\n\nFor example your application can have Calendar.Read, Mail.ReadWrite and Mail.Send permissions, but the application can request access only to the Mail.ReadWrite and Mail.Send permission.\nThis is done by providing scopes to the connection object like so:\n```python\nfrom pyo365 import Connection\n\ncredentials = ('client_id', 'client_secret')\n\nscopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send']\n\ncon = Connection(credentials, scopes=scopes)\n```\n\nScope implementation depends on the protocol used. So by using protocol data you can automatically set the scopes needed:\n\nYou can get the same scopes as before using protocols like this:\n\n```python\nprotocol_graph = MSGraphProtocol()\n\nscopes_graph = protocol.get_scopes_for('message all')\n# scopes here are: ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send']\n\nprotocol_office = MSOffice365Protocol()\n\nscopes_office = protocol.get_scopes_for('message all')\n# scopes here are: ['https://outlook.office.com/Mail.ReadWrite', 'https://outlook.office.com/Mail.Send']\n\ncon = Connection(credentials, scopes=scopes_graph)\n```\n\n\n##### Authentication Flow\n1. To work with oauth you first need to register your application at [Microsoft Application Registration Portal](https://apps.dev.microsoft.com/).\n\n 1. Login at [Microsoft Application Registration Portal](https://apps.dev.microsoft.com/)\n 2. Create an app, note your app id (client_id)\n 3. Generate a new password (client_secret) under \"Application Secrets\" section\n 4. Under the \"Platform\" section, add a new Web platform and set \"https://outlook.office365.com/owa/\" as the redirect URL\n 5. Under \"Microsoft Graph Permissions\" section, add the delegated permissions you want (see scopes), as an example, to read and send emails use:\n 1. Mail.ReadWrite\n 2. Mail.Send\n 3. User.Read\n\n2. Then you need to login for the first time to get the access token by consenting the application to access the resources it needs.\n 1. First get the authorization url.\n ```python\n url = account.connection.get_authorization_url()\n ```\n 2. The user must visit this url and give consent to the application. When consent is given, the page will rediret to: \"https://outlook.office365.com/owa/\".\n\n Then the user must copy the resulting page url and give it to the connection object:\n\n ```python\n result_url = input('Paste the result url here...')\n\n account.connection.request_token(result_url) # This, if succesful, will store the token in a txt file on the user project folder.\n ```\n\n Take care, the access token must remain protected from unauthorized users.\n\n 3. At this point you will have an access token that will provide valid credentials when using the api. If you change the scope requested, then the current token won't work, and you will need the user to give consent again on the application to gain access to the new scopes requested.\n\n The access token only lasts 60 minutes, but the app will automatically request new tokens through the refresh tokens, but note that a refresh token only lasts for 90 days. So you must use it before or you will need to request a new access token again (no new consent needed by the user, just a login).\n\n If your application needs to work for more than 90 days without user interaction and without interacting with the API, then you must implement a periodic call to `Connection.refresh_token` before the 90 days have passed.\n\n\n##### Using pyo365 to authenticate\n\nYou can manually authenticate by using a single `Connection` instance as described before or use the helper methods provided by the library.\n\n1. `account.authenticate`:\n\n This is the preferred way for performing authentication.\n\n Create an `Account` instance and authenticate using the `authenticate` method:\n ```python\n from pyo365 import Account\n\n account = Account(credentials=('client_id', 'client_secret'))\n result = account.authenticate(scopes=['basic', 'message_all']) # request a token for this scopes\n\n # this will ask to visit the app consent screen where the user will be asked to give consent on the requested scopes.\n # then the user will have to provide the result url afeter consent. \n # if all goes as expected, result will be True and a token will be stored in the default location.\n ```\n\n2. `oauth_authentication_flow`:\n\n ```python\n from pyo365 import oauth_authentication_flow\n\n result = oauth_authentication_flow('client_id', 'client_secret', ['scopes_required'])\n ```\n\n## Account Class and Modularity \nUsually you will only need to work with the `Account` Class. This is a wrapper around all functionality.\n\nBut you can also work only with the pieces you want.\n\nFor example, instead of:\n```python\nfrom pyo365 import Account\n\naccount = Account(('client_id', 'client_secret'))\nmessage = account.new_message()\n# ...\nmailbox = account.mailbox()\n# ...\n```\n\nYou can work only with the required pieces:\n\n```python\nfrom pyo365 import Connection, MSGraphProtocol, Message, MailBox\n\nmy_protocol = MSGraphProtocol()\ncon = Connection(('client_id', 'client_secret'))\n\nmessage = Message(con=con, protocol=my_protocol)\n# ...\nmailbox = MailBox(con=con, protocol=my_protocol)\nmessage2 = Message(parent=mailbox) # message will inherit the connection and protocol from mailbox when using parent.\n# ...\n```\n\nIt's also easy to implement a custom Class.\n\nJust Inherit from `ApiComponent`, define the endpoints, and use the connection to make requests. If needed also inherit from Protocol to handle different comunications aspects with the API server.\n\n```python\nfrom pyo365.utils import ApiComponent \n\nclass CustomClass(ApiComponent):\n _endpoints = {'my_url_key': '/customendpoint'}\n\n def __init__(self, *, parent=None, con=None, **kwargs):\n super().__init__(parent=parent, con=con, **kwargs)\n # ...\n\n def do_some_stuff(self):\n\n # self.build_url just merges the protocol service_url with the enpoint passed as a parameter\n # to change the service_url implement your own protocol inherinting from Protocol Class\n url = self.build_url(self._endpoints.get('my_url_key')) \n\n my_params = {'param1': 'param1'}\n\n response = self.con.get(url, params=my_params) # note the use of the connection here.\n\n # handle response and return to the user...\n```\n\n## MailBox\nMailbox groups the funcionality of both the messages and the email folders.\n\n```python\nmailbox = account.mailbox()\n\ninbox = mailbox.inbox_folder()\n\nfor message in inbox.get_messages():\n print(message)\n\nsent_folder = mailbox.sent_folder()\n\nfor message in sent_folder.get_messages():\n print(message)\n\nm = mailbox.new_message()\n\nm.to.add('to_example@example.com')\nm.body = 'George Best quote: In 1969 I gave up women and alcohol - it was the worst 20 minutes of my life.'\nm.save_draft()\n```\n\n#### Email Folder\nRepresents a `Folder` within your email mailbox.\n\nYou can get any folder in your mailbox by requesting child folders or filtering by name.\n\n```python\nmailbox = account.mailbox()\n\narchive = mailbox.get_folder(folder_name='archive') # get a folder with 'archive' name\n\nchild_folders = archive.get_folders(25) # get at most 25 child folders of 'archive' folder\n\nfor folder in child_folders:\n print(folder.name, folder.parent_id)\n\nnew_folder = archive.create_child_folder('George Best Quotes')\n```\n\n#### Message\nAn email object with all it's data and methods.\n\nCreating a draft message is as easy as this:\n```python\nmessage = mailbox.new_message()\nmessage.to.add(['example1@example.com', 'example2@example.com'])\nmessage.sender.address = 'my_shared_account@example.com' # changing the from address\nmessage.body = 'George Best quote: I might go to Alcoholics Anonymous, but I think it would be difficult for me to remain anonymous'\nmessage.attachments.add('george_best_quotes.txt')\nmessage.save_draft() # save the message on the cloud as a draft in the drafts folder\n```\n\nWorking with saved emails is also easy:\n```python\nquery = mailbox.new_query().on_attribute('subject').contains('george best') # see Query object in Utils\nmessages = mailbox.get_messages(limit=25, query=query)\n\nmessage = messages[0] # get the first one\n\nmessage.mark_as_read()\nreply_msg = message.reply()\n\nif 'example@example.com' in reply_msg.to: # magic methods implemented\n reply_msg.body = 'George Best quote: I spent a lot of money on booze, birds and fast cars. The rest I just squandered.'\nelse:\n reply_msg.body = 'George Best quote: I used to go missing a lot... Miss Canada, Miss United Kingdom, Miss World.'\n\nreply_msg.send()\n```\n\n## AddressBook\nAddressBook groups the funcionality of both the Contact Folders and Contacts. Outlook Distribution Groups are not supported (By the Microsoft API's).\n\n#### Contact Folders\nRepresents a Folder within your Contacts Section in Office 365.\nAddressBook class represents the parent folder (it's a folder itself).\n\nYou can get any folder in your address book by requesting child folders or filtering by name.\n\n```python\naddress_book = account.address_book()\n\ncontacts = address_book.get_contacts(limit=None) # get all the contacts in the Personal Contacts root folder\n\nwork_contacts_folder = address_book.get_folder(folder_name='Work Contacts') # get a folder with 'Work Contacts' name\n\nmessage_to_all_contats_in_folder = work_contacts_folder.new_message() # creates a draft message with all the contacts as recipients\n\nmessage_to_all_contats_in_folder.subject = 'Hallo!'\nmessage_to_all_contats_in_folder.body = \"\"\"\nGeorge Best quote:\n\nIf you'd given me the choice of going out and beating four men and smashing a goal in\nfrom thirty yards against Liverpool or going to bed with Miss World,\nit would have been a difficult choice. Luckily, I had both.\n\"\"\"\nmessage_to_all_contats_in_folder.send()\n\n# querying folders is easy:\nchild_folders = address_book.get_folders(25) # get at most 25 child folders\n\nfor folder in child_folders:\n print(folder.name, folder.parent_id)\n\n# creating a contact folder:\naddress_book.create_child_folder('new folder')\n```\n\n#### The Global Address List\nOffice 365 API (Nor MS Graph API) has no concept such as the Outlook Global Address List.\nHowever you can use the [Users API](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/users) to access all the users within your organization.\n\nWithout admin consent you can only access a few properties of each user such as name and email and litte more.\nYou can search by name or retrieve a contact specifying the complete email.\n\n- Basic Permision needed is Users.ReadBasic.All (limit info)\n- Full Permision is Users.Read.All but needs admin consent.\n\nTo search the Global Address List (Users API):\n\n```python\nglobal_address_list = account.address_book(address_book='gal')\n\n# start a new query:\nq = global_address_list.new_query('display_name')\nq.startswith('George Best')\n\nprint(global_address_list.get_contacts(query=q))\n```\n\n\nTo retrieve a contact by it's email:\n\n```python\ncontact = global_address_list.get_contact_by_email('example@example.com')\n```\n\n#### Contacts\nEverything returned from an `AddressBook` instance is a `Contact` instance.\nContacts have all the information stored as attributes\n\nCreating a contact from an `AddressBook`:\n\n```python\nnew_contact = address_book.new_contact()\n\nnew_contact.name = 'George Best'\nnew_contact.job_title = 'football player'\nnew_contact.emails.add('george@best.com')\n\nnew_contact.save() # saved on the cloud\n\nmessage = new_contact.new_message() # Bonus: send a message to this contact\n\n# ...\n\nnew_contact.delete() # Bonus: deteled from the cloud\n```\n\n\n## Calendar\nThe calendar and events functionality is group in a `Schedule` object.\n\nA `Schedule` instance can list and create calendars. It can also list or create events on the default user calendar.\nTo use other calendars use a `Calendar` instance. \n\nWorking with the `Schedule` instance:\n```python\nimport datetime as dt\n\n# ...\nschedule = account.schedule()\n\nnew_event = schedule.new_event() # creates a new event in the user default calendar\nnew_event.subject = 'Recruit George Best!'\nnew_event.location = 'England'\n\n# naive datetimes will automatically be converted to timezone aware datetime\n# objects using the local timezone detected or the protocol provided timezone\n\nnew_event.start = dt.datetime(2018, 9, 5, 19, 45) \n# so new_event.start becomes: datetime.datetime(2018, 9, 5, 19, 45, tzinfo=)\n\nnew_event.recurrence.set_daily(1, end=dt.datetime(2018, 9, 10))\nnew_event.remind_before_minutes = 45\n\nnew_event.save()\n```\n\nWorking with `Calendar` instances:\n```python\ncalendar = schedule.get_calendar(calendar_name='Birthdays')\n\ncalendar.name = 'Football players birthdays'\ncalendar.update()\n\nq = calendar.new_query('start').ge(dt.datetime(2018, 5, 20)).chain('and').on_attribute('end').le(dt.datetime(2018, 5, 24))\n\nbirthdays = calendar.get_events(query=q)\n\nfor event in birthdays:\n if event.subject == 'George Best Birthday':\n # He died in 2005... but we celebrate anyway!\n event.accept(\"I'll attend!\") # send a response accepting\n else:\n event.decline(\"No way I'm comming, I'll be in Spain\", send_response=False) # decline the event but don't send a reponse to the organizer\n```\n\n## OneDrive\nThe `Storage` class handles all functionality around One Drive and Document Library Storage in Sharepoint.\n\nThe `Storage` instance allows to retrieve `Drive` instances which handles all the Files and Folders from within the selected `Storage`.\nUsually you will only need to work with the default drive. But the `Storage` instances can handle multiple drives.\n\n\nA `Drive` will allow you to work with Folders and Files.\n\n```python\naccount = Account(credentials=my_credentials)\n\nstorage = account.storage() # here we get the storage instance that handles all the storage options.\n\n# list all the drives:\ndrives = storage.get_drives()\n\n# get the default drive\nmy_drive = storage.get_default_drive() # or get_drive('drive-id')\n\n# get some folders:\nroot_folder = my_drive.get_root_folder()\nattachments_folder = my_drive.get_special_folder('attachments')\n\n# iterate over the first 25 items on the root folder\nfor item in root_folder.get_items(limit=25):\n if item.is_folder:\n print(item.get_items(2)) # print the first to element on this folder.\n elif item.is_file:\n if item.is_photo:\n print(item.camera_model) # print some metadata of this photo\n elif item.is_image:\n print(item.dimensione) # print the image dimensions\n else:\n # regular file:\n print(item.mime_type) # print the mime type\n```\n\nBoth Files and Folders are DriveItems. Both Image and Photo are Files, but Photo is also an Image. All have some different methods and properties. \nTake care when using 'is_xxxx'.\n\nWhen coping a DriveItem the api can return a direct copy of the item or a pointer to a resource that will inform on the progress of the copy operation.\n\n```python\n# copy a file to the documents special folder\n\ndocuments_folder = drive.get_special_folder('documents')\n\nfiles = drive.search('george best quotes', limit=1)\n\nif files:\n george_best_quotes = files[0]\n operation = george_best_quotes.copy(target=documents_folder) # operation here is an instance of CopyOperation\n\n # to check for the result just loop over check_status.\n # check_status is a generator that will yield a new status and progress until the file is finally copied\n for status, progress in operation.check_status(): # if it's an async operations, this will request to the api for the status in every loop\n print('{} - {}'.format(status, progress)) # prints 'in progress - 77.3' until finally completed: 'completed - 100.0'\n copied_item = operation.get_item() # the copy operation is completed so you can get the item.\n if copied_item:\n copied_item.delete() # ... oops!\n```\n\nYou can also work with share permissions:\n\n```python\ncurrent_permisions = file.get_permissions() # get all the current permissions on this drive_item (some may be inherited)\n\n# share with link\npermission = file.share_with_link(share_type='edit')\nif permission:\n print(permission.share_link) # the link you can use to share this drive item\n# share with invite\npermission = file.share_with_invite(recipients='george_best@best.com', send_email=True, message='Greetings!!', share_type='edit')\nif permission:\n print(permission.granted_to) # the person you share this item with\n```\n\nYou can also:\n```python\n# download files:\nfile.download(to_path='/quotes/')\n\n# upload files:\n\n# if the uploaded file is bigger than 4MB the file will be uploaded in chunks of 5 MB until completed.\n# this can take several requests and can be time consuming.\nuploaded_file = folder.upload_file(item='path_to_my_local_file')\n\n# restore versions:\nversiones = file.get_versions()\nfor version in versions:\n if version.name == '2.0':\n version.restore() # restore the version 2.0 of this file\n\n# ... and much more ...\n```\n\n\n## Sharepoint\nWork in progress\n\n\n## Utils\n\n#### Pagination\n\nWhen using certain methods, it is possible that you request more items than the api can return in a single api call.\nIn this case the Api, returns a \"next link\" url where you can pull more data.\n\nWhen this is the case, the methods in this library will return a `Pagination` object which abstracts all this into a single iterator.\nThe pagination object will request \"next links\" as soon as they are needed.\n\nFor example:\n\n```python\nmaibox = account.mailbox()\n\nmessages = mailbox.get_messages(limit=1500) # the Office 365 and MS Graph API have a 999 items limit returned per api call.\n\n# Here messages is a Pagination instance. It's an Iterator so you can iterate over.\n\n# The first 999 iterations will be normal list iterations, returning one item at a time.\n# When the iterator reaches the 1000 item, the Pagination instance will call the api again requesting exactly 500 items\n# or the items specified in the batch parameter (see later).\n\nfor message in messages:\n print(message.subject)\n```\n\nWhen using certain methods you will have the option to specify not only a limit option (the number of items to be returned) but a batch option.\nThis option will indicate the method to request data to the api in batches until the limit is reached or the data consumed.\nThis is usefull when you want to optimize memory or network latency.\n\nFor example:\n\n```python\nmessages = mailbox.get_messages(limit=100, batch=25)\n\n# messages here is a Pagination instance\n# when iterating over it will call the api 4 times (each requesting 25 items).\n\nfor message in messages: # 100 loops with 4 requests to the api server\n print(message.subject)\n```\n\n#### The Query helper\n\nWhen using the Office 365 API you can filter some fields.\nThis filtering is tedious as is using [Open Data Protocol (OData)](http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html).\n\nEvery `ApiComponent` (such as `MailBox`) implements a new_query method that will return a `Query` instance.\nThis `Query` instance can handle the filtering (and sorting and selecting) very easily.\n\nFor example:\n\n```python\nquery = mailbox.new_query()\n\nquery = query.on_attribute('subject').contains('george best').chain('or').startswith('quotes')\n\n# 'created_date_time' will automatically be converted to the protocol casing.\n# For example when using MS Graph this will become 'createdDateTime'.\n\nquery = query.chain('and').on_attribute('created_date_time').greater(datetime(2018, 3, 21))\n\nprint(query)\n\n# contains(subject, 'george best') or startswith(subject, 'quotes') and createdDateTime gt '2018-03-21T00:00:00Z'\n# note you can pass naive datetimes and those will be converted to you local timezone and then send to the api as UTC in iso8601 format\n\n# To use Query objetcs just pass it to the query parameter:\nfiltered_messages = mailbox.get_messages(query=query)\n```\n\nYou can also specify specific data to be retrieved with \"select\":\n\n```python\n# select only some properties for the retrieved messages:\nquery = mailbox.new_query().select('subject', 'to_recipients', 'created_date_time')\n\nmessages_with_selected_properties = mailbox.get_messages(query=query)\n```\n\n#### Request Error Handling and Custom Errors\n\nWhenever a Request error raises, the connection object will raise an exception.\nThen the exception will be captured and logged it to the stdout with it's message, an return Falsy (None, False, [], etc...)\n\nHttpErrors 4xx (Bad Request) and 5xx (Internal Server Error) are considered exceptions and raised also by the connection (you can configure this on the connection).\n\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/janscas/pyo365", "keywords": "", "license": "Apache License 2.0", "maintainer": "Janscas", "maintainer_email": "janscas@users.noreply.github.com", "name": "pyo365", "package_url": "https://pypi.org/project/pyo365/", "platform": "", "project_url": "https://pypi.org/project/pyo365/", "project_urls": { "Homepage": "https://github.com/janscas/pyo365" }, "release_url": "https://pypi.org/project/pyo365/0.1.3/", "requires_dist": [ "requests (>=2.0.0)", "requests-oauthlib (>=1.0.0)", "python-dateutil (>=2.7)", "pytz (>=2018.5)", "tzlocal (>=1.5.0)", "beautifulsoup4 (>=4.0.0)", "stringcase (>=1.2.0)" ], "requires_python": ">=3.4", "summary": "A simple python library to interact with Microsoft Graph and Office 365 API", "version": "0.1.3" }, "last_serial": 4433544, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "9a33281a0a860bc5fabf2aa2d3b79023", "sha256": "6c5c0a7732bd3e5166b4098555e6050d31e46d4bed2e29cff715429c532ced5e" }, "downloads": -1, "filename": "pyo365-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "9a33281a0a860bc5fabf2aa2d3b79023", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.4", "size": 68763, "upload_time": "2018-10-19T13:13:53", "url": "https://files.pythonhosted.org/packages/52/e7/e688d79141b9760870b4e6478bb471c4618eb231eced9b88212c370a1954/pyo365-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "13f8d565718ad7513ae2e7d982abe87e", "sha256": "3512cc75994bb39c8d344b45a8188edd90cf52c297bc7dabbeb580332f241314" }, "downloads": -1, "filename": "pyo365-0.1.0.tar.gz", "has_sig": false, "md5_digest": "13f8d565718ad7513ae2e7d982abe87e", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4", "size": 73931, "upload_time": "2018-10-19T13:13:54", "url": "https://files.pythonhosted.org/packages/bd/d5/fbd76f65dc9687bec660a46e2d98ab8b5dee69f899788d1870d77dfd2e38/pyo365-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "698580271cb6c7dd10973bf4076350ab", "sha256": "2d5651abe80bac24c609dbf9d6fb62d1416742b141f0486812bb8db1adf64e59" }, "downloads": -1, "filename": "pyo365-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "698580271cb6c7dd10973bf4076350ab", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.4", "size": 69686, "upload_time": "2018-10-22T16:55:23", "url": "https://files.pythonhosted.org/packages/33/f0/4f3b8a9580752766f63e33e6b27ed4ff4cbe2a0b7e670deb7748606ecad4/pyo365-0.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9a8fd8e21eea523d7b3f28f06da82a1d", "sha256": "831b1e01419c789fe24a974b2091408cefe211d88a09c449f57afdca5ccb9dfd" }, "downloads": -1, "filename": "pyo365-0.1.1.tar.gz", "has_sig": false, "md5_digest": "9a8fd8e21eea523d7b3f28f06da82a1d", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4", "size": 74770, "upload_time": "2018-10-22T16:55:24", "url": "https://files.pythonhosted.org/packages/30/2e/9c3267db0de9ee861515f464eff91b4357a69980e2f13668d0717bf1071a/pyo365-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "8fa8dde5baa7207593fc0c569d72ab94", "sha256": "71b2faef79eb93a947158312a1320d408f6deda460ad5cf7104ae818b004ca52" }, "downloads": -1, "filename": "pyo365-0.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "8fa8dde5baa7207593fc0c569d72ab94", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.4", "size": 67483, "upload_time": "2018-10-26T15:43:31", "url": "https://files.pythonhosted.org/packages/63/c7/516607eea724324a69cf9a07cb3799bb1dd036b32d450cb889d82650be73/pyo365-0.1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c68569d9fb57b2dd369780fb2561b6f6", "sha256": "1e56f9b472556a0e31aee3183cc09483e9510301c1b10f08403b1b27565bd072" }, "downloads": -1, "filename": "pyo365-0.1.2.tar.gz", "has_sig": false, "md5_digest": "c68569d9fb57b2dd369780fb2561b6f6", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4", "size": 73759, "upload_time": "2018-10-26T15:43:33", "url": "https://files.pythonhosted.org/packages/14/71/f20bc8d81bedda4bf3d6f3f3d673267c4411c63685f219dcf77da484e081/pyo365-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "530ec39113c9b3bf62bac9701e9f39ce", "sha256": "6f2f3cf568551d589ca328db6d45235b381f0ee9d305aa057c1a986aa355097a" }, "downloads": -1, "filename": "pyo365-0.1.3-py3-none-any.whl", "has_sig": false, "md5_digest": "530ec39113c9b3bf62bac9701e9f39ce", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.4", "size": 68188, "upload_time": "2018-10-30T22:32:22", "url": "https://files.pythonhosted.org/packages/5c/0d/d4514292f5838cdc9eb373a05801e3110d09f92944dde2434934269807e1/pyo365-0.1.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f43bd84160b6ff6979f8bd7ef1197392", "sha256": "a13e8a99a710061edf736a18b3934b699455d1447dd2883c5cd28e0a1f307c11" }, "downloads": -1, "filename": "pyo365-0.1.3.tar.gz", "has_sig": false, "md5_digest": "f43bd84160b6ff6979f8bd7ef1197392", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4", "size": 72570, "upload_time": "2018-10-30T22:32:23", "url": "https://files.pythonhosted.org/packages/0d/6e/ef5bb6e7e85ac9698c04c6ee7904ba8e06787a2e1834014141aff57d55e4/pyo365-0.1.3.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "530ec39113c9b3bf62bac9701e9f39ce", "sha256": "6f2f3cf568551d589ca328db6d45235b381f0ee9d305aa057c1a986aa355097a" }, "downloads": -1, "filename": "pyo365-0.1.3-py3-none-any.whl", "has_sig": false, "md5_digest": "530ec39113c9b3bf62bac9701e9f39ce", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.4", "size": 68188, "upload_time": "2018-10-30T22:32:22", "url": "https://files.pythonhosted.org/packages/5c/0d/d4514292f5838cdc9eb373a05801e3110d09f92944dde2434934269807e1/pyo365-0.1.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f43bd84160b6ff6979f8bd7ef1197392", "sha256": "a13e8a99a710061edf736a18b3934b699455d1447dd2883c5cd28e0a1f307c11" }, "downloads": -1, "filename": "pyo365-0.1.3.tar.gz", "has_sig": false, "md5_digest": "f43bd84160b6ff6979f8bd7ef1197392", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.4", "size": 72570, "upload_time": "2018-10-30T22:32:23", "url": "https://files.pythonhosted.org/packages/0d/6e/ef5bb6e7e85ac9698c04c6ee7904ba8e06787a2e1834014141aff57d55e4/pyo365-0.1.3.tar.gz" } ] }