ticktick-focus-client 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.
- ticktick_focus_client-0.2.0/.gitignore +6 -0
- ticktick_focus_client-0.2.0/LICENSE +19 -0
- ticktick_focus_client-0.2.0/PKG-INFO +166 -0
- ticktick_focus_client-0.2.0/README.md +154 -0
- ticktick_focus_client-0.2.0/flake.lock +61 -0
- ticktick_focus_client-0.2.0/flake.nix +21 -0
- ticktick_focus_client-0.2.0/pyproject.toml +34 -0
- ticktick_focus_client-0.2.0/src/ticktick_focus_client/__init__.py +46 -0
- ticktick_focus_client-0.2.0/src/ticktick_focus_client/client.py +418 -0
- ticktick_focus_client-0.2.0/src/ticktick_focus_client/health.py +106 -0
- ticktick_focus_client-0.2.0/src/ticktick_focus_client/py.typed +0 -0
- ticktick_focus_client-0.2.0/src/ticktick_focus_client/state.py +190 -0
- ticktick_focus_client-0.2.0/tests/test_client.py +222 -0
- ticktick_focus_client-0.2.0/tests/test_health.py +84 -0
- ticktick_focus_client-0.2.0/tests/test_state.py +165 -0
- ticktick_focus_client-0.2.0/uv.lock +352 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Jon Wood
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ticktick-focus-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Read live TickTick focus/pomodoro state
|
|
5
|
+
Author-email: Jon Wood <jon@blankpad.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: websockets>=13.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# ticktick-focus-client
|
|
14
|
+
|
|
15
|
+
A small async Python library that reads **live** TickTick focus state from
|
|
16
|
+
TickTick's private API and websocket server.
|
|
17
|
+
|
|
18
|
+
## Why this exists
|
|
19
|
+
|
|
20
|
+
TickTick's documented Open API (`/open/v1/focus`) returns **completed** focus
|
|
21
|
+
records only. It has no endpoint for the session you are in right now, so it
|
|
22
|
+
cannot answer "is Jon focusing?".
|
|
23
|
+
|
|
24
|
+
The desktop and web clients get live state from a separate, undocumented
|
|
25
|
+
channel, which this library speaks:
|
|
26
|
+
|
|
27
|
+
| Piece | What it does |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `wss://wssp.ticktick.com/web?x-device=…&hl=…` | Push channel. Server sends `{"type":"focusSync"}` when anything changes — a doorbell, carrying no state. Client sends `{"type":"ping"}` on open and every 540 s. |
|
|
30
|
+
| `POST https://ms.ticktick.com/focus/batch/focusOp` | The state. Body `{"lastPoint": <n>, "opList": []}` returns `{"point", "current", "updates"}`. `current` is the live session. |
|
|
31
|
+
|
|
32
|
+
Sending an empty `opList` argument makes no changes to focus state, this
|
|
33
|
+
client is purely read only. (Although I wouldn't be against adding some
|
|
34
|
+
write support in the future.)
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from ticktick_focus_client import FocusClient, FocusStatus
|
|
40
|
+
|
|
41
|
+
async with FocusClient(cookie) as tt:
|
|
42
|
+
# One-shot. Raises AuthError if the cookie is rejected.
|
|
43
|
+
print(await tt.current())
|
|
44
|
+
|
|
45
|
+
# Live, driven by the push socket. Never raises for auth or network
|
|
46
|
+
# trouble — that arrives as state instead.
|
|
47
|
+
async for state in tt.watch():
|
|
48
|
+
if state.state is FocusStatus.FOCUSING:
|
|
49
|
+
print(state.task_title, state.remaining_seconds)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`watch()` yields the first time it has a state, and thereafter whenever a read
|
|
53
|
+
produces something different from the last value yielded — a real change, or a
|
|
54
|
+
reconcile tick refreshing the elapsed/remaining clocks mid-session. Steady idle
|
|
55
|
+
is silent.
|
|
56
|
+
|
|
57
|
+
### `FocusState`
|
|
58
|
+
|
|
59
|
+
A frozen dataclass. `state` is a `FocusStatus`, `focus_type` a `FocusKind`,
|
|
60
|
+
`started_at` and `scheduled_end` are `datetime`s, and the rest are
|
|
61
|
+
`task_title`, `task_id`, `elapsed_seconds`, `remaining_seconds`, `pomo_count`
|
|
62
|
+
and `session_id`. `as_dict()` gives a JSON-safe view: times as ISO 8601, unset
|
|
63
|
+
fields dropped.
|
|
64
|
+
|
|
65
|
+
| Enum | Members |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `FocusStatus` | `IDLE`, `FOCUSING`, `PAUSED`, `BREAK`, `UNAVAILABLE`, plus `.in_session` |
|
|
68
|
+
| `FocusKind` | `POMODORO`, `STOPWATCH`, each carrying the API's int as `.api_value` |
|
|
69
|
+
| `SessionStatus` | `RUNNING`, `COMPLETED`, `ABANDONED` — the API's `status` |
|
|
70
|
+
| `PauseLogType` | `PAUSED`, `RESUMED` — entries in the API's `pauseLogs` |
|
|
71
|
+
|
|
72
|
+
`FocusStatus` and `FocusKind` are `StrEnum`s, so `state.state == "focusing"`
|
|
73
|
+
holds and `json.dumps` needs no help. `UNAVAILABLE` means "cannot currently
|
|
74
|
+
tell" — before the first successful read, or while the cookie is not working.
|
|
75
|
+
It is never conflated with `IDLE`.
|
|
76
|
+
|
|
77
|
+
### `client.health`
|
|
78
|
+
|
|
79
|
+
Health of the client, kept separate from the focus state it carries:
|
|
80
|
+
`auth_ok`, `websocket_connected`, `healthy`, `can_report_focus`,
|
|
81
|
+
`last_sync_at`, `last_error`, `last_error_at`, `consecutive_failures`, a
|
|
82
|
+
`failure_reason` of `AUTH_EXPIRED` or `NETWORK`, plus Premium details once
|
|
83
|
+
`await client.refresh_account()` has run. `as_dict()` behaves as it does on
|
|
84
|
+
`FocusState`.
|
|
85
|
+
|
|
86
|
+
### `client.point`
|
|
87
|
+
|
|
88
|
+
The sync checkpoint, which only ever moves forward. Nothing is written to disk;
|
|
89
|
+
persist it yourself and hand it back if you want a new process to resume rather
|
|
90
|
+
than re-read from scratch:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
FocusClient(cookie, point=saved_point)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Options
|
|
97
|
+
|
|
98
|
+
| Argument | Default | Notes |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `cookie` | — | The `t` session cookie value. Required. |
|
|
101
|
+
| `domain` | `Domain.TICKTICK` | Or `Domain.DIDA` for the Chinese service. Plain strings are accepted and validated. |
|
|
102
|
+
| `device_id` | a fixed placeholder | Any 24-char hex-ish id. |
|
|
103
|
+
| `language` | `en_US` | |
|
|
104
|
+
| `reconcile_seconds` | `300` | Safety-net re-read, in case a poke is missed. |
|
|
105
|
+
| `point` | `0` | Sync checkpoint to resume from. |
|
|
106
|
+
| `http` | — | An `httpx.AsyncClient` to borrow; the caller closes it. |
|
|
107
|
+
|
|
108
|
+
### Typing
|
|
109
|
+
|
|
110
|
+
The package ships a `py.typed` marker and checks clean under
|
|
111
|
+
[ty](https://github.com/astral-sh/ty), so the enums and datetimes above reach
|
|
112
|
+
anything built on top of it.
|
|
113
|
+
|
|
114
|
+
## Requirements
|
|
115
|
+
|
|
116
|
+
- Python 3.11+
|
|
117
|
+
- **TickTick Premium.** Cross-device focus sync is gated behind it
|
|
118
|
+
(`focusConf.keepInSync` plus a Premium check). Without it the server does not
|
|
119
|
+
push and `current` will not track your sessions. (Untested)
|
|
120
|
+
- **"Keep in Sync" enabled** in TickTick's focus settings.
|
|
121
|
+
- A session cookie (see below).
|
|
122
|
+
|
|
123
|
+
## Getting the session cookie
|
|
124
|
+
|
|
125
|
+
This endpoint is not covered by the Open API, and an Open API personal token
|
|
126
|
+
(`Authorization: Bearer …`) **will not work** — it belongs to a different auth
|
|
127
|
+
realm. You need the `t` cookie from a logged-in session:
|
|
128
|
+
|
|
129
|
+
1. Sign in at <https://ticktick.com> in a browser.
|
|
130
|
+
2. DevTools → Application → Cookies → `https://ticktick.com`.
|
|
131
|
+
3. Copy the **Value** of the `t` cookie.
|
|
132
|
+
|
|
133
|
+
The library takes it as a plain string and never touches the filesystem. Where
|
|
134
|
+
it comes from — a file, a keychain, an environment variable — is yours to
|
|
135
|
+
decide. A cookie that has been rotated means a new client.
|
|
136
|
+
|
|
137
|
+
## Design notes
|
|
138
|
+
|
|
139
|
+
- **Push, not poll.** The socket is the trigger; the reconcile timer only
|
|
140
|
+
covers missed pokes. Sync requests collapse — a burst of pokes causes one read.
|
|
141
|
+
- **`endTime` does not mean "finished".** For a pomodoro it is the *projected*
|
|
142
|
+
end (start + configured duration) and is present throughout the session.
|
|
143
|
+
Liveness comes from `status == 0` and `exited == false`; `endTime` is only a
|
|
144
|
+
fallback when `status` is absent. Getting this wrong reports `idle` mid-session.
|
|
145
|
+
- **Nothing is fatal while watching.** Network errors and expired cookies are
|
|
146
|
+
reported as state and retried; only a genuine bug escapes the iterator.
|
|
147
|
+
- **Auth failures are distinguished from network failures.** A connection reset
|
|
148
|
+
does not clear `auth_ok`, so a blip never gets reported as an expired cookie.
|
|
149
|
+
- Reconnects with exponential backoff (2 s → 5 min).
|
|
150
|
+
|
|
151
|
+
## Caveats
|
|
152
|
+
|
|
153
|
+
- This uses TickTick's **private API**, which is against their Terms of Service.
|
|
154
|
+
It can change without notice.
|
|
155
|
+
- Sessions shorter than TickTick's minimum valid duration are discarded by the
|
|
156
|
+
app and never appear anywhere.
|
|
157
|
+
|
|
158
|
+
## Tests
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv run pytest
|
|
162
|
+
uv run ty check
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The state machine is a pure function of the API payload, so the interesting
|
|
166
|
+
logic is covered without network or credentials.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# ticktick-focus-client
|
|
2
|
+
|
|
3
|
+
A small async Python library that reads **live** TickTick focus state from
|
|
4
|
+
TickTick's private API and websocket server.
|
|
5
|
+
|
|
6
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
TickTick's documented Open API (`/open/v1/focus`) returns **completed** focus
|
|
9
|
+
records only. It has no endpoint for the session you are in right now, so it
|
|
10
|
+
cannot answer "is Jon focusing?".
|
|
11
|
+
|
|
12
|
+
The desktop and web clients get live state from a separate, undocumented
|
|
13
|
+
channel, which this library speaks:
|
|
14
|
+
|
|
15
|
+
| Piece | What it does |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `wss://wssp.ticktick.com/web?x-device=…&hl=…` | Push channel. Server sends `{"type":"focusSync"}` when anything changes — a doorbell, carrying no state. Client sends `{"type":"ping"}` on open and every 540 s. |
|
|
18
|
+
| `POST https://ms.ticktick.com/focus/batch/focusOp` | The state. Body `{"lastPoint": <n>, "opList": []}` returns `{"point", "current", "updates"}`. `current` is the live session. |
|
|
19
|
+
|
|
20
|
+
Sending an empty `opList` argument makes no changes to focus state, this
|
|
21
|
+
client is purely read only. (Although I wouldn't be against adding some
|
|
22
|
+
write support in the future.)
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from ticktick_focus_client import FocusClient, FocusStatus
|
|
28
|
+
|
|
29
|
+
async with FocusClient(cookie) as tt:
|
|
30
|
+
# One-shot. Raises AuthError if the cookie is rejected.
|
|
31
|
+
print(await tt.current())
|
|
32
|
+
|
|
33
|
+
# Live, driven by the push socket. Never raises for auth or network
|
|
34
|
+
# trouble — that arrives as state instead.
|
|
35
|
+
async for state in tt.watch():
|
|
36
|
+
if state.state is FocusStatus.FOCUSING:
|
|
37
|
+
print(state.task_title, state.remaining_seconds)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`watch()` yields the first time it has a state, and thereafter whenever a read
|
|
41
|
+
produces something different from the last value yielded — a real change, or a
|
|
42
|
+
reconcile tick refreshing the elapsed/remaining clocks mid-session. Steady idle
|
|
43
|
+
is silent.
|
|
44
|
+
|
|
45
|
+
### `FocusState`
|
|
46
|
+
|
|
47
|
+
A frozen dataclass. `state` is a `FocusStatus`, `focus_type` a `FocusKind`,
|
|
48
|
+
`started_at` and `scheduled_end` are `datetime`s, and the rest are
|
|
49
|
+
`task_title`, `task_id`, `elapsed_seconds`, `remaining_seconds`, `pomo_count`
|
|
50
|
+
and `session_id`. `as_dict()` gives a JSON-safe view: times as ISO 8601, unset
|
|
51
|
+
fields dropped.
|
|
52
|
+
|
|
53
|
+
| Enum | Members |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `FocusStatus` | `IDLE`, `FOCUSING`, `PAUSED`, `BREAK`, `UNAVAILABLE`, plus `.in_session` |
|
|
56
|
+
| `FocusKind` | `POMODORO`, `STOPWATCH`, each carrying the API's int as `.api_value` |
|
|
57
|
+
| `SessionStatus` | `RUNNING`, `COMPLETED`, `ABANDONED` — the API's `status` |
|
|
58
|
+
| `PauseLogType` | `PAUSED`, `RESUMED` — entries in the API's `pauseLogs` |
|
|
59
|
+
|
|
60
|
+
`FocusStatus` and `FocusKind` are `StrEnum`s, so `state.state == "focusing"`
|
|
61
|
+
holds and `json.dumps` needs no help. `UNAVAILABLE` means "cannot currently
|
|
62
|
+
tell" — before the first successful read, or while the cookie is not working.
|
|
63
|
+
It is never conflated with `IDLE`.
|
|
64
|
+
|
|
65
|
+
### `client.health`
|
|
66
|
+
|
|
67
|
+
Health of the client, kept separate from the focus state it carries:
|
|
68
|
+
`auth_ok`, `websocket_connected`, `healthy`, `can_report_focus`,
|
|
69
|
+
`last_sync_at`, `last_error`, `last_error_at`, `consecutive_failures`, a
|
|
70
|
+
`failure_reason` of `AUTH_EXPIRED` or `NETWORK`, plus Premium details once
|
|
71
|
+
`await client.refresh_account()` has run. `as_dict()` behaves as it does on
|
|
72
|
+
`FocusState`.
|
|
73
|
+
|
|
74
|
+
### `client.point`
|
|
75
|
+
|
|
76
|
+
The sync checkpoint, which only ever moves forward. Nothing is written to disk;
|
|
77
|
+
persist it yourself and hand it back if you want a new process to resume rather
|
|
78
|
+
than re-read from scratch:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
FocusClient(cookie, point=saved_point)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Options
|
|
85
|
+
|
|
86
|
+
| Argument | Default | Notes |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `cookie` | — | The `t` session cookie value. Required. |
|
|
89
|
+
| `domain` | `Domain.TICKTICK` | Or `Domain.DIDA` for the Chinese service. Plain strings are accepted and validated. |
|
|
90
|
+
| `device_id` | a fixed placeholder | Any 24-char hex-ish id. |
|
|
91
|
+
| `language` | `en_US` | |
|
|
92
|
+
| `reconcile_seconds` | `300` | Safety-net re-read, in case a poke is missed. |
|
|
93
|
+
| `point` | `0` | Sync checkpoint to resume from. |
|
|
94
|
+
| `http` | — | An `httpx.AsyncClient` to borrow; the caller closes it. |
|
|
95
|
+
|
|
96
|
+
### Typing
|
|
97
|
+
|
|
98
|
+
The package ships a `py.typed` marker and checks clean under
|
|
99
|
+
[ty](https://github.com/astral-sh/ty), so the enums and datetimes above reach
|
|
100
|
+
anything built on top of it.
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
- Python 3.11+
|
|
105
|
+
- **TickTick Premium.** Cross-device focus sync is gated behind it
|
|
106
|
+
(`focusConf.keepInSync` plus a Premium check). Without it the server does not
|
|
107
|
+
push and `current` will not track your sessions. (Untested)
|
|
108
|
+
- **"Keep in Sync" enabled** in TickTick's focus settings.
|
|
109
|
+
- A session cookie (see below).
|
|
110
|
+
|
|
111
|
+
## Getting the session cookie
|
|
112
|
+
|
|
113
|
+
This endpoint is not covered by the Open API, and an Open API personal token
|
|
114
|
+
(`Authorization: Bearer …`) **will not work** — it belongs to a different auth
|
|
115
|
+
realm. You need the `t` cookie from a logged-in session:
|
|
116
|
+
|
|
117
|
+
1. Sign in at <https://ticktick.com> in a browser.
|
|
118
|
+
2. DevTools → Application → Cookies → `https://ticktick.com`.
|
|
119
|
+
3. Copy the **Value** of the `t` cookie.
|
|
120
|
+
|
|
121
|
+
The library takes it as a plain string and never touches the filesystem. Where
|
|
122
|
+
it comes from — a file, a keychain, an environment variable — is yours to
|
|
123
|
+
decide. A cookie that has been rotated means a new client.
|
|
124
|
+
|
|
125
|
+
## Design notes
|
|
126
|
+
|
|
127
|
+
- **Push, not poll.** The socket is the trigger; the reconcile timer only
|
|
128
|
+
covers missed pokes. Sync requests collapse — a burst of pokes causes one read.
|
|
129
|
+
- **`endTime` does not mean "finished".** For a pomodoro it is the *projected*
|
|
130
|
+
end (start + configured duration) and is present throughout the session.
|
|
131
|
+
Liveness comes from `status == 0` and `exited == false`; `endTime` is only a
|
|
132
|
+
fallback when `status` is absent. Getting this wrong reports `idle` mid-session.
|
|
133
|
+
- **Nothing is fatal while watching.** Network errors and expired cookies are
|
|
134
|
+
reported as state and retried; only a genuine bug escapes the iterator.
|
|
135
|
+
- **Auth failures are distinguished from network failures.** A connection reset
|
|
136
|
+
does not clear `auth_ok`, so a blip never gets reported as an expired cookie.
|
|
137
|
+
- Reconnects with exponential backoff (2 s → 5 min).
|
|
138
|
+
|
|
139
|
+
## Caveats
|
|
140
|
+
|
|
141
|
+
- This uses TickTick's **private API**, which is against their Terms of Service.
|
|
142
|
+
It can change without notice.
|
|
143
|
+
- Sessions shorter than TickTick's minimum valid duration are discarded by the
|
|
144
|
+
app and never appear anywhere.
|
|
145
|
+
|
|
146
|
+
## Tests
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
uv run pytest
|
|
150
|
+
uv run ty check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The state machine is a pure function of the API payload, so the interesting
|
|
154
|
+
logic is covered without network or credentials.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": {
|
|
3
|
+
"flake-utils": {
|
|
4
|
+
"inputs": {
|
|
5
|
+
"systems": "systems"
|
|
6
|
+
},
|
|
7
|
+
"locked": {
|
|
8
|
+
"lastModified": 1731533236,
|
|
9
|
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
10
|
+
"owner": "numtide",
|
|
11
|
+
"repo": "flake-utils",
|
|
12
|
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
13
|
+
"type": "github"
|
|
14
|
+
},
|
|
15
|
+
"original": {
|
|
16
|
+
"owner": "numtide",
|
|
17
|
+
"repo": "flake-utils",
|
|
18
|
+
"type": "github"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"nixpkgs": {
|
|
22
|
+
"locked": {
|
|
23
|
+
"lastModified": 1789286504,
|
|
24
|
+
"narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=",
|
|
25
|
+
"owner": "NixOS",
|
|
26
|
+
"repo": "nixpkgs",
|
|
27
|
+
"rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec",
|
|
28
|
+
"type": "github"
|
|
29
|
+
},
|
|
30
|
+
"original": {
|
|
31
|
+
"owner": "NixOS",
|
|
32
|
+
"ref": "nixos-unstable",
|
|
33
|
+
"repo": "nixpkgs",
|
|
34
|
+
"type": "github"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"root": {
|
|
38
|
+
"inputs": {
|
|
39
|
+
"flake-utils": "flake-utils",
|
|
40
|
+
"nixpkgs": "nixpkgs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"systems": {
|
|
44
|
+
"locked": {
|
|
45
|
+
"lastModified": 1681028828,
|
|
46
|
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
47
|
+
"owner": "nix-systems",
|
|
48
|
+
"repo": "default",
|
|
49
|
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
50
|
+
"type": "github"
|
|
51
|
+
},
|
|
52
|
+
"original": {
|
|
53
|
+
"owner": "nix-systems",
|
|
54
|
+
"repo": "default",
|
|
55
|
+
"type": "github"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"root": "root",
|
|
60
|
+
"version": 7
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
description = "Read live TickTick focus state";
|
|
3
|
+
|
|
4
|
+
inputs = {
|
|
5
|
+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
6
|
+
flake-utils.url = "github:numtide/flake-utils";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
outputs = { self, nixpkgs, flake-utils }:
|
|
10
|
+
flake-utils.lib.eachDefaultSystem (system:
|
|
11
|
+
let pkgs = nixpkgs.legacyPackages.${system};
|
|
12
|
+
in {
|
|
13
|
+
devShells.default = pkgs.mkShell {
|
|
14
|
+
packages = [ pkgs.uv pkgs.python313 ];
|
|
15
|
+
shellHook = ''
|
|
16
|
+
export UV_PYTHON="${pkgs.python313}/bin/python3"
|
|
17
|
+
echo "ticktick-focus-client dev shell — 'uv sync' then 'uv run pytest'"
|
|
18
|
+
'';
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ticktick-focus-client"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Jon Wood", email="jon@blankpad.net" }
|
|
6
|
+
]
|
|
7
|
+
description = "Read live TickTick focus/pomodoro state"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE*"]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = ["httpx>=0.27", "websockets>=13.0"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"pytest>=8.0",
|
|
17
|
+
"ty>=0.0.80",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/ticktick_focus_client"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.ty.src]
|
|
31
|
+
include = ["src", "tests"]
|
|
32
|
+
|
|
33
|
+
[tool.ty.environment]
|
|
34
|
+
python-version = "3.11"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Read live TickTick focus/pomodoro state.
|
|
2
|
+
|
|
3
|
+
async with FocusClient(cookie) as tt:
|
|
4
|
+
print(await tt.current())
|
|
5
|
+
|
|
6
|
+
async for state in tt.watch():
|
|
7
|
+
print(state, tt.health)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .client import (
|
|
11
|
+
AuthError,
|
|
12
|
+
Domain,
|
|
13
|
+
FocusClient,
|
|
14
|
+
TooBusy,
|
|
15
|
+
)
|
|
16
|
+
from .health import FailureReason, Health
|
|
17
|
+
from .state import (
|
|
18
|
+
FocusKind,
|
|
19
|
+
FocusState,
|
|
20
|
+
FocusStatus,
|
|
21
|
+
PauseLogType,
|
|
22
|
+
Payload,
|
|
23
|
+
SessionStatus,
|
|
24
|
+
derive,
|
|
25
|
+
parse_time,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.2.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AuthError",
|
|
32
|
+
"Domain",
|
|
33
|
+
"FailureReason",
|
|
34
|
+
"FocusClient",
|
|
35
|
+
"FocusKind",
|
|
36
|
+
"FocusState",
|
|
37
|
+
"FocusStatus",
|
|
38
|
+
"Health",
|
|
39
|
+
"PauseLogType",
|
|
40
|
+
"Payload",
|
|
41
|
+
"SessionStatus",
|
|
42
|
+
"TooBusy",
|
|
43
|
+
"__version__",
|
|
44
|
+
"derive",
|
|
45
|
+
"parse_time",
|
|
46
|
+
]
|