doploy-cli 0.2.3__tar.gz → 0.2.5__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.
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/PKG-INFO +1 -1
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli/main.py +58 -18
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/PKG-INFO +1 -1
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/pyproject.toml +1 -1
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/LICENSE +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/README.md +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli/SKILL.md +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli/__init__.py +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/SOURCES.txt +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/dependency_links.txt +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/entry_points.txt +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/requires.txt +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/doploy_cli.egg-info/top_level.txt +0 -0
- {doploy_cli-0.2.3 → doploy_cli-0.2.5}/setup.cfg +0 -0
|
@@ -20,8 +20,14 @@ import webbrowser
|
|
|
20
20
|
import stat
|
|
21
21
|
import sys
|
|
22
22
|
import time
|
|
23
|
+
from contextlib import contextmanager
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
26
|
+
try:
|
|
27
|
+
import fcntl
|
|
28
|
+
except ImportError: # Windows: no cross-process lock, refresh as before
|
|
29
|
+
fcntl = None
|
|
30
|
+
|
|
25
31
|
from typing import Optional
|
|
26
32
|
|
|
27
33
|
import httpx
|
|
@@ -39,9 +45,34 @@ def _load_config() -> dict:
|
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
def _save_config(cfg: dict) -> None:
|
|
48
|
+
# Write-then-rename so a concurrent reader never sees a half-written file
|
|
49
|
+
# (it would parse as {} and report "not logged in").
|
|
50
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
tmp = CONFIG_PATH.with_name(f'{CONFIG_PATH.name}.{os.getpid()}.tmp')
|
|
52
|
+
tmp.unlink(missing_ok=True) # leftover from a crashed run
|
|
53
|
+
# Created 0600 (token inside) and O_EXCL: never readable by other users,
|
|
54
|
+
# never follows a planted symlink.
|
|
55
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, stat.S_IRUSR | stat.S_IWUSR)
|
|
56
|
+
with os.fdopen(fd, 'w') as fh:
|
|
57
|
+
fh.write(json.dumps(cfg, indent=2) + '\n')
|
|
58
|
+
os.replace(tmp, CONFIG_PATH)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextmanager
|
|
62
|
+
def _refresh_lock():
|
|
63
|
+
"""Serialize token refresh across doploy processes. The server rotates the
|
|
64
|
+
refresh token on every use and revokes the whole grant if an already-used
|
|
65
|
+
one is presented again, so two processes refreshing at once log you out."""
|
|
66
|
+
if fcntl is None:
|
|
67
|
+
yield
|
|
68
|
+
return
|
|
42
69
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
CONFIG_PATH.
|
|
44
|
-
|
|
70
|
+
with open(CONFIG_PATH.with_name(CONFIG_PATH.name + '.lock'), 'w') as fh:
|
|
71
|
+
fcntl.flock(fh, fcntl.LOCK_EX)
|
|
72
|
+
try:
|
|
73
|
+
yield
|
|
74
|
+
finally:
|
|
75
|
+
fcntl.flock(fh, fcntl.LOCK_UN)
|
|
45
76
|
|
|
46
77
|
|
|
47
78
|
class Client:
|
|
@@ -56,21 +87,30 @@ class Client:
|
|
|
56
87
|
def _refresh_access_token(self) -> bool:
|
|
57
88
|
if not self.refresh or not self.refresh.get('refresh_token'):
|
|
58
89
|
return False
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
with _refresh_lock():
|
|
91
|
+
# Another process may have refreshed while we waited for the lock:
|
|
92
|
+
# use its tokens instead of replaying our now-rotated refresh token.
|
|
93
|
+
cfg = _load_config()
|
|
94
|
+
if cfg.get('refresh_token') and cfg['refresh_token'] != self.refresh['refresh_token']:
|
|
95
|
+
self.token = cfg.get('access_token') or ''
|
|
96
|
+
self.refresh['refresh_token'] = cfg['refresh_token']
|
|
97
|
+
self.refresh['expires_at'] = cfg.get('expires_at', 0)
|
|
98
|
+
return True
|
|
99
|
+
r = _oauth_token_request(self.base, {
|
|
100
|
+
'grant_type': 'refresh_token',
|
|
101
|
+
'refresh_token': self.refresh['refresh_token'],
|
|
102
|
+
'client_id': self.refresh['client_id'],
|
|
103
|
+
})
|
|
104
|
+
if r is None:
|
|
105
|
+
return False
|
|
106
|
+
self.token = r['access_token']
|
|
107
|
+
self.refresh['refresh_token'] = r['refresh_token']
|
|
108
|
+
self.refresh['expires_at'] = time.time() + int(r.get('expires_in') or 3600)
|
|
109
|
+
cfg = _load_config()
|
|
110
|
+
cfg.update(access_token=self.token, refresh_token=self.refresh['refresh_token'],
|
|
111
|
+
expires_at=self.refresh['expires_at'])
|
|
112
|
+
_save_config(cfg)
|
|
113
|
+
return True
|
|
74
114
|
|
|
75
115
|
def request(self, method: str, path: str, **kw):
|
|
76
116
|
if self.refresh and self.refresh.get('expires_at', 0) < time.time() + 30:
|
|
@@ -573,7 +613,7 @@ def main(argv=None):
|
|
|
573
613
|
help='required when targeting production')
|
|
574
614
|
sp.set_defaults(func=cmd_restart)
|
|
575
615
|
|
|
576
|
-
sp = sub.add_parser('logs', help='tail build logs (build, test or odoo source)')
|
|
616
|
+
sp = sub.add_parser('logs', help='tail build logs (build, test, or odoo source)')
|
|
577
617
|
sp.add_argument('build_id', type=int)
|
|
578
618
|
sp.add_argument('--source', choices=['build', 'test', 'odoo'], default='build')
|
|
579
619
|
sp.add_argument('--lines', type=int, default=200, help='tail length (max 2000)')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|