pyselfupdate 0.3.2__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyselfupdate
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Self-update and update notification for Python CLIs installed with uv tool
5
5
  Keywords: uv,cli,self-update,update-notifier,release
6
6
  Author: Chris Birch
@@ -18,7 +18,7 @@ Requires-Dist: typer>=0.12.0 ; extra == 'typer'
18
18
  Requires-Python: >=3.11
19
19
  Project-URL: Repository, https://github.com/datapointchris/pyselfupdate
20
20
  Project-URL: Issues, https://github.com/datapointchris/pyselfupdate/issues
21
- Project-URL: Changelog, https://github.com/datapointchris/pyselfupdate/blob/main/CHANGELOG.md
21
+ Project-URL: Changelog, https://github.com/datapointchris/pyselfupdate/releases
22
22
  Provides-Extra: typer
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -218,7 +218,8 @@ command, and nothing above the `Source` protocol learns either name.
218
218
 
219
219
  ## State
220
220
 
221
- `${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate.json`, written atomically:
221
+ `${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate-<machine>.json`, written
222
+ atomically:
222
223
 
223
224
  ```json
224
225
  {
@@ -237,6 +238,13 @@ the user, and deleting it changes behaviour rather than merely costing a
237
238
  recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
238
239
  is where `gh` puts the same thing.
239
240
 
241
+ `<machine>` is the bare lowercased hostname, from `state.machine()`. It is in
242
+ the name because both fields that vary — the version installed here, and the
243
+ instant this box last checked — describe one machine. A state directory shared
244
+ between machines, by a file syncer or a network home directory, otherwise has
245
+ two writers on one path and reports whichever wrote last as the state of all of
246
+ them.
247
+
240
248
  The timestamp is written **before** the network call. `gh` stamps only on
241
249
  success, so a rate-limited or offline user re-hits the API on every invocation
242
250
  until the window resets; an interval exists to bound the request rate, and only
@@ -244,11 +252,10 @@ this ordering actually does that.
244
252
 
245
253
  ## Siblings
246
254
 
247
- The same two-layer design in other languages. **`update` exists in all three;
248
- the `notify` layer, the shared `autoupdate.json` schema and the
249
- `NO_AUTO_UPDATE` contract are implemented in this library and in its bash
250
- sibling, and are still to be added to goselfupdate** — until then goselfupdate
251
- provides the update half only.
255
+ The same two-layer design in other languages. All three carry both layers, and
256
+ they share the `autoupdate-<machine>.json` schema, the machine derivation that
257
+ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
258
+ "update" means a different operation in each.
252
259
 
253
260
  - [goselfupdate](https://github.com/datapointchris/goselfupdate) — replaces a Go binary
254
261
  - [bashselfupdate](https://github.com/datapointchris/bashselfupdate) — moves a git checkout to its newest tag
@@ -194,7 +194,8 @@ command, and nothing above the `Source` protocol learns either name.
194
194
 
195
195
  ## State
196
196
 
197
- `${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate.json`, written atomically:
197
+ `${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate-<machine>.json`, written
198
+ atomically:
198
199
 
199
200
  ```json
200
201
  {
@@ -213,6 +214,13 @@ the user, and deleting it changes behaviour rather than merely costing a
213
214
  recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
214
215
  is where `gh` puts the same thing.
215
216
 
217
+ `<machine>` is the bare lowercased hostname, from `state.machine()`. It is in
218
+ the name because both fields that vary — the version installed here, and the
219
+ instant this box last checked — describe one machine. A state directory shared
220
+ between machines, by a file syncer or a network home directory, otherwise has
221
+ two writers on one path and reports whichever wrote last as the state of all of
222
+ them.
223
+
216
224
  The timestamp is written **before** the network call. `gh` stamps only on
217
225
  success, so a rate-limited or offline user re-hits the API on every invocation
218
226
  until the window resets; an interval exists to bound the request rate, and only
@@ -220,11 +228,10 @@ this ordering actually does that.
220
228
 
221
229
  ## Siblings
222
230
 
223
- The same two-layer design in other languages. **`update` exists in all three;
224
- the `notify` layer, the shared `autoupdate.json` schema and the
225
- `NO_AUTO_UPDATE` contract are implemented in this library and in its bash
226
- sibling, and are still to be added to goselfupdate** — until then goselfupdate
227
- provides the update half only.
231
+ The same two-layer design in other languages. All three carry both layers, and
232
+ they share the `autoupdate-<machine>.json` schema, the machine derivation that
233
+ names it, and the `NO_AUTO_UPDATE` contract. They do not share an API, because
234
+ "update" means a different operation in each.
228
235
 
229
236
  - [goselfupdate](https://github.com/datapointchris/goselfupdate) — replaces a Go binary
230
237
  - [bashselfupdate](https://github.com/datapointchris/bashselfupdate) — moves a git checkout to its newest tag
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyselfupdate"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "Self-update and update notification for Python CLIs installed with uv tool"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -36,7 +36,7 @@ typer = ["typer>=0.12.0"]
36
36
  [project.urls]
37
37
  Repository = "https://github.com/datapointchris/pyselfupdate"
38
38
  Issues = "https://github.com/datapointchris/pyselfupdate/issues"
39
- Changelog = "https://github.com/datapointchris/pyselfupdate/blob/main/CHANGELOG.md"
39
+ Changelog = "https://github.com/datapointchris/pyselfupdate/releases"
40
40
 
41
41
  [dependency-groups]
42
42
  dev = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyselfupdate"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "Self-update and update notification for Python CLIs installed with uv tool"
5
5
  authors = [{ name = "Chris Birch", email = "datapointchris@gmail.com" }]
6
6
  license = { text = "MIT" }
@@ -34,7 +34,7 @@ typer = ["typer>=0.12.0"]
34
34
  [project.urls]
35
35
  Repository = "https://github.com/datapointchris/pyselfupdate"
36
36
  Issues = "https://github.com/datapointchris/pyselfupdate/issues"
37
- Changelog = "https://github.com/datapointchris/pyselfupdate/blob/main/CHANGELOG.md"
37
+ Changelog = "https://github.com/datapointchris/pyselfupdate/releases"
38
38
 
39
39
  [dependency-groups]
40
40
  dev = [
@@ -1,8 +1,8 @@
1
- """The per-tool state file.
1
+ """The per-tool, per-machine state file.
2
2
 
3
3
  One schema, shared byte-for-byte with goselfupdate and bashselfupdate, so that
4
4
  any tool can read any other tool's state and a single dashboard can glob
5
- `~/.local/state/*/autoupdate.json` with no per-tool knowledge.
5
+ `~/.local/state/*/autoupdate-*.json` with no per-tool knowledge.
6
6
 
7
7
  State, not config and not cache: it persists across runs, it is not authored by
8
8
  the user, and deleting it changes behaviour rather than merely costing a
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import json
16
16
  import os
17
+ import socket
17
18
  from dataclasses import asdict
18
19
  from dataclasses import dataclass
19
20
  from datetime import UTC
@@ -21,7 +22,45 @@ from datetime import datetime
21
22
  from pathlib import Path
22
23
 
23
24
  SCHEMA = 1
24
- FILENAME = 'autoupdate.json'
25
+
26
+ #: Names the file when the host cannot be read, so a filename is always
27
+ #: well-formed and two unidentifiable boxes collide only with each other.
28
+ UNKNOWN_MACHINE = 'unknown'
29
+
30
+
31
+ def machine() -> str:
32
+ """The box this process runs on: bare hostname, no domain, lowercased.
33
+
34
+ goselfupdate and bashselfupdate derive it the same way, so the three write
35
+ interleaved files a single reader can enumerate.
36
+ """
37
+ try:
38
+ return canonical_machine(socket.gethostname())
39
+ except OSError:
40
+ return UNKNOWN_MACHINE
41
+
42
+
43
+ def canonical_machine(name: str) -> str:
44
+ """A hostname from any source, reduced to the form `machine` records.
45
+
46
+ A hostname reaches a reader fully qualified as often as bare, so a
47
+ comparison against a recorded name has to canonicalize both sides or it
48
+ fails on every host.
49
+ """
50
+ return name.strip().lower().split('.')[0] or UNKNOWN_MACHINE
51
+
52
+
53
+ def filename(machine_name: str) -> str:
54
+ """The file one machine writes inside a tool's state directory.
55
+
56
+ The machine is part of the name because a state directory is a synced
57
+ directory on some installations, and a file syncer has no merge: two boxes
58
+ writing one path leaves one winner plus a conflict copy nobody reads. Both
59
+ fields this file carries — the version installed here and the instant this
60
+ box last checked — describe one machine, so the split costs nothing and
61
+ makes the collision unreachable.
62
+ """
63
+ return f'autoupdate-{machine_name}.json'
25
64
 
26
65
 
27
66
  @dataclass
@@ -54,7 +93,8 @@ def state_home() -> Path:
54
93
 
55
94
 
56
95
  def state_path(tool: str) -> Path:
57
- return state_home() / tool / FILENAME
96
+ """Where this machine's state for a tool lives."""
97
+ return state_home() / tool / filename(machine())
58
98
 
59
99
 
60
100
  def read(tool: str) -> State: