karma365 0.2.0__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.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: karma365
3
+ Version: 0.2.0
4
+ Summary: Add to your KARMA 365 library from Python. Your data stays on your phone.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/ayushkotadiya5499/karma365-sdk
7
+ Project-URL: Bug Tracker, https://github.com/ayushkotadiya5499/karma365-sdk/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: cryptography>=42
14
+ Requires-Dist: httpx>=0.25
15
+
16
+ # KARMA 365 SDK for Python
17
+
18
+ Add to your KARMA 365 library from Python: results, plots, videos, notes and tasks.
19
+
20
+ Your data stays on your phone. This SDK has no database and no cloud. It talks to the
21
+ app over your own Wi-Fi, hotspot or USB, and everything on the way is encrypted.
22
+
23
+ ## Before you start
24
+
25
+ 1. Open KARMA 365 on your phone.
26
+ 2. Go to Settings, AI MCP, and turn on **Allow PC connection**.
27
+ 3. Copy the token, and put it in an environment variable:
28
+
29
+ ```bash
30
+ export KARMA365_TOKEN="your token" # macOS, Linux
31
+ setx KARMA365_TOKEN "your token" # Windows, then open a new terminal
32
+ ```
33
+
34
+ **Keep the token out of your code, and off GitHub.** Whoever holds it, on the same
35
+ network as your phone, can read and change everything that is not PIN-locked. If it
36
+ leaks, make a new one in the app. The old one stops working at once.
37
+
38
+ The phone must be on, with the app unlocked, on the same network as this PC.
39
+
40
+ ## Is it connected?
41
+
42
+ ```bash
43
+ python -m karma365 status
44
+ ```
45
+
46
+ ```python
47
+ status = await karma.status()
48
+ print(status.connected, status.reason, status.message, status.hint)
49
+ ```
50
+
51
+ `connected` is true only after your phone has answered with your token's key, so
52
+ another device cannot pass as your phone. The answer is true for that moment; if the
53
+ Wi-Fi goes later, the next call says so.
54
+
55
+ In the app, Settings, AI MCP shows the last connection, and a file being received.
56
+
57
+ ## Use it
58
+
59
+ With `await`:
60
+
61
+ ```python
62
+ import asyncio
63
+ from karma365 import AsyncKarmaClient
64
+
65
+ async def main():
66
+ async with AsyncKarmaClient() as karma:
67
+ topic = await karma.find_minor_id("Machine Learning")
68
+ await karma.add_image(topic, "outputs/confusion_matrix.png", caption="Accuracy 96.4%")
69
+ await karma.create_task("Train ResNet-50", priority="high")
70
+
71
+ asyncio.run(main())
72
+ ```
73
+
74
+ Without `await`, for scripts and notebooks. It works inside Jupyter too:
75
+
76
+ ```python
77
+ from karma365 import KarmaClient
78
+
79
+ karma = KarmaClient()
80
+ karma.create_task("Train ResNet-50", priority="high")
81
+ ```
82
+
83
+ ## Large files
84
+
85
+ ```python
86
+ await karma.add_video(
87
+ "Machine Learning", "lecture_8gb.mp4", title="Lecture 1",
88
+ on_progress=lambda sent, total: print(f"{sent * 100 // total}%"),
89
+ )
90
+ ```
91
+
92
+ - Any size. The limit is your phone's free space, which is checked before anything is sent.
93
+ - The file goes in encrypted pieces, so it never fills your memory.
94
+ - **If it stops, run the same call again.** Only the missing pieces are sent.
95
+ - Nothing appears in the app until the whole file has arrived.
96
+ - On the phone, a notification keeps it going while the screen is off. Android can
97
+ still close the app, Oppo phones especially. Open it again and repeat the call.
98
+
99
+ `await` does not itself make a file faster. It keeps your program free, and lets
100
+ several pieces travel at once. The speed comes from your Wi-Fi and your phone. Measure
101
+ yours, using a test file that is deleted afterwards:
102
+
103
+ ```bash
104
+ python -m karma365 speedtest 100
105
+ ```
106
+
107
+ This needs a minor topic to exist, which the test file passes through.
108
+
109
+ ## Locked items
110
+
111
+ The phone checks every PIN, and allows 6 wrong tries before a short wait.
112
+
113
+ ```python
114
+ import getpass
115
+ await karma.unlock(getpass.getpass("PIN: ")) # kept in memory until lock() or close()
116
+ await karma.list_resources("Private", pin=my_pin) # or for one call
117
+ ```
118
+
119
+ Do not write the PIN in your code. Without it, locked rows say only that they are
120
+ locked, and `is_locked` is true on them. The PIN can only be set or changed in the app.
121
+
122
+ ## Errors
123
+
124
+ Every error says what happened and what to do, and has a `code` that never changes.
125
+
126
+ ```python
127
+ from karma365 import KarmaError, PhoneNotFoundError
128
+
129
+ try:
130
+ await karma.add_video("ML", "lecture.mp4")
131
+ except PhoneNotFoundError as e:
132
+ print(e.hint)
133
+ except KarmaError as e:
134
+ print(e.code, e.message, e.details)
135
+ ```
136
+
137
+ | Code | Meaning |
138
+ |---|---|
139
+ | `TOKEN_MISSING`, `TOKEN_REJECTED` | No token, or the phone did not accept it |
140
+ | `CLOCK_OUT_OF_SYNC` | This PC's clock and the phone's are too far apart |
141
+ | `PHONE_NOT_FOUND` | No answer. App closed, Allow PC off, or another network |
142
+ | `NOT_YOUR_PHONE` | Something answered, but could not prove it is your phone |
143
+ | `APP_LOCKED` | Unlock the app on your phone |
144
+ | `CONNECTION_LOST`, `PHONE_TIMEOUT` | Lost during a call. A change may have happened, so check before repeating it |
145
+ | `APP_UPDATE_NEEDED` | The app on your phone is too old for this |
146
+ | `PIN_REQUIRED`, `PIN_WRONG`, `PIN_LOCKED_OUT`, `PARENT_LOCKED` | About locks |
147
+ | `NOT_FOUND`, `AMBIGUOUS_NAME`, `INVALID_INPUT` | About what you passed |
148
+ | `FILE_NOT_FOUND`, `FILE_NOT_ALLOWED`, `FILE_CHANGED` | About the file on this PC |
149
+ | `PHONE_STORAGE_FULL`, `UPLOAD_INTERRUPTED` | About sending a file |
150
+ | `PHONE_ERROR` | Unexpected. Set `KARMA365_DEBUG=1` to see more |
151
+
152
+ Reads, and pieces of a file, are tried again by themselves. A create or a delete is
153
+ not, because the first try may have worked. A wrong PIN is never tried again, so a
154
+ script cannot use up your tries.
155
+
156
+ ## Deleting locked items
157
+
158
+ Something locked is never deleted, not even with the PIN. Neither is something under a
159
+ lock, or something holding a locked item. Remove the lock first, then delete. Until then
160
+ the call fails with `PARENT_LOCKED`.
161
+
162
+ ## Names and IDs
163
+
164
+ Anything that changes something needs an ID, or a whole name that fits only one item.
165
+ Part of a name works only in `find_id()` and `search()`, so a short word can never
166
+ delete the wrong thing.
167
+
168
+ ## Methods
169
+
170
+ **Connection:** `status`, `is_connected`, `unlock`, `lock`, `close`
171
+
172
+ **Finding:** `find_id`, `find_minor_id`, `find_major_id`, `get_info`, `search`
173
+
174
+ **Topics:** `create_major`, `create_minor`, `list_majors`, `list_minors`, `get_major`,
175
+ `get_minor`, `change_name`, `change_minor_name`, `change_major_name`, `change_color`,
176
+ `change_minor_color`, `change_major_color`, `change_dp`, `change_difficulty`,
177
+ `transfer_minor_to`, `transfer_to`, `pin`, `unpin`, `star`, `unstar`, `delete`, `clean_empty`
178
+
179
+ **Resources:** `add_image`, `add_video`, `add_audio`, `add_pdf`, `add_youtube`, `add_link`,
180
+ `add_code`, `add_note`, `append_note`, `list_resources`, `view_all`, `get_resource`,
181
+ `change_description`, `mark_done`, `unmark_done`
182
+
183
+ **Tasks:** `create_task`, `list_tasks`, `get_task`, `change_priority`, `mark_done`,
184
+ `unmark_done`, `delete`, `clear_completed_tasks`
185
+
186
+ **Stats:** `get_stats`
187
+
188
+ ## Settings
189
+
190
+ | Variable | Use |
191
+ |---|---|
192
+ | `KARMA365_TOKEN` | Your token |
193
+ | `KARMA365_PHONE_URL` | The phone's address, if it is not found by itself |
194
+ | `KARMA365_DEBUG` | Set to 1 to add the technical cause to errors. Never shows your token or PIN |
@@ -0,0 +1,179 @@
1
+ # KARMA 365 SDK for Python
2
+
3
+ Add to your KARMA 365 library from Python: results, plots, videos, notes and tasks.
4
+
5
+ Your data stays on your phone. This SDK has no database and no cloud. It talks to the
6
+ app over your own Wi-Fi, hotspot or USB, and everything on the way is encrypted.
7
+
8
+ ## Before you start
9
+
10
+ 1. Open KARMA 365 on your phone.
11
+ 2. Go to Settings, AI MCP, and turn on **Allow PC connection**.
12
+ 3. Copy the token, and put it in an environment variable:
13
+
14
+ ```bash
15
+ export KARMA365_TOKEN="your token" # macOS, Linux
16
+ setx KARMA365_TOKEN "your token" # Windows, then open a new terminal
17
+ ```
18
+
19
+ **Keep the token out of your code, and off GitHub.** Whoever holds it, on the same
20
+ network as your phone, can read and change everything that is not PIN-locked. If it
21
+ leaks, make a new one in the app. The old one stops working at once.
22
+
23
+ The phone must be on, with the app unlocked, on the same network as this PC.
24
+
25
+ ## Is it connected?
26
+
27
+ ```bash
28
+ python -m karma365 status
29
+ ```
30
+
31
+ ```python
32
+ status = await karma.status()
33
+ print(status.connected, status.reason, status.message, status.hint)
34
+ ```
35
+
36
+ `connected` is true only after your phone has answered with your token's key, so
37
+ another device cannot pass as your phone. The answer is true for that moment; if the
38
+ Wi-Fi goes later, the next call says so.
39
+
40
+ In the app, Settings, AI MCP shows the last connection, and a file being received.
41
+
42
+ ## Use it
43
+
44
+ With `await`:
45
+
46
+ ```python
47
+ import asyncio
48
+ from karma365 import AsyncKarmaClient
49
+
50
+ async def main():
51
+ async with AsyncKarmaClient() as karma:
52
+ topic = await karma.find_minor_id("Machine Learning")
53
+ await karma.add_image(topic, "outputs/confusion_matrix.png", caption="Accuracy 96.4%")
54
+ await karma.create_task("Train ResNet-50", priority="high")
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ Without `await`, for scripts and notebooks. It works inside Jupyter too:
60
+
61
+ ```python
62
+ from karma365 import KarmaClient
63
+
64
+ karma = KarmaClient()
65
+ karma.create_task("Train ResNet-50", priority="high")
66
+ ```
67
+
68
+ ## Large files
69
+
70
+ ```python
71
+ await karma.add_video(
72
+ "Machine Learning", "lecture_8gb.mp4", title="Lecture 1",
73
+ on_progress=lambda sent, total: print(f"{sent * 100 // total}%"),
74
+ )
75
+ ```
76
+
77
+ - Any size. The limit is your phone's free space, which is checked before anything is sent.
78
+ - The file goes in encrypted pieces, so it never fills your memory.
79
+ - **If it stops, run the same call again.** Only the missing pieces are sent.
80
+ - Nothing appears in the app until the whole file has arrived.
81
+ - On the phone, a notification keeps it going while the screen is off. Android can
82
+ still close the app, Oppo phones especially. Open it again and repeat the call.
83
+
84
+ `await` does not itself make a file faster. It keeps your program free, and lets
85
+ several pieces travel at once. The speed comes from your Wi-Fi and your phone. Measure
86
+ yours, using a test file that is deleted afterwards:
87
+
88
+ ```bash
89
+ python -m karma365 speedtest 100
90
+ ```
91
+
92
+ This needs a minor topic to exist, which the test file passes through.
93
+
94
+ ## Locked items
95
+
96
+ The phone checks every PIN, and allows 6 wrong tries before a short wait.
97
+
98
+ ```python
99
+ import getpass
100
+ await karma.unlock(getpass.getpass("PIN: ")) # kept in memory until lock() or close()
101
+ await karma.list_resources("Private", pin=my_pin) # or for one call
102
+ ```
103
+
104
+ Do not write the PIN in your code. Without it, locked rows say only that they are
105
+ locked, and `is_locked` is true on them. The PIN can only be set or changed in the app.
106
+
107
+ ## Errors
108
+
109
+ Every error says what happened and what to do, and has a `code` that never changes.
110
+
111
+ ```python
112
+ from karma365 import KarmaError, PhoneNotFoundError
113
+
114
+ try:
115
+ await karma.add_video("ML", "lecture.mp4")
116
+ except PhoneNotFoundError as e:
117
+ print(e.hint)
118
+ except KarmaError as e:
119
+ print(e.code, e.message, e.details)
120
+ ```
121
+
122
+ | Code | Meaning |
123
+ |---|---|
124
+ | `TOKEN_MISSING`, `TOKEN_REJECTED` | No token, or the phone did not accept it |
125
+ | `CLOCK_OUT_OF_SYNC` | This PC's clock and the phone's are too far apart |
126
+ | `PHONE_NOT_FOUND` | No answer. App closed, Allow PC off, or another network |
127
+ | `NOT_YOUR_PHONE` | Something answered, but could not prove it is your phone |
128
+ | `APP_LOCKED` | Unlock the app on your phone |
129
+ | `CONNECTION_LOST`, `PHONE_TIMEOUT` | Lost during a call. A change may have happened, so check before repeating it |
130
+ | `APP_UPDATE_NEEDED` | The app on your phone is too old for this |
131
+ | `PIN_REQUIRED`, `PIN_WRONG`, `PIN_LOCKED_OUT`, `PARENT_LOCKED` | About locks |
132
+ | `NOT_FOUND`, `AMBIGUOUS_NAME`, `INVALID_INPUT` | About what you passed |
133
+ | `FILE_NOT_FOUND`, `FILE_NOT_ALLOWED`, `FILE_CHANGED` | About the file on this PC |
134
+ | `PHONE_STORAGE_FULL`, `UPLOAD_INTERRUPTED` | About sending a file |
135
+ | `PHONE_ERROR` | Unexpected. Set `KARMA365_DEBUG=1` to see more |
136
+
137
+ Reads, and pieces of a file, are tried again by themselves. A create or a delete is
138
+ not, because the first try may have worked. A wrong PIN is never tried again, so a
139
+ script cannot use up your tries.
140
+
141
+ ## Deleting locked items
142
+
143
+ Something locked is never deleted, not even with the PIN. Neither is something under a
144
+ lock, or something holding a locked item. Remove the lock first, then delete. Until then
145
+ the call fails with `PARENT_LOCKED`.
146
+
147
+ ## Names and IDs
148
+
149
+ Anything that changes something needs an ID, or a whole name that fits only one item.
150
+ Part of a name works only in `find_id()` and `search()`, so a short word can never
151
+ delete the wrong thing.
152
+
153
+ ## Methods
154
+
155
+ **Connection:** `status`, `is_connected`, `unlock`, `lock`, `close`
156
+
157
+ **Finding:** `find_id`, `find_minor_id`, `find_major_id`, `get_info`, `search`
158
+
159
+ **Topics:** `create_major`, `create_minor`, `list_majors`, `list_minors`, `get_major`,
160
+ `get_minor`, `change_name`, `change_minor_name`, `change_major_name`, `change_color`,
161
+ `change_minor_color`, `change_major_color`, `change_dp`, `change_difficulty`,
162
+ `transfer_minor_to`, `transfer_to`, `pin`, `unpin`, `star`, `unstar`, `delete`, `clean_empty`
163
+
164
+ **Resources:** `add_image`, `add_video`, `add_audio`, `add_pdf`, `add_youtube`, `add_link`,
165
+ `add_code`, `add_note`, `append_note`, `list_resources`, `view_all`, `get_resource`,
166
+ `change_description`, `mark_done`, `unmark_done`
167
+
168
+ **Tasks:** `create_task`, `list_tasks`, `get_task`, `change_priority`, `mark_done`,
169
+ `unmark_done`, `delete`, `clear_completed_tasks`
170
+
171
+ **Stats:** `get_stats`
172
+
173
+ ## Settings
174
+
175
+ | Variable | Use |
176
+ |---|---|
177
+ | `KARMA365_TOKEN` | Your token |
178
+ | `KARMA365_PHONE_URL` | The phone's address, if it is not found by itself |
179
+ | `KARMA365_DEBUG` | Set to 1 to add the technical cause to errors. Never shows your token or PIN |
@@ -0,0 +1,56 @@
1
+ """KARMA 365 SDK for Python. Your data stays on your phone; this talks to it."""
2
+
3
+ from .client import AsyncKarmaClient, KarmaClient
4
+ from .errors import (
5
+ AmbiguousNameError,
6
+ AppLockedError,
7
+ AppUpdateNeededError,
8
+ ConnectionLostError,
9
+ FileProblemError,
10
+ InvalidInputError,
11
+ KarmaError,
12
+ NotFoundError,
13
+ NotYourPhoneError,
14
+ ParentLockedError,
15
+ PhoneError,
16
+ PhoneNotFoundError,
17
+ PhoneStorageFullError,
18
+ PinLockedOutError,
19
+ PinRequiredError,
20
+ PinWrongError,
21
+ TokenError,
22
+ UploadInterruptedError,
23
+ )
24
+ from .link import Status
25
+ from .models import InfoResult, MajorTopic, Resource, Task, Topic, WorkspaceStats
26
+
27
+ __version__ = "0.2.0"
28
+ __all__ = [
29
+ "AsyncKarmaClient",
30
+ "KarmaClient",
31
+ "Status",
32
+ "MajorTopic",
33
+ "Topic",
34
+ "Resource",
35
+ "Task",
36
+ "WorkspaceStats",
37
+ "InfoResult",
38
+ "KarmaError",
39
+ "TokenError",
40
+ "PhoneNotFoundError",
41
+ "NotYourPhoneError",
42
+ "AppLockedError",
43
+ "ConnectionLostError",
44
+ "AppUpdateNeededError",
45
+ "PinRequiredError",
46
+ "PinWrongError",
47
+ "PinLockedOutError",
48
+ "ParentLockedError",
49
+ "NotFoundError",
50
+ "AmbiguousNameError",
51
+ "InvalidInputError",
52
+ "FileProblemError",
53
+ "PhoneStorageFullError",
54
+ "UploadInterruptedError",
55
+ "PhoneError",
56
+ ]
@@ -0,0 +1,76 @@
1
+ """`python -m karma365 status` and `python -m karma365 speedtest`."""
2
+
3
+ import asyncio
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import time
8
+
9
+ from .client import AsyncKarmaClient
10
+ from .errors import KarmaError
11
+ from .upload import cancel_file, send_file
12
+
13
+
14
+ async def status() -> int:
15
+ async with AsyncKarmaClient() as karma:
16
+ result = await karma.status()
17
+ if result.connected:
18
+ print(f"Connected to your phone at {result.phone_url} ({result.latency_ms} ms)")
19
+ return 0
20
+ print(f"Not connected: {result.message}")
21
+ if result.hint:
22
+ print(f"What to do: {result.hint}")
23
+ print(f"Code: {result.reason}")
24
+ return 1
25
+
26
+
27
+ async def speedtest(megabytes: int) -> int:
28
+ """Sends a throwaway file over the real path and measures it. Wi-Fi, the phone's
29
+ decrypting and its storage all count, so this is the speed you will really get."""
30
+ async with AsyncKarmaClient() as karma:
31
+ minors = await karma.list_minors()
32
+ if not minors:
33
+ print("Create a minor topic first. The test needs one, and leaves nothing in it.")
34
+ return 1
35
+ handle, path = tempfile.mkstemp(suffix=".bin", prefix="karma_speedtest_")
36
+ try:
37
+ with os.fdopen(handle, "wb") as file:
38
+ for _ in range(megabytes):
39
+ file.write(os.urandom(1024 * 1024))
40
+
41
+ def progress(sent: int, total: int) -> None:
42
+ print(f"\r{sent * 100 // total}%", end="", flush=True)
43
+
44
+ started = time.perf_counter()
45
+ resource_id = await send_file(
46
+ karma._link,
47
+ path=path,
48
+ file_type="video",
49
+ topic_id=minors[0].id,
50
+ title="KARMA 365 speed test",
51
+ on_progress=progress,
52
+ )
53
+ seconds = time.perf_counter() - started
54
+ await karma.delete(resource_id)
55
+ print(f"\n{megabytes} MB in {seconds:.1f} s: {megabytes / seconds:.1f} MB/s")
56
+ return 0
57
+ finally:
58
+ os.unlink(path)
59
+
60
+
61
+ def main() -> int:
62
+ command = sys.argv[1] if len(sys.argv) > 1 else "status"
63
+ try:
64
+ if command == "status":
65
+ return asyncio.run(status())
66
+ if command == "speedtest":
67
+ return asyncio.run(speedtest(int(sys.argv[2]) if len(sys.argv) > 2 else 100))
68
+ except KarmaError as error:
69
+ print(error)
70
+ return 1
71
+ print("Usage: python -m karma365 status\n python -m karma365 speedtest [MB]")
72
+ return 2
73
+
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main())