{"id":13585545,"url":"https://github.com/mandarons/icloudpy","last_synced_at":"2025-05-15T20:06:01.687Z","repository":{"id":43766403,"uuid":"450727871","full_name":"mandarons/icloudpy","owner":"mandarons","description":"iCloud web service interface library in Python","archived":false,"fork":false,"pushed_at":"2025-04-28T20:06:45.000Z","size":3104,"stargazers_count":234,"open_issues_count":15,"forks_count":24,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-05-12T02:18:58.340Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mandarons.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":["https://www.buymeacoffee.com/mandarons"]}},"created_at":"2022-01-22T05:38:19.000Z","updated_at":"2025-05-01T04:26:14.000Z","dependencies_parsed_at":"2023-11-26T17:24:40.774Z","dependency_job_id":"2909ad8e-abd0-4bbf-b9c6-824fce93b75d","html_url":"https://github.com/mandarons/icloudpy","commit_stats":{"total_commits":41,"total_committers":4,"mean_commits":10.25,"dds":"0.12195121951219512","last_synced_commit":"e7edabd2f82fc217d8504cacf9e942e5d7a9808b"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mandarons%2Ficloudpy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mandarons%2Ficloudpy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mandarons%2Ficloudpy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mandarons%2Ficloudpy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mandarons","download_url":"https://codeload.github.com/mandarons/icloudpy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254414499,"owners_count":22067272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T15:05:00.403Z","updated_at":"2025-05-15T20:05:56.612Z","avatar_url":"https://github.com/mandarons.png","language":"Python","funding_links":["https://www.buymeacoffee.com/mandarons"],"categories":["Python"],"sub_categories":[],"readme":"# iCloudPy\n\n[![CI - Main](https://github.com/mandarons/icloudpy/actions/workflows/ci-main-test-coverage.yml/badge.svg)](https://github.com/mandarons/icloudpy/actions/workflows/ci-main-test-coverage.yml)\n[![Tests](https://mandarons.github.io/icloudpy/badges/tests.svg)](https://mandarons.github.io/icloudpy/test-results/)\n[![Coverage](https://mandarons.github.io/icloudpy/badges/coverage.svg)](https://mandarons.github.io/icloudpy/test-coverage/index.html)\n[![Discord](https://img.shields.io/discord/871555550444408883?style=for-the-badge)](https://discord.gg/BnNpJUQ2)\n\u003ca href=\"https://www.buymeacoffee.com/mandarons\" target=\"_blank\"\u003e\u003cimg src=\"https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png\" alt=\"Buy Me A Coffee\" style=\"height: 30px !important;width: 150px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;\" \u003e\u003c/a\u003e\n\n:love*you_gesture: \\*\\*\\_Please star this repository if you end up using this project. If it has improved your life in any way, consider donating for my effort using 'Buy Me a Coffee' button above. It will help me continue supporting this product.*\\*\\* :pray:\n\niCloudPy is a simple iCloud webservices wrapper library written in Python. It is a major reuse of [pyiCloud](https://github.com/picklepete/pyicloud) python library.\n\niCloudPy connects to iCloud using your `username` and `password`, stores the session locally and then performs various queries to iCloud server.\n\n## Authentication\n\nAuthentication without using a saved password is as simple as passing your username and password to the `ICloudPyService` class:\n\n```python\nfrom icloudpy import ICloudPyService\napi = ICloudPyService('jappleseed@apple.com', 'password')\n# For China region\napi = ICloudPyService('jappleseed@apple.com', 'password', home_endpoint=\"https://www.icloud.com.cn\",setup_endpoint=\"https://setup.icloud.com.cn/setup/ws/1\",)\n```\n\nIn the event that the username/password combination is invalid, a `ICloudPyFailedLoginException` exception is thrown.\n\nYou can also store your password in the system keyring using the command-line tool:\n\n```bash\n\u003e icloud --username=jappleseed@apple.com\nICloud Password for jappleseed@apple.com:\nSave password in keyring? (y/N)\n# For China region\n\u003e icloud --username=jappleseed@apple.com --region=china\nICloud Password for jappleseed@apple.com:\nSave password in keyring? (y/N)\n```\n\nIf you have stored a password in the keyring, you will not be required to provide a password when interacting with the command-line tool or instantiating the `ICloudPyService` class for the username you stored the password for.\n\n```python\napi = ICloudPyService('jappleseed@apple.com')\n```\n\nIf you would like to delete a password stored in your system keyring, you can clear a stored password using the `--delete-from-keyring` command-line option:\n\n```bash\n\u003e icloud --username=jappleseed@apple.com --delete-from-keyring\n```\n\n**_Note: Authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months._**\n\n## Two-step and two-factor authentication (2SA/2FA)\n\nIf you have enabled [two-factor authentications (2FA) or two-step authentication (2SA)](https://support.apple.com/en-us/HT204152) for the account you will have to do some extra work:\n\n```python\n\n    if api.requires_2fa:\n        print \"Two-factor authentication required.\"\n        code = input(\"Enter the code you received of one of your approved devices: \")\n        result = api.validate_2fa_code(code)\n        print(\"Code validation result: %s\" % result)\n\n        if not result:\n            print(\"Failed to verify security code\")\n            sys.exit(1)\n\n        if not api.is_trusted_session:\n            print(\"Session is not trusted. Requesting trust...\")\n            result = api.trust_session()\n            print(\"Session trust result %s\" % result)\n\n            if not result:\n                print(\"Failed to request trust. You will likely be prompted for the code again in the coming weeks\")\n    elif api.requires_2sa:\n        import click\n        print \"Two-step authentication required. Your trusted devices are:\"\n\n        devices = api.trusted_devices\n        for i, device in enumerate(devices):\n            print \"  %s: %s\" % (i, device.get('deviceName',\n                \"SMS to %s\" % device.get('phoneNumber')))\n\n        device = click.prompt('Which device would you like to use?', default=0)\n        device = devices[device]\n        if not api.send_verification_code(device):\n            print \"Failed to send verification code\"\n            sys.exit(1)\n\n        code = click.prompt('Please enter validation code')\n        if not api.validate_verification_code(device, code):\n            print \"Failed to verify verification code\"\n            sys.exit(1)\n```\n\n## Devices\n\nYou can list which devices associated with your account by using the `devices` property:\n\n```bash\n\u003e\u003e\u003e api.devices\n{\nu'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': \u003cAppleDevice(iPhone 4S: Johnny Appleseed's iPhone)\u003e,\nu'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': \u003cAppleDevice(MacBook Air 11\": Johnny Appleseed's MacBook Air)\u003e\n}\n```\n\nand you can access individual devices by either their index, or their ID:\n\n```bash\n\u003e\u003e\u003e api.devices[0]\n\u003cAppleDevice(iPhone 4S: Johnny Appleseed's iPhone)\u003e\n\u003e\u003e\u003e api.devices['i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==']\n\u003cAppleDevice(iPhone 4S: Johnny Appleseed's iPhone)\u003e\n```\n\nor, as a shorthand if you have only one associated apple device, you can simply use the `iphone` property to access the first device associated with your account:\n\n```bash\n\u003e\u003e\u003e api.iphone\n\u003cAppleDevice(iPhone 4S: Johnny Appleseed's iPhone)\u003e\n```\n\n**_Note: the first device associated with your account may not necessarily be your iPhone._**\n\n## Find My iPhone\n\nOnce you have successfully authenticated, you can start querying your data!\n\n### Location\n\nReturns the device's last known location. The Find My iPhone app must have been installed and initialized.\n\n```bash\n\u003e\u003e\u003e api.iphone.location()\n{u'timeStamp': 1357753796553, u'locationFinished': True, u'longitude': -0.14189, u'positionType': u'GPS', u'locationType': None, u'latitude': 51.501364, u'isOld': False, u'horizontalAccuracy': 5.0}\n```\n\n### Status\n\nThe Find My iPhone response is quite bloated, so for simplicity's sake this method will return a subset of the properties.\n\n```bash\n\u003e\u003e\u003e api.iphone.status()\n{'deviceDisplayName': u'iPhone 5', 'deviceStatus': u'200', 'batteryLevel': 0.6166913, 'name': u\"Peter's iPhone\"}\n```\n\nIf you wish to request further properties, you may do so by passing in a list of property names.\n\n### Play Sound\n\nSends a request to the device to play a sound, if you wish pass a custom message you can do so by changing the subject arg.\n\n```bash\n\u003e\u003e\u003e api.iphone.play_sound()\n```\n\nA few moments later, the device will play a ringtone, display the default notification (\"Find My iPhone Alert\") and a confirmation email will be sent to you.\n\n### Lost Mode\n\nLost mode is slightly different to the \"Play Sound\" functionality in that it allows the person who picks up the phone to call a specific phone number _without having to enter the passcode_. Just like \"Play Sound\" you may pass a custom message which the device will display, if it's not overridden the custom message of \"This iPhone has been lost. Please call me.\" is used.\n\n```bash\n\u003e\u003e\u003e phone_number = '555-373-383'\n\u003e\u003e\u003e message = 'Thief! Return my phone immediately.'\n\u003e\u003e\u003e api.iphone.lost_device(phone_number, message)\n```\n\n## Calendar\n\nThe calendar webservice currently only supports fetching events.\n\n### Events\n\nReturns this month's events:\n\n```bash\n\u003e\u003e\u003e api.calendar.events()\n```\n\nOr, between a specific date range:\n\n```bash\n\u003e\u003e\u003e from_dt = datetime(2012, 1, 1)\n\u003e\u003e\u003e to_dt = datetime(2012, 1, 31)\n\u003e\u003e\u003e api.calendar.events(from_dt, to_dt)\n```\n\nAlternatively, you may fetch a single event's details, like so:\n\n```bash\n\u003e\u003e\u003e api.calendar.get_event_detail('CALENDAR', 'EVENT_ID')\n```\n\n## Contacts\n\nYou can access your iCloud contacts/address book through the `contacts` property:\n\n```bash\n\u003e\u003e\u003e for c in api.contacts.all():\n\u003e\u003e\u003e print c.get('firstName'), c.get('phones')\nJohn [{u'field': u'+1 555-55-5555-5', u'label': u'MOBILE'}]\n```\n\n**_Note: These contacts do not include contacts federated from e.g. Facebook, only the ones stored in iCloud._**\n\n## File Storage (Ubiquity)\n\n**NOTE: It seems like having app documents are no longer stored in Ubiquity [and migrated to iCloud Drive](https://developer.apple.com/library/archive/technotes/tn2348/_index.html). See below.**\n\nYou can access documents stored in your iCloud account by using the `files` property's `dir` method:\n\n```bash\n\u003e\u003e\u003e api.files.dir()\n[u'.do-not-delete',\n u'.localized',\n u'com~apple~Notes',\n u'com~apple~Preview',\n u'com~apple~mail',\n u'com~apple~shoebox',\n u'com~apple~system~spotlight'\n]\n```\n\nYou can access children and their children's children using the filename as an index:\n\n```bash\n\u003e\u003e\u003e api.files['com~apple~Notes']\n\u003cFolder: u'com~apple~Notes'\u003e\n\u003e\u003e\u003e api.files['com~apple~Notes'].type\nu'folder'\n\u003e\u003e\u003e api.files['com~apple~Notes'].dir()\n[u'Documents']\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents'].dir()\n[u'Some Document']\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['Some Document'].name\nu'Some Document'\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['Some Document'].modified\ndatetime.datetime(2012, 9, 13, 2, 26, 17)\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['Some Document'].size\n1308134\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['Some Document'].type\nu'file'\n```\n\nAnd when you have a file that you'd like to download, the `open` method will return a response object from which you can read the `content`.\n\n```bash\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['Some Document'].open().content\n'Hello, these are the file contents'\n```\n\nThe object returned from the above `open` method is a [response object](http://www.python-requests.org/en/latest/api/#classes) and the `open` method can accept any parameters you might normally use in a request using [requests](https://github.com/kennethreitz/requests).\n\nFor example, if you know that the file you're opening has JSON content:\n\n```bash\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['information.json'].open().json()\n{'How much we love you': 'lots'}\n\u003e\u003e\u003e api.files['com~apple~Notes']['Documents']['information.json'].open().json()['How much we love you']\n'lots'\n```\n\nOr, if you're downloading a particularly large file, you may want to use the `stream` keyword argument, and read directly from the raw response object:\n\n```bash\n\u003e\u003e\u003e download = api.files['com~apple~Notes']['Documents']['big_file.zip'].open(stream=True)\n\u003e\u003e\u003e with open('downloaded_file.zip', 'wb') as opened_file:\n        opened_file.write(download.raw.read())\n```\n\n## File Storage (iCloud Drive)\n\nYou can access your iCloud Drive using an API identical to the Ubiquity one described in the previous section, except that it is rooted at `api.drive`:\n\n```bash\n\u003e\u003e\u003e api.drive.dir()\n['Holiday Photos', 'Work Files']\n\u003e\u003e\u003e api.drive['Holiday Photos']['2013']['Sicily'].dir()\n['DSC08116.JPG', 'DSC08117.JPG']\n\n\u003e\u003e\u003e drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG']\n\u003e\u003e\u003e drive_file.name\nu'DSC08116.JPG'\n\u003e\u003e\u003e drive_file.date_modified\ndatetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC\n\u003e\u003e\u003e drive_file.size\n2021698\n\u003e\u003e\u003e drive_file.type\nu'file'\n```\n\nThe `open` method will return a response object from which you can read the file's contents:\n\n```bash\n\u003e\u003e\u003e from shutil import copyfileobj\n\u003e\u003e\u003e with drive_file.open(stream=True) as response:\n\u003e\u003e\u003e     with open(drive_file.name, 'wb') as file_out:\n\u003e\u003e\u003e         copyfileobj(response.raw, file_out)\n```\n\nTo interact with files and directions the `mkdir`, `rename` and `delete` functions are available\nfor a file or folder:\n\n```bash\n\u003e\u003e\u003e api.drive['Holiday Photos'].mkdir('2020')\n\u003e\u003e\u003e api.drive['Holiday Photos']['2020'].rename('2020_copy')\n\u003e\u003e\u003e api.drive['Holiday Photos']['2020_copy'].delete()\n```\n\nThe `upload` method can be used to send a file-like object to the iCloud Drive:\n\n```bash\n\u003e\u003e\u003e with open('Vacation.jpeg', 'rb') as file_in:\n\u003e\u003e\u003e\u003e    api.drive['Holiday Photos'].upload(file_in)\n```\n\nIt is strongly suggested to open file handles as binary rather than text to prevent decoding errors\nfurther down the line.\n\n### Accessing App Data\n\nThe `get_app_node` method can be used to retrieve a node with app data (that is not shown in `api.drive.dir()`). This is where the individual apps store related documents.\n\n```bash\n\u003e\u003e\u003e node = api.drive.get_app_node(\"XXXXXXXXXX.com.apple.iMovie\")\n```\n\nIds of individual app data can be found in `~/Library/Mobile Documents` (can only be accessed in Terminal). `~` must be replaced with `.`.\n\nNode can then be used just like any other node, supporting `mkdir`, `rename`, `delete` and so on:\n\n```\n\u003e\u003e\u003e node.mkdir('2020')\n\u003e\u003e\u003e node.rename('2020_copy')\n\u003e\u003e\u003e node.delete()\n```\n\n## Photo Library\n\nYou can access the iCloud Photo Library through the `photos` property.\n\n```bash\n\u003e\u003e\u003e api.photos.all\n\u003cPhotoAlbum: 'All Photos'\u003e\n```\n\nIndividual albums are available through the `albums` property:\n\n```bash\n\u003e\u003e\u003e api.photos.albums['Screenshots']\n\u003cPhotoAlbum: 'Screenshots'\u003e\n```\n\nWhich you can iterate to access the photo assets. The 'All Photos' album is sorted by `added_date` so the most recently added photos are returned first. All other albums are sorted by `asset_date` (which represents the exif date) :\n\n```bash\n\u003e\u003e\u003e for photo in api.photos.albums['Screenshots']:\n        print photo, photo.filename\n\u003cPhotoAsset: id=AVbLPCGkp798nTb9KZozCXtO7jds\u003e IMG_6045.JPG\n```\n\nTo download a photo use the `download` method, which will return a [response object](http://www.python-requests.org/en/latest/api/#classes), initialized with `stream` set to `True`, so you can read from the raw response object:\n\n```bash\n\u003e\u003e\u003e photo = next(iter(api.photos.albums['Screenshots']), None)\n\u003e\u003e\u003e download = photo.download()\n\u003e\u003e\u003e with open(photo.filename, 'wb') as opened_file:\n        opened_file.write(download.raw.read())\n```\n\n**_Note: Consider using `shutil.copyfile` or another buffered strategy for downloading the file so that the whole file isn't read into memory before writing._**\n\nInformation about each version can be accessed through the `versions` property:\n\n```bash\n\u003e\u003e\u003e photo.versions.keys()\n[u'medium', u'original', u'thumb']\n```\n\nTo download a specific version of the photo asset, pass the version to `download()`:\n\n```bash\n\u003e\u003e\u003e download = photo.download('thumb')\n\u003e\u003e\u003e with open(photo.versions['thumb']['filename'], 'wb') as thumb_file:\n        thumb_file.write(download.raw.read())\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmandarons%2Ficloudpy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmandarons%2Ficloudpy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmandarons%2Ficloudpy/lists"}