obris-cli 0.4.0__tar.gz → 0.4.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.
- {obris_cli-0.4.0 → obris_cli-0.4.2}/PKG-INFO +1 -1
- {obris_cli-0.4.0 → obris_cli-0.4.2}/pyproject.toml +1 -1
- obris_cli-0.4.2/ruff.toml +2 -0
- obris_cli-0.4.0/src/obris/commands/auth.py → obris_cli-0.4.2/src/obris/auth/session.py +68 -128
- obris_cli-0.4.2/src/obris/commands/auth.py +173 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/config.py +28 -1
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/mapping.py +3 -1
- obris_cli-0.4.2/src/obris/utils/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/.claude/settings.local.json +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/.gitignore +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/README.md +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/api/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/api/client.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/api/knowledge.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/api/topics.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/assets/icon.png +0 -0
- {obris_cli-0.4.0/src/obris/commands → obris_cli-0.4.2/src/obris/auth}/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/cli.py +0 -0
- {obris_cli-0.4.0/src/obris/sync → obris_cli-0.4.2/src/obris/commands}/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/commands/env.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/commands/knowledge.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/commands/save.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/commands/sync.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/commands/topic.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/output.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/routes.py +0 -0
- {obris_cli-0.4.0/src/obris/utils → obris_cli-0.4.2/src/obris/sync}/__init__.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/commands.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/constants.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/engine.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/io.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/models.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/sync/state.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/utils/capture.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/utils/notify.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/src/obris/utils/upload.py +0 -0
- {obris_cli-0.4.0 → obris_cli-0.4.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"""Device auth session lifecycle: start, poll, check, finalize."""
|
|
2
|
+
|
|
2
3
|
import time
|
|
3
|
-
import webbrowser
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
import requests
|
|
@@ -11,145 +11,52 @@ from obris.api.topics import list_topics
|
|
|
11
11
|
from obris.output import as_json, is_json
|
|
12
12
|
|
|
13
13
|
POLL_INTERVAL = 2
|
|
14
|
-
# Server session lives 15 minutes; a
|
|
15
|
-
# readable for another COMPLETED_TTL (
|
|
16
|
-
#
|
|
17
|
-
POLL_TIMEOUT =
|
|
14
|
+
# Server session lives 15 minutes (SESSION_TTL); a completed session is
|
|
15
|
+
# readable for another COMPLETED_TTL (300s). Clamp the CLI deadline past
|
|
16
|
+
# both so we don't time out in the same second the user authorizes.
|
|
17
|
+
POLL_TIMEOUT = 1200
|
|
18
18
|
POLL_MAX_BACKOFF = 30
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"""Manage authentication."""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@auth.command("login")
|
|
27
|
-
def auth_login():
|
|
28
|
-
"""Authenticate via browser (recommended).
|
|
29
|
-
|
|
30
|
-
Opens a browser to log in. Works from any environment —
|
|
31
|
-
copy the link if the browser doesn't open automatically.
|
|
32
|
-
"""
|
|
33
|
-
api_base = config.get_api_base()
|
|
34
|
-
app_base = config.get_app_base()
|
|
35
|
-
|
|
21
|
+
def start_session(api_base):
|
|
22
|
+
"""POST a new device auth session and return its session_id."""
|
|
36
23
|
try:
|
|
37
24
|
resp = requests.post(f"{api_base}/{routes.device_sessions()}", timeout=10)
|
|
38
25
|
resp.raise_for_status()
|
|
39
26
|
payload = resp.json()
|
|
40
|
-
|
|
27
|
+
return payload["session_id"]
|
|
41
28
|
except requests.RequestException as e:
|
|
42
29
|
raise SystemExit(f"Failed to start login session: {e}") from e
|
|
43
30
|
except (ValueError, KeyError) as e:
|
|
44
31
|
raise SystemExit(f"Unexpected response from login session endpoint: {e}") from e
|
|
45
|
-
url = f"{app_base}/auth/device/{session_id}"
|
|
46
|
-
|
|
47
|
-
if not is_json():
|
|
48
|
-
click.echo("\nTo authenticate, open this URL in your browser:\n")
|
|
49
|
-
click.echo(f" {url}\n")
|
|
50
32
|
|
|
51
|
-
with contextlib.suppress(webbrowser.Error):
|
|
52
|
-
webbrowser.open(url)
|
|
53
33
|
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
def check_session(api_base, session_id):
|
|
35
|
+
"""One-shot session check used by `auth complete`.
|
|
56
36
|
|
|
37
|
+
Returns the session dict if readable, or None if the session is
|
|
38
|
+
gone (404). Unlike `poll_for_completion`, this does not loop or
|
|
39
|
+
retry — callers are expected to invoke it at most once per command
|
|
40
|
+
run.
|
|
41
|
+
"""
|
|
42
|
+
poll_url = f"{api_base}/{routes.device_session(session_id)}"
|
|
57
43
|
try:
|
|
58
|
-
|
|
59
|
-
except
|
|
60
|
-
|
|
61
|
-
raise SystemExit(130) from None
|
|
62
|
-
|
|
63
|
-
email = session.get("email")
|
|
64
|
-
config.save_tokens(
|
|
65
|
-
access_token=session["access_token"],
|
|
66
|
-
refresh_token=session["refresh_token"],
|
|
67
|
-
expires_in=session.get("expires_in", 3600),
|
|
68
|
-
client_id=session.get("client_id", ""),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
scratch_id = _detect_scratch()
|
|
72
|
-
|
|
73
|
-
if is_json():
|
|
74
|
-
as_json(
|
|
75
|
-
{
|
|
76
|
-
"env": config.get_active_env(),
|
|
77
|
-
"email": email,
|
|
78
|
-
"scratch_topic_id": scratch_id,
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
if email:
|
|
84
|
-
click.echo(f"Logged in as {email}")
|
|
85
|
-
env = config.get_active_env()
|
|
86
|
-
if scratch_id:
|
|
87
|
-
click.echo(f"[{env}] Scratch topic: {scratch_id}")
|
|
88
|
-
else:
|
|
89
|
-
click.echo(f"[{env}] No 'Scratch' topic found.")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@auth.command("status")
|
|
93
|
-
def auth_status():
|
|
94
|
-
"""Show current authentication status."""
|
|
95
|
-
env = config.get_active_env()
|
|
96
|
-
data = config._env_data()
|
|
97
|
-
token = data.get(config.KEY_ACCESS_TOKEN)
|
|
98
|
-
|
|
99
|
-
if not token:
|
|
100
|
-
if is_json():
|
|
101
|
-
as_json({"env": env, "authenticated": False})
|
|
102
|
-
return
|
|
103
|
-
click.echo(f"[{env}] Not authenticated.")
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
expires_at = data.get(config.KEY_TOKEN_EXPIRES_AT, "")
|
|
107
|
-
scratch = data.get(config.KEY_SCRATCH_TOPIC)
|
|
108
|
-
|
|
109
|
-
if is_json():
|
|
110
|
-
as_json(
|
|
111
|
-
{
|
|
112
|
-
"env": env,
|
|
113
|
-
"authenticated": True,
|
|
114
|
-
"token_expires_at": expires_at,
|
|
115
|
-
"scratch_topic_id": scratch,
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
click.echo(f"[{env}] Authenticated (OAuth token)")
|
|
121
|
-
if expires_at:
|
|
122
|
-
click.echo(f"[{env}] Token expires: {expires_at}")
|
|
123
|
-
if scratch:
|
|
124
|
-
click.echo(f"[{env}] Scratch topic: {scratch}")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@auth.command("logout")
|
|
128
|
-
def auth_logout():
|
|
129
|
-
"""Remove stored credentials."""
|
|
130
|
-
env = config.get_active_env()
|
|
131
|
-
data = config._env_data()
|
|
132
|
-
|
|
133
|
-
if not data.get(config.KEY_ACCESS_TOKEN):
|
|
134
|
-
if is_json():
|
|
135
|
-
as_json({"env": env, "logged_out": False})
|
|
136
|
-
return
|
|
137
|
-
click.echo(f"[{env}] Not authenticated.")
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
config.clear_tokens()
|
|
141
|
-
if is_json():
|
|
142
|
-
as_json({"env": env, "logged_out": True})
|
|
143
|
-
return
|
|
144
|
-
click.echo(f"[{env}] Logged out.")
|
|
44
|
+
resp = requests.get(poll_url, timeout=10)
|
|
45
|
+
except requests.RequestException as e:
|
|
46
|
+
raise SystemExit(f"Failed to check login session: {e}") from e
|
|
145
47
|
|
|
48
|
+
if resp.status_code == 404:
|
|
49
|
+
return None
|
|
50
|
+
if not resp.ok:
|
|
51
|
+
raise SystemExit(f"Failed to check login session ({resp.status_code}): {resp.text}")
|
|
146
52
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
53
|
+
try:
|
|
54
|
+
return resp.json()
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
raise SystemExit(f"Unexpected response from login session endpoint: {e}") from e
|
|
150
57
|
|
|
151
58
|
|
|
152
|
-
def
|
|
59
|
+
def poll_for_completion(api_base, session_id):
|
|
153
60
|
"""Poll the session endpoint until completed or expired.
|
|
154
61
|
|
|
155
62
|
Backs off on server errors (5xx, 429) with exponential delay,
|
|
@@ -193,7 +100,6 @@ def _poll_for_completion(api_base, session_id):
|
|
|
193
100
|
raise SystemExit("Session completed but no token received.")
|
|
194
101
|
return data
|
|
195
102
|
|
|
196
|
-
# Still pending — reset backoff and wait normally
|
|
197
103
|
last_server_error = None
|
|
198
104
|
backoff = POLL_INTERVAL
|
|
199
105
|
_sleep_within(POLL_INTERVAL, deadline)
|
|
@@ -203,12 +109,38 @@ def _poll_for_completion(api_base, session_id):
|
|
|
203
109
|
raise SystemExit("Login timed out. Run 'obris auth login' to try again.")
|
|
204
110
|
|
|
205
111
|
|
|
206
|
-
def
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
|
|
112
|
+
def finalize_login(session):
|
|
113
|
+
"""Save tokens from a completed session, detect the Scratch topic,
|
|
114
|
+
and report to the user. Shared by the blocking and non-blocking
|
|
115
|
+
login paths.
|
|
116
|
+
"""
|
|
117
|
+
email = session.get("email")
|
|
118
|
+
config.save_tokens(
|
|
119
|
+
access_token=session["access_token"],
|
|
120
|
+
refresh_token=session["refresh_token"],
|
|
121
|
+
expires_in=session.get("expires_in", 3600),
|
|
122
|
+
client_id=session.get("client_id", ""),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
scratch_id = _detect_scratch()
|
|
126
|
+
env = config.get_active_env()
|
|
127
|
+
|
|
128
|
+
if is_json():
|
|
129
|
+
as_json(
|
|
130
|
+
{
|
|
131
|
+
"env": env,
|
|
132
|
+
"email": email,
|
|
133
|
+
"scratch_topic_id": scratch_id,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
210
136
|
return
|
|
211
|
-
|
|
137
|
+
|
|
138
|
+
if email:
|
|
139
|
+
click.echo(f"Logged in as {email}")
|
|
140
|
+
if scratch_id:
|
|
141
|
+
click.echo(f"[{env}] Scratch topic: {scratch_id}")
|
|
142
|
+
else:
|
|
143
|
+
click.echo(f"[{env}] No 'Scratch' topic found.")
|
|
212
144
|
|
|
213
145
|
|
|
214
146
|
def _detect_scratch():
|
|
@@ -228,3 +160,11 @@ def _detect_scratch():
|
|
|
228
160
|
cfg[env][config.KEY_SCRATCH_TOPIC] = scratch_id
|
|
229
161
|
config.save(cfg)
|
|
230
162
|
return scratch_id
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _sleep_within(seconds, deadline):
|
|
166
|
+
"""Sleep for up to `seconds`, clamped so we never sleep past the deadline."""
|
|
167
|
+
remaining = deadline - time.time()
|
|
168
|
+
if remaining <= 0:
|
|
169
|
+
return
|
|
170
|
+
time.sleep(min(seconds, remaining))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import webbrowser
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from obris import config
|
|
7
|
+
from obris.auth.session import check_session, finalize_login, poll_for_completion, start_session
|
|
8
|
+
from obris.output import as_json, is_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group("auth")
|
|
12
|
+
def auth():
|
|
13
|
+
"""Manage authentication."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@auth.command("login")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--no-wait",
|
|
19
|
+
is_flag=True,
|
|
20
|
+
help="Print the login URL and exit without polling. Finish with `obris auth complete`.",
|
|
21
|
+
)
|
|
22
|
+
def auth_login(no_wait):
|
|
23
|
+
"""Authenticate via browser (recommended).
|
|
24
|
+
|
|
25
|
+
Opens a browser to log in. Works from any environment — copy the
|
|
26
|
+
link if the browser doesn't open automatically.
|
|
27
|
+
|
|
28
|
+
By default, the CLI polls in the foreground and exits once login is
|
|
29
|
+
complete. Pass `--no-wait` to print the URL and exit immediately;
|
|
30
|
+
when you (or your user) have authorized in the browser, run
|
|
31
|
+
`obris auth complete` to finalize. This mode is required for LLMs
|
|
32
|
+
and scripted contexts, where blocking the caller would prevent the
|
|
33
|
+
URL from being relayed to the user.
|
|
34
|
+
"""
|
|
35
|
+
api_base = config.get_api_base()
|
|
36
|
+
app_base = config.get_app_base()
|
|
37
|
+
|
|
38
|
+
session_id = start_session(api_base)
|
|
39
|
+
url = f"{app_base}/auth/device/{session_id}"
|
|
40
|
+
|
|
41
|
+
if not is_json():
|
|
42
|
+
click.echo("\nTo authenticate, open this URL in your browser:\n")
|
|
43
|
+
click.echo(f" {url}\n")
|
|
44
|
+
|
|
45
|
+
with contextlib.suppress(webbrowser.Error):
|
|
46
|
+
webbrowser.open(url)
|
|
47
|
+
|
|
48
|
+
env = config.get_active_env()
|
|
49
|
+
|
|
50
|
+
if no_wait:
|
|
51
|
+
config.save_pending_session(session_id)
|
|
52
|
+
if is_json():
|
|
53
|
+
as_json({"env": env, "session_id": session_id, "url": url, "status": "pending"})
|
|
54
|
+
return
|
|
55
|
+
click.echo(f"[{env}] After authorizing, run: obris auth complete")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if not is_json():
|
|
59
|
+
click.echo("Waiting for authentication...")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
session = poll_for_completion(api_base, session_id)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
click.echo("\nLogin cancelled.", err=True)
|
|
65
|
+
raise SystemExit(130) from None
|
|
66
|
+
|
|
67
|
+
finalize_login(session)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@auth.command("status")
|
|
71
|
+
def auth_status():
|
|
72
|
+
"""Show current authentication status (read-only).
|
|
73
|
+
|
|
74
|
+
Reports whether an access token is stored for the active env and,
|
|
75
|
+
if so, when it expires. If a login was started with `--no-wait` and
|
|
76
|
+
hasn't been finalized yet, reports that the session is pending.
|
|
77
|
+
This command never mutates stored state; run `obris auth complete`
|
|
78
|
+
to finalize a pending session.
|
|
79
|
+
"""
|
|
80
|
+
env = config.get_active_env()
|
|
81
|
+
data = config._env_data()
|
|
82
|
+
token = data.get(config.KEY_ACCESS_TOKEN)
|
|
83
|
+
pending = config.get_pending_session()
|
|
84
|
+
|
|
85
|
+
if not token:
|
|
86
|
+
if is_json():
|
|
87
|
+
as_json({"env": env, "authenticated": False, "pending": bool(pending)})
|
|
88
|
+
return
|
|
89
|
+
if pending:
|
|
90
|
+
click.echo(f"[{env}] Login pending. Finish with: obris auth complete")
|
|
91
|
+
else:
|
|
92
|
+
click.echo(f"[{env}] Not authenticated.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
expires_at = data.get(config.KEY_TOKEN_EXPIRES_AT, "")
|
|
96
|
+
scratch = data.get(config.KEY_SCRATCH_TOPIC)
|
|
97
|
+
|
|
98
|
+
if is_json():
|
|
99
|
+
as_json(
|
|
100
|
+
{
|
|
101
|
+
"env": env,
|
|
102
|
+
"authenticated": True,
|
|
103
|
+
"token_expires_at": expires_at,
|
|
104
|
+
"scratch_topic_id": scratch,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
click.echo(f"[{env}] Authenticated (OAuth token)")
|
|
110
|
+
if expires_at:
|
|
111
|
+
click.echo(f"[{env}] Token expires: {expires_at}")
|
|
112
|
+
if scratch:
|
|
113
|
+
click.echo(f"[{env}] Scratch topic: {scratch}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@auth.command("complete")
|
|
117
|
+
def auth_complete():
|
|
118
|
+
"""Finalize a pending `auth login --no-wait` session.
|
|
119
|
+
|
|
120
|
+
Checks the pending session once and, if the user has authorized in
|
|
121
|
+
the browser, saves the tokens and clears the pending state. Exits
|
|
122
|
+
non-zero if no pending session exists or the session has expired.
|
|
123
|
+
"""
|
|
124
|
+
env = config.get_active_env()
|
|
125
|
+
pending = config.get_pending_session()
|
|
126
|
+
|
|
127
|
+
if not pending:
|
|
128
|
+
if is_json():
|
|
129
|
+
as_json({"env": env, "completed": False, "reason": "no_pending_session"})
|
|
130
|
+
raise SystemExit(1)
|
|
131
|
+
raise SystemExit(f"[{env}] No pending login session. Start one with: obris auth login --no-wait")
|
|
132
|
+
|
|
133
|
+
api_base = config.get_api_base()
|
|
134
|
+
session = check_session(api_base, pending)
|
|
135
|
+
|
|
136
|
+
if session is None:
|
|
137
|
+
config.clear_pending_session()
|
|
138
|
+
if is_json():
|
|
139
|
+
as_json({"env": env, "completed": False, "reason": "session_expired"})
|
|
140
|
+
raise SystemExit(1)
|
|
141
|
+
raise SystemExit(f"[{env}] Pending login session expired. Run: obris auth login --no-wait")
|
|
142
|
+
|
|
143
|
+
if session.get("status") != "completed":
|
|
144
|
+
app_base = config.get_app_base()
|
|
145
|
+
url = f"{app_base}/auth/device/{pending}"
|
|
146
|
+
if is_json():
|
|
147
|
+
as_json({"env": env, "completed": False, "reason": "still_pending", "url": url})
|
|
148
|
+
raise SystemExit(1)
|
|
149
|
+
raise SystemExit(f"[{env}] Login still pending. Open: {url}")
|
|
150
|
+
|
|
151
|
+
finalize_login(session)
|
|
152
|
+
config.clear_pending_session()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@auth.command("logout")
|
|
156
|
+
def auth_logout():
|
|
157
|
+
"""Remove stored credentials."""
|
|
158
|
+
env = config.get_active_env()
|
|
159
|
+
data = config._env_data()
|
|
160
|
+
|
|
161
|
+
if not data.get(config.KEY_ACCESS_TOKEN):
|
|
162
|
+
if is_json():
|
|
163
|
+
as_json({"env": env, "logged_out": False})
|
|
164
|
+
return
|
|
165
|
+
click.echo(f"[{env}] Not authenticated.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
config.clear_tokens()
|
|
169
|
+
config.clear_pending_session()
|
|
170
|
+
if is_json():
|
|
171
|
+
as_json({"env": env, "logged_out": True})
|
|
172
|
+
return
|
|
173
|
+
click.echo(f"[{env}] Logged out.")
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
UTC = timezone.utc
|
|
7
|
+
|
|
6
8
|
import requests
|
|
7
9
|
|
|
8
10
|
from obris import routes
|
|
@@ -22,6 +24,7 @@ KEY_CLIENT_ID = "client_id"
|
|
|
22
24
|
KEY_DEFAULT_ENV = "default_env"
|
|
23
25
|
KEY_ENVIRONMENTS = "environments"
|
|
24
26
|
KEY_SCRATCH_TOPIC = "scratch_topic_id"
|
|
27
|
+
KEY_PENDING_SESSION_ID = "pending_session_id"
|
|
25
28
|
|
|
26
29
|
REFRESH_BUFFER = timedelta(minutes=5)
|
|
27
30
|
|
|
@@ -144,6 +147,30 @@ def clear_tokens():
|
|
|
144
147
|
save(cfg)
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
def save_pending_session(session_id):
|
|
151
|
+
"""Remember a session_id started by `auth login --no-wait` so a later
|
|
152
|
+
`auth status` call knows which session to finalize."""
|
|
153
|
+
env = get_active_env()
|
|
154
|
+
cfg = load()
|
|
155
|
+
cfg.setdefault(env, {})
|
|
156
|
+
cfg[env][KEY_PENDING_SESSION_ID] = session_id
|
|
157
|
+
save(cfg)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_pending_session():
|
|
161
|
+
"""Return the pending session_id for the active environment, or None."""
|
|
162
|
+
return _env_data().get(KEY_PENDING_SESSION_ID)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def clear_pending_session():
|
|
166
|
+
"""Drop any pending session_id for the active environment."""
|
|
167
|
+
env = get_active_env()
|
|
168
|
+
cfg = load()
|
|
169
|
+
env_data = cfg.get(env, {})
|
|
170
|
+
env_data.pop(KEY_PENDING_SESSION_ID, None)
|
|
171
|
+
save(cfg)
|
|
172
|
+
|
|
173
|
+
|
|
147
174
|
def _refresh_if_needed():
|
|
148
175
|
"""Refresh the access token if it expires within the buffer window.
|
|
149
176
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Filename computation, hashing, and file utilities for sync."""
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
UTC = timezone.utc
|
|
8
|
+
|
|
7
9
|
CONFLICT_MARKER = "(conflict "
|
|
8
10
|
HASH_CHUNK_SIZE = 64 * 1024
|
|
9
11
|
|
|
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
|
|
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
|
|
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
|
|
File without changes
|