icloud-cli-tools 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {icloud_cli_tools-0.1.1/src/icloud_cli_tools.egg-info → icloud_cli_tools-0.1.2}/PKG-INFO +2 -2
  2. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/pyproject.toml +2 -2
  3. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/__init__.py +1 -1
  4. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/auth.py +20 -2
  5. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/main.py +56 -0
  6. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/services/calendar.py +69 -21
  7. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/services/findmy.py +5 -5
  8. icloud_cli_tools-0.1.2/src/icloud_cli/services/reminders.py +409 -0
  9. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2/src/icloud_cli_tools.egg-info}/PKG-INFO +2 -2
  10. icloud_cli_tools-0.1.2/tests/test_calendar.py +177 -0
  11. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/tests/test_findmy.py +21 -10
  12. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/tests/test_reminders.py +103 -2
  13. icloud_cli_tools-0.1.1/src/icloud_cli/services/reminders.py +0 -209
  14. icloud_cli_tools-0.1.1/tests/test_calendar.py +0 -104
  15. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/.github/workflows/ci.yml +0 -0
  16. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/.gitignore +0 -0
  17. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/LICENSE +0 -0
  18. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/README.md +0 -0
  19. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/setup.cfg +0 -0
  20. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/config.py +0 -0
  21. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/daemon.py +0 -0
  22. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/output.py +0 -0
  23. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/services/__init__.py +0 -0
  24. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli/services/notes.py +0 -0
  25. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli_tools.egg-info/SOURCES.txt +0 -0
  26. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli_tools.egg-info/dependency_links.txt +0 -0
  27. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli_tools.egg-info/entry_points.txt +0 -0
  28. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli_tools.egg-info/requires.txt +0 -0
  29. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/src/icloud_cli_tools.egg-info/top_level.txt +0 -0
  30. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/systemd/icloud-cli-sync.service +0 -0
  31. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/tests/conftest.py +0 -0
  32. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/tests/test_auth.py +0 -0
  33. {icloud_cli_tools-0.1.1 → icloud_cli_tools-0.1.2}/tests/test_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icloud-cli-tools
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Access iCloud Calendar, Reminders, Notes, and Find My from Linux
5
5
  Author: alan
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Topic :: Utilities
21
- Requires-Python: >=3.9
21
+ Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: pyicloud>=2.5.0
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "icloud-cli-tools"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Access iCloud Calendar, Reminders, Notes, and Find My from Linux"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
- requires-python = ">=3.9"
11
+ requires-python = ">=3.10"
12
12
  authors = [{ name = "alan" }]
13
13
  keywords = [
14
14
  "icloud",
@@ -1,3 +1,3 @@
1
1
  """icloud-cli-tools: Access iCloud services from Linux."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -89,7 +89,26 @@ class AuthManager:
89
89
  def _handle_2fa(self) -> bool:
90
90
  """Handle two-factor authentication (trusted device code)."""
91
91
  info("Two-factor authentication required.")
92
- info("A verification code has been sent to your trusted devices.")
92
+
93
+ try:
94
+ self._api.request_2fa_code()
95
+ except Exception as e:
96
+ error(f"Failed to trigger 2FA delivery: {e}")
97
+ return False
98
+
99
+ method = self._api.two_factor_delivery_method
100
+ notice = self._api.two_factor_delivery_notice
101
+
102
+ if method == "trusted_device":
103
+ info("Approve the sign-in request on your trusted device.")
104
+ info("Enter the 6-digit code shown after you approve.")
105
+ elif method == "sms":
106
+ info("A verification code has been sent via SMS.")
107
+ else:
108
+ info("A verification code has been sent to your trusted devices.")
109
+
110
+ if notice:
111
+ warning(notice)
93
112
 
94
113
  code = click.prompt("Enter 2FA code")
95
114
 
@@ -99,7 +118,6 @@ class AuthManager:
99
118
  error("Invalid 2FA code.")
100
119
  return False
101
120
 
102
- # Trust this session
103
121
  if not self._api.is_trusted_session:
104
122
  info("Trusting this session...")
105
123
  self._api.trust_session()
@@ -227,6 +227,62 @@ def reminders_add(
227
227
  error("Failed to create reminder.")
228
228
 
229
229
 
230
+ @reminders.group("lists")
231
+ def reminders_lists():
232
+ """Manage reminder lists."""
233
+ pass
234
+
235
+
236
+ @reminders_lists.command("list")
237
+ @pass_context
238
+ def reminders_lists_list(ctx: AppContext):
239
+ """List reminder lists."""
240
+ from icloud_cli.services.reminders import RemindersService
241
+
242
+ service = RemindersService(ctx.auth.api, ctx.config)
243
+ items = service.list_reminder_lists()
244
+ render(items, format=ctx.format, title="Reminder Lists",
245
+ columns=["title", "count", "color", "id"])
246
+
247
+
248
+ @reminders_lists.command("create")
249
+ @click.argument("name")
250
+ @click.option("--color", default="blue", help="List color.")
251
+ @pass_context
252
+ def reminders_lists_create(ctx: AppContext, name: str, color: str):
253
+ """Create a reminder list."""
254
+ from icloud_cli.services.reminders import RemindersService
255
+
256
+ service = RemindersService(ctx.auth.api, ctx.config)
257
+ list_id = service.create_list(name, color=color)
258
+ if list_id:
259
+ success(f"Reminder list '{name}' created.")
260
+ else:
261
+ error(f"Failed to create reminder list '{name}'.")
262
+
263
+
264
+ @reminders.group("sections")
265
+ def reminders_sections():
266
+ """Manage reminder list sections."""
267
+ pass
268
+
269
+
270
+ @reminders_sections.command("create")
271
+ @click.argument("list_name")
272
+ @click.argument("section_name")
273
+ @pass_context
274
+ def reminders_sections_create(ctx: AppContext, list_name: str, section_name: str):
275
+ """Create a section in a reminder list."""
276
+ from icloud_cli.services.reminders import RemindersService
277
+
278
+ service = RemindersService(ctx.auth.api, ctx.config)
279
+ section_id = service.create_section(list_name=list_name, section_name=section_name)
280
+ if section_id:
281
+ success(f"Reminder section '{section_name}' created.")
282
+ else:
283
+ error(f"Failed to create reminder section '{section_name}'.")
284
+
285
+
230
286
  @reminders.command("complete")
231
287
  @click.argument("reminder_id")
232
288
  @pass_context
@@ -10,6 +10,7 @@ from typing import Any
10
10
 
11
11
  from dateutil import parser as dateutil_parser
12
12
  from pyicloud import PyiCloudService
13
+ from pyicloud.services.calendar import EventObject
13
14
 
14
15
  from icloud_cli.config import Config
15
16
 
@@ -47,6 +48,23 @@ def _format_datetime(dt: Any) -> str:
47
48
  return dt
48
49
  if isinstance(dt, datetime):
49
50
  return dt.strftime("%Y-%m-%d %H:%M")
51
+ if isinstance(dt, list) and len(dt) >= 4:
52
+ # Calendar events come back as Apple's raw, packed-first list
53
+ # (pyicloud's get_events passes the response through untransformed):
54
+ # [packed_str, year, month, day, hour, minute, tz_offset_minutes]
55
+ # (Reminders use pyicloud's typed API and return datetimes, not lists,
56
+ # so this function only ever sees the packed-first calendar shape.)
57
+ try:
58
+ parsed = datetime(
59
+ dt[1],
60
+ dt[2],
61
+ dt[3],
62
+ dt[4] if len(dt) > 4 else 0,
63
+ dt[5] if len(dt) > 5 else 0,
64
+ )
65
+ return parsed.strftime("%Y-%m-%d %H:%M")
66
+ except (IndexError, TypeError, ValueError):
67
+ return str(dt)
50
68
  if isinstance(dt, (int, float)):
51
69
  # Unix timestamp in milliseconds
52
70
  try:
@@ -80,7 +98,11 @@ class CalendarService:
80
98
  end = _parse_date(to_date, default=start + timedelta(days=7))
81
99
 
82
100
  try:
83
- events = self.api.calendar.events(from_dt=start, to_dt=end)
101
+ cal_name = {
102
+ cal.get("guid"): cal.get("title", "")
103
+ for cal in self.api.calendar.get_calendars()
104
+ }
105
+ events = self.api.calendar.get_events(from_dt=start, to_dt=end)
84
106
  except Exception as e:
85
107
  from icloud_cli.output import error
86
108
  error(f"Failed to fetch events: {e}")
@@ -88,6 +110,7 @@ class CalendarService:
88
110
 
89
111
  result = []
90
112
  for event in events:
113
+ pguid = event.get("pGuid", "")
91
114
  result.append({
92
115
  "id": event.get("guid", ""),
93
116
  "title": event.get("title", "Untitled"),
@@ -97,7 +120,7 @@ class CalendarService:
97
120
  "end": _format_datetime(
98
121
  event.get("endDate") or event.get("localEndDate")
99
122
  ),
100
- "calendar": event.get("pGuid", ""),
123
+ "calendar": cal_name.get(pguid, pguid),
101
124
  "location": event.get("location", ""),
102
125
  "all_day": event.get("allDay", False),
103
126
  })
@@ -120,7 +143,7 @@ class CalendarService:
120
143
  end = now + timedelta(days=365)
121
144
 
122
145
  try:
123
- events = self.api.calendar.events(from_dt=start, to_dt=end)
146
+ events = self.api.calendar.get_events(from_dt=start, to_dt=end)
124
147
  except Exception:
125
148
  return None
126
149
 
@@ -175,19 +198,33 @@ class CalendarService:
175
198
  return False
176
199
 
177
200
  try:
178
- # Build event payload
179
- event_data = {
180
- "title": title,
181
- "startDate": _datetime_to_icloud(start_dt),
182
- "endDate": _datetime_to_icloud(end_dt),
183
- }
184
-
185
- if location:
186
- event_data["location"] = location
201
+ calendars = self.api.calendar.get_calendars()
202
+ cal_guid = None
203
+ if calendar_name:
204
+ for cal in calendars:
205
+ if cal.get("title", "").lower() == calendar_name.lower():
206
+ cal_guid = cal.get("guid")
207
+ break
208
+ if not cal_guid and calendars:
209
+ cal_guid = calendars[0].get("guid")
210
+ if not cal_guid:
211
+ from icloud_cli.output import error
212
+ error("No calendars found.")
213
+ return False
214
+
215
+ event_obj = EventObject(
216
+ pguid=cal_guid,
217
+ title=title,
218
+ start_date=start_dt,
219
+ end_date=end_dt,
220
+ location=location or "",
221
+ )
187
222
  if description:
188
- event_data["description"] = description
189
-
190
- self.api.calendar.create_event(**event_data)
223
+ # pyicloud's EventObject has no notes/description field, so notes
224
+ # cannot be persisted. Warn rather than dropping them silently.
225
+ from icloud_cli.output import warning
226
+ warning("Event notes are not supported by the backend and were not saved.")
227
+ self.api.calendar.add_event(event_obj)
191
228
  return True
192
229
  except Exception as e:
193
230
  from icloud_cli.output import error
@@ -204,14 +241,25 @@ class CalendarService:
204
241
  True if event was deleted successfully.
205
242
  """
206
243
  try:
207
- self.api.calendar.delete_event(event_id)
244
+ now = datetime.now()
245
+ events = self.api.calendar.get_events(
246
+ from_dt=now - timedelta(days=365),
247
+ to_dt=now + timedelta(days=365),
248
+ )
249
+ pguid = None
250
+ for ev in events:
251
+ if ev.get("guid") == event_id:
252
+ pguid = ev.get("pGuid")
253
+ break
254
+ if not pguid:
255
+ from icloud_cli.output import error
256
+ error(f"Event '{event_id}' not found.")
257
+ return False
258
+
259
+ event_obj = self.api.calendar.get_event_detail(pguid, event_id, as_obj=True)
260
+ self.api.calendar.remove_event(event_obj)
208
261
  return True
209
262
  except Exception as e:
210
263
  from icloud_cli.output import error
211
264
  error(f"Failed to delete event: {e}")
212
265
  return False
213
-
214
-
215
- def _datetime_to_icloud(dt: datetime) -> list[int]:
216
- """Convert a datetime to iCloud's date format [year, month, day, hour, minute]."""
217
- return [dt.year, dt.month, dt.day, dt.hour, dt.minute]
@@ -30,9 +30,9 @@ class FindMyService:
30
30
  return []
31
31
 
32
32
  result = []
33
- for device in devices:
33
+ for device in devices.values():
34
34
  status_data = device.status()
35
- location = device.location()
35
+ location = device.location
36
36
 
37
37
  loc_str = ""
38
38
  if location and location.get("latitude") and location.get("longitude"):
@@ -69,7 +69,7 @@ class FindMyService:
69
69
  return None
70
70
 
71
71
  status_data = device.status()
72
- location = device.location()
72
+ location = device.location
73
73
 
74
74
  if not location:
75
75
  from icloud_cli.output import warning
@@ -157,13 +157,13 @@ class FindMyService:
157
157
  name_lower = name.lower()
158
158
 
159
159
  # Exact match first
160
- for device in devices:
160
+ for device in devices.values():
161
161
  status = device.status()
162
162
  if status.get("name", "").lower() == name_lower:
163
163
  return device
164
164
 
165
165
  # Partial match
166
- for device in devices:
166
+ for device in devices.values():
167
167
  status = device.status()
168
168
  if name_lower in status.get("name", "").lower():
169
169
  return device