arionxiv 1.0.32__py3-none-any.whl
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.
- arionxiv/__init__.py +40 -0
- arionxiv/__main__.py +10 -0
- arionxiv/arxiv_operations/__init__.py +0 -0
- arionxiv/arxiv_operations/client.py +225 -0
- arionxiv/arxiv_operations/fetcher.py +173 -0
- arionxiv/arxiv_operations/searcher.py +122 -0
- arionxiv/arxiv_operations/utils.py +293 -0
- arionxiv/cli/__init__.py +4 -0
- arionxiv/cli/commands/__init__.py +1 -0
- arionxiv/cli/commands/analyze.py +587 -0
- arionxiv/cli/commands/auth.py +365 -0
- arionxiv/cli/commands/chat.py +714 -0
- arionxiv/cli/commands/daily.py +482 -0
- arionxiv/cli/commands/fetch.py +217 -0
- arionxiv/cli/commands/library.py +295 -0
- arionxiv/cli/commands/preferences.py +426 -0
- arionxiv/cli/commands/search.py +254 -0
- arionxiv/cli/commands/settings_unified.py +1407 -0
- arionxiv/cli/commands/trending.py +41 -0
- arionxiv/cli/commands/welcome.py +168 -0
- arionxiv/cli/main.py +407 -0
- arionxiv/cli/ui/__init__.py +1 -0
- arionxiv/cli/ui/global_theme_manager.py +173 -0
- arionxiv/cli/ui/logo.py +127 -0
- arionxiv/cli/ui/splash.py +89 -0
- arionxiv/cli/ui/theme.py +32 -0
- arionxiv/cli/ui/theme_system.py +391 -0
- arionxiv/cli/utils/__init__.py +54 -0
- arionxiv/cli/utils/animations.py +522 -0
- arionxiv/cli/utils/api_client.py +583 -0
- arionxiv/cli/utils/api_config.py +505 -0
- arionxiv/cli/utils/command_suggestions.py +147 -0
- arionxiv/cli/utils/db_config_manager.py +254 -0
- arionxiv/github_actions_runner.py +206 -0
- arionxiv/main.py +23 -0
- arionxiv/prompts/__init__.py +9 -0
- arionxiv/prompts/prompts.py +247 -0
- arionxiv/rag_techniques/__init__.py +8 -0
- arionxiv/rag_techniques/basic_rag.py +1531 -0
- arionxiv/scheduler_daemon.py +139 -0
- arionxiv/server.py +1000 -0
- arionxiv/server_main.py +24 -0
- arionxiv/services/__init__.py +73 -0
- arionxiv/services/llm_client.py +30 -0
- arionxiv/services/llm_inference/__init__.py +58 -0
- arionxiv/services/llm_inference/groq_client.py +469 -0
- arionxiv/services/llm_inference/llm_utils.py +250 -0
- arionxiv/services/llm_inference/openrouter_client.py +564 -0
- arionxiv/services/unified_analysis_service.py +872 -0
- arionxiv/services/unified_auth_service.py +457 -0
- arionxiv/services/unified_config_service.py +456 -0
- arionxiv/services/unified_daily_dose_service.py +823 -0
- arionxiv/services/unified_database_service.py +1633 -0
- arionxiv/services/unified_llm_service.py +366 -0
- arionxiv/services/unified_paper_service.py +604 -0
- arionxiv/services/unified_pdf_service.py +522 -0
- arionxiv/services/unified_prompt_service.py +344 -0
- arionxiv/services/unified_scheduler_service.py +589 -0
- arionxiv/services/unified_user_service.py +954 -0
- arionxiv/utils/__init__.py +51 -0
- arionxiv/utils/api_helpers.py +200 -0
- arionxiv/utils/file_cleanup.py +150 -0
- arionxiv/utils/ip_helper.py +96 -0
- arionxiv-1.0.32.dist-info/METADATA +336 -0
- arionxiv-1.0.32.dist-info/RECORD +69 -0
- arionxiv-1.0.32.dist-info/WHEEL +5 -0
- arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
- arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
- arionxiv-1.0.32.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""CLI Authentication Interface for ArionXiv
|
|
2
|
+
|
|
3
|
+
Uses the hosted ArionXiv API for authentication - no local database required.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.prompt import Prompt, Confirm
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
import getpass
|
|
17
|
+
from typing import Optional, Dict, Any
|
|
18
|
+
|
|
19
|
+
from ..utils.api_client import api_client, APIClientError
|
|
20
|
+
from ...services.unified_user_service import unified_user_service
|
|
21
|
+
from ..ui.theme import create_themed_console, style_text, print_success, print_error, print_warning, create_themed_panel, get_theme_colors
|
|
22
|
+
from ..utils.animations import shake_text, left_to_right_reveal
|
|
23
|
+
from .welcome import show_logo_and_features
|
|
24
|
+
|
|
25
|
+
console = create_themed_console()
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthInterface:
|
|
30
|
+
"""Handles CLI authentication interface using the hosted API"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.console = console
|
|
34
|
+
logger.debug("AuthInterface initialized")
|
|
35
|
+
|
|
36
|
+
async def ensure_authenticated(self) -> Optional[Dict[str, Any]]:
|
|
37
|
+
"""Ensure user is authenticated, prompt if not"""
|
|
38
|
+
logger.debug("Checking authentication status")
|
|
39
|
+
|
|
40
|
+
# Check if already authenticated locally
|
|
41
|
+
if unified_user_service.is_authenticated():
|
|
42
|
+
logger.info("User already authenticated")
|
|
43
|
+
return unified_user_service.get_current_user()
|
|
44
|
+
|
|
45
|
+
# Check if API client has a stored token
|
|
46
|
+
if api_client.is_authenticated():
|
|
47
|
+
try:
|
|
48
|
+
profile = await api_client.get_profile()
|
|
49
|
+
if profile.get("success") and profile.get("user"):
|
|
50
|
+
user = profile["user"]
|
|
51
|
+
unified_user_service.create_session(user)
|
|
52
|
+
logger.info(f"Restored session for: {user.get('user_name')}")
|
|
53
|
+
return user
|
|
54
|
+
except APIClientError:
|
|
55
|
+
logger.debug("Stored token invalid, need to re-authenticate")
|
|
56
|
+
|
|
57
|
+
# Show authentication prompt
|
|
58
|
+
return await self._authentication_flow()
|
|
59
|
+
|
|
60
|
+
async def _authentication_flow(self) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""Main authentication flow"""
|
|
62
|
+
logger.debug("Starting authentication flow")
|
|
63
|
+
self.console.print()
|
|
64
|
+
self.console.print(create_themed_panel(
|
|
65
|
+
"[bold]Welcome to ArionXiv![/bold]\n\n"
|
|
66
|
+
"To access and interact with all the features, please provide your credentials below.\n"
|
|
67
|
+
"Please login or create an account.",
|
|
68
|
+
title="Authentication Required"
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
self.console.print(f"\n[bold]{style_text('Choose an option:', 'primary')}[/bold]")
|
|
73
|
+
self.console.print(f"{style_text('1', 'primary')}. Login with existing account")
|
|
74
|
+
self.console.print(f"{style_text('2', 'primary')}. Create new account")
|
|
75
|
+
self.console.print(f"{style_text('3', 'primary')}. Exit")
|
|
76
|
+
|
|
77
|
+
choice = Prompt.ask(
|
|
78
|
+
f"\n[bold]{style_text('Select option (1-3)', 'primary')}[/bold]",
|
|
79
|
+
choices=["1", "2", "3"],
|
|
80
|
+
default=f"1"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
left_to_right_reveal(self.console, f"Option {style_text(choice, 'primary')} selected!", duration=0.5)
|
|
84
|
+
|
|
85
|
+
if choice == "1":
|
|
86
|
+
user = await self._login_flow()
|
|
87
|
+
if user:
|
|
88
|
+
return user
|
|
89
|
+
elif choice == "2":
|
|
90
|
+
user = await self._register_flow()
|
|
91
|
+
if user:
|
|
92
|
+
return user
|
|
93
|
+
elif choice == "3":
|
|
94
|
+
colors = get_theme_colors()
|
|
95
|
+
primary_color = colors["primary"]
|
|
96
|
+
left_to_right_reveal(self.console, f"\n{style_text('Goodbye!', f'bold {primary_color}')}", duration=0.5)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
async def _login_flow(self) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""Handle user login via hosted API"""
|
|
101
|
+
self.console.print(f"\n[bold]{style_text('Login to ArionXiv', 'primary')}[/bold]")
|
|
102
|
+
self.console.print(f"[bold]{style_text('-' * 30, 'primary')}[/bold]")
|
|
103
|
+
|
|
104
|
+
max_attempts = 3
|
|
105
|
+
attempts = 0
|
|
106
|
+
|
|
107
|
+
while attempts < max_attempts:
|
|
108
|
+
try:
|
|
109
|
+
identifier = Prompt.ask(
|
|
110
|
+
f"\n[bold]{style_text('Username or Email', 'primary')}[/bold]"
|
|
111
|
+
).strip()
|
|
112
|
+
|
|
113
|
+
if not identifier:
|
|
114
|
+
print_error(self.console, f"{style_text('Username/Email is required', 'error')}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
self.console.print(f"\n[bold]{style_text('Password:', 'primary')}[/bold]")
|
|
118
|
+
password = getpass.getpass(f"> ")
|
|
119
|
+
|
|
120
|
+
if not password:
|
|
121
|
+
print_error(self.console, f"{style_text('Password is required', 'error')}")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
primary_color = get_theme_colors()["primary"]
|
|
125
|
+
self.console.print(f"\n{style_text('Authenticating...', f'bold {primary_color}')}")
|
|
126
|
+
logger.info(f"Attempting login for: {identifier}")
|
|
127
|
+
|
|
128
|
+
result = await api_client.login(identifier, password)
|
|
129
|
+
|
|
130
|
+
if result.get("success"):
|
|
131
|
+
user = result.get("user", {})
|
|
132
|
+
logger.info(f"Login successful for user: {user.get('user_name')}")
|
|
133
|
+
|
|
134
|
+
session_token = unified_user_service.create_session(user)
|
|
135
|
+
if session_token:
|
|
136
|
+
logger.debug("Session created successfully")
|
|
137
|
+
left_to_right_reveal(self.console, f"Welcome back, [bold]{style_text(user.get('user_name', 'User'), 'primary')}![/bold]", duration=0.5)
|
|
138
|
+
self.console.print()
|
|
139
|
+
show_logo_and_features(self.console, animate=False)
|
|
140
|
+
return user
|
|
141
|
+
else:
|
|
142
|
+
logger.error("Failed to create session after successful login")
|
|
143
|
+
print_error(self.console, f"{style_text('Failed to create session', 'error')}")
|
|
144
|
+
return None
|
|
145
|
+
else:
|
|
146
|
+
attempts += 1
|
|
147
|
+
remaining = max_attempts - attempts
|
|
148
|
+
error_msg = result.get('message') or result.get('error', 'Login failed')
|
|
149
|
+
logger.warning(f"Login failed for {identifier}: {error_msg}")
|
|
150
|
+
print_error(self.console, f"{style_text(error_msg, 'error')}")
|
|
151
|
+
|
|
152
|
+
if remaining > 0:
|
|
153
|
+
print_warning(self.console, f"You have {remaining} attempts remaining")
|
|
154
|
+
else:
|
|
155
|
+
print_error(self.console, f"{style_text('Maximum login attempts exceeded', 'error')}")
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
except APIClientError as e:
|
|
159
|
+
attempts += 1
|
|
160
|
+
remaining = max_attempts - attempts
|
|
161
|
+
logger.warning(f"API login error: {e.message}")
|
|
162
|
+
print_error(self.console, f"{style_text(e.message, 'error')}")
|
|
163
|
+
if remaining > 0:
|
|
164
|
+
print_warning(self.console, f"You have {remaining} attempts remaining")
|
|
165
|
+
except KeyboardInterrupt:
|
|
166
|
+
self.console.print(f"\n{style_text('Login cancelled', 'warning')}")
|
|
167
|
+
return None
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Login error: {str(e)}", exc_info=True)
|
|
170
|
+
print_error(self.console, f"Login error: {str(e)}")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
async def _register_flow(self) -> Optional[Dict[str, Any]]:
|
|
176
|
+
"""Handle user registration via hosted API"""
|
|
177
|
+
self.console.print(f"\n{style_text('Create ArionXiv Account', 'primary')}")
|
|
178
|
+
self.console.print("-" * 40)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
full_name = Prompt.ask(
|
|
182
|
+
f"\n[bold]{style_text('Full Name (optional)', 'primary')}[/bold]",
|
|
183
|
+
default=""
|
|
184
|
+
).strip()
|
|
185
|
+
|
|
186
|
+
while True:
|
|
187
|
+
email = Prompt.ask(
|
|
188
|
+
f"\n[bold]{style_text('Email Address', 'primary')}[/bold]",
|
|
189
|
+
default=""
|
|
190
|
+
).strip()
|
|
191
|
+
|
|
192
|
+
if not email:
|
|
193
|
+
print_error(self.console, f"{style_text('Email is required', 'error')}")
|
|
194
|
+
continue
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
while True:
|
|
198
|
+
user_name = Prompt.ask(
|
|
199
|
+
f"\n[bold]{style_text('Username', 'primary')}[/bold] (letters, numbers, underscore, hyphen only)",
|
|
200
|
+
).strip()
|
|
201
|
+
|
|
202
|
+
if not user_name:
|
|
203
|
+
print_error(self.console, f"{style_text('Username is required', 'error')}")
|
|
204
|
+
continue
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
while True:
|
|
208
|
+
self.console.print(f"\n[bold]{style_text('Password:', 'primary')}[/bold] (minimum 8 characters, must contain letter and number)")
|
|
209
|
+
password = getpass.getpass("> ")
|
|
210
|
+
|
|
211
|
+
if not password:
|
|
212
|
+
print_error(self.console, f"{style_text('Password is required', 'error')}")
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
password_confirm = getpass.getpass(f"Confirm Password: ")
|
|
216
|
+
|
|
217
|
+
if password != password_confirm:
|
|
218
|
+
print_error(self.console, f"{style_text('Passwords do not match', 'error')}")
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
self.console.print(f"\n[bold]{style_text('Account Summary', 'primary')}[/bold]")
|
|
224
|
+
self.console.print(f"Full Name: {style_text(full_name, 'primary') if full_name else style_text('Not provided', 'secondary')}")
|
|
225
|
+
self.console.print(f"Email: {style_text(email, 'primary')}")
|
|
226
|
+
self.console.print(f"Username: {style_text(user_name, 'primary')}")
|
|
227
|
+
|
|
228
|
+
if not Confirm.ask(f"\n[bold]{style_text('Create account with these details?', 'primary')}[/bold]"):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
self.console.print(f"\n[white]{style_text('Creating account...', 'primary')}[/white]")
|
|
232
|
+
logger.info(f"Attempting registration for: {email} ({user_name})")
|
|
233
|
+
|
|
234
|
+
result = await api_client.register(email, user_name, password, full_name)
|
|
235
|
+
|
|
236
|
+
if result.get("success"):
|
|
237
|
+
user = result.get("user", {})
|
|
238
|
+
logger.info(f"Registration successful for user: {user.get('user_name')}")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
login_result = await api_client.login(user_name, password)
|
|
242
|
+
if login_result.get("success"):
|
|
243
|
+
user = login_result.get("user", user)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
session_token = unified_user_service.create_session(user)
|
|
248
|
+
if session_token:
|
|
249
|
+
logger.debug("Session created for new user")
|
|
250
|
+
shake_text(self.console, f"Account created! Welcome, {user.get('user_name', 'User')}!")
|
|
251
|
+
self.console.print()
|
|
252
|
+
show_logo_and_features(self.console, animate=False)
|
|
253
|
+
return user
|
|
254
|
+
else:
|
|
255
|
+
logger.error("Failed to create session for new user")
|
|
256
|
+
print_error(self.console, style_text("Failed to create session", "error"))
|
|
257
|
+
return None
|
|
258
|
+
else:
|
|
259
|
+
error_msg = result.get("message") or result.get("error", "Registration failed")
|
|
260
|
+
logger.warning(f"Registration failed for {email}: {error_msg}")
|
|
261
|
+
print_error(self.console, error_msg)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
except APIClientError as e:
|
|
265
|
+
logger.warning(f"API registration error: {e.message}")
|
|
266
|
+
print_error(self.console, f"{style_text(e.message, 'error')}")
|
|
267
|
+
return None
|
|
268
|
+
except KeyboardInterrupt:
|
|
269
|
+
logger.debug("Registration cancelled by user")
|
|
270
|
+
self.console.print(f"\n{style_text('Registration cancelled', 'warning')}")
|
|
271
|
+
return None
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Registration error: {str(e)}", exc_info=True)
|
|
274
|
+
print_error(self.console, f"{style_text('Registration error:', 'error')} {str(e)}")
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def show_session_info(self):
|
|
278
|
+
"""Show current session information"""
|
|
279
|
+
logger.debug("Showing session info")
|
|
280
|
+
session_info = unified_user_service.get_session_info()
|
|
281
|
+
|
|
282
|
+
if session_info:
|
|
283
|
+
user = session_info["user"]
|
|
284
|
+
session = session_info["session"]
|
|
285
|
+
|
|
286
|
+
self.console.print(f"\n[bold]{style_text('Current Session', 'primary')}[/bold]")
|
|
287
|
+
self.console.print(f"[bold]{style_text('-' * 30, 'primary')}[/bold]")
|
|
288
|
+
self.console.print(f"User: [bold]{style_text(user['user_name'], 'primary')}[/bold] ({user['email']})")
|
|
289
|
+
if user.get('full_name'):
|
|
290
|
+
self.console.print(f"Name: [bold]{style_text(user['full_name'], 'primary')}[/bold]")
|
|
291
|
+
self.console.print(f"Session created: {style_text(session['created'], 'primary')}")
|
|
292
|
+
self.console.print(f"Expires: {style_text(session['expires'], 'primary')} ({style_text(session['days_remaining'], 'primary')} days remaining)")
|
|
293
|
+
self.console.print(f"Last activity: {style_text(session['last_activity'], 'primary')}")
|
|
294
|
+
else:
|
|
295
|
+
print_warning(self.console, f"{style_text('No active session', 'warning')}")
|
|
296
|
+
|
|
297
|
+
async def logout(self):
|
|
298
|
+
"""Logout current user"""
|
|
299
|
+
if unified_user_service.is_authenticated() or api_client.is_authenticated():
|
|
300
|
+
user = unified_user_service.get_current_user()
|
|
301
|
+
user_name = user.get('user_name', 'User') if user else 'User'
|
|
302
|
+
logger.info(f"Logging out user: {user_name}")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
await api_client.logout()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
unified_user_service.clear_session()
|
|
310
|
+
left_to_right_reveal(self.console, f"Goodbye, [bold]{style_text(user_name, 'primary')}[/bold]!", duration=0.5)
|
|
311
|
+
else:
|
|
312
|
+
logger.debug("Logout called but no active session")
|
|
313
|
+
print_warning(self.console, f"{style_text('No active session to logout', 'warning')}")
|
|
314
|
+
|
|
315
|
+
# Global auth interface instance
|
|
316
|
+
auth_interface = AuthInterface()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@click.command()
|
|
320
|
+
def login_command():
|
|
321
|
+
"""Login to your ArionXiv account"""
|
|
322
|
+
async def _login():
|
|
323
|
+
await auth_interface.ensure_authenticated()
|
|
324
|
+
asyncio.run(_login())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@click.command()
|
|
328
|
+
def logout_command():
|
|
329
|
+
"""Logout from your ArionXiv account"""
|
|
330
|
+
async def _logout():
|
|
331
|
+
await auth_interface.logout()
|
|
332
|
+
asyncio.run(_logout())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@click.command()
|
|
336
|
+
def register_command():
|
|
337
|
+
"""Create a new ArionXiv account"""
|
|
338
|
+
async def _register():
|
|
339
|
+
await auth_interface._register_flow()
|
|
340
|
+
asyncio.run(_register())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@click.command()
|
|
344
|
+
def session_command():
|
|
345
|
+
"""Show current session information"""
|
|
346
|
+
auth_interface.show_session_info()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@click.command(hidden=True)
|
|
350
|
+
@click.option('--login', '-l', is_flag=True, help='Force login prompt')
|
|
351
|
+
@click.option('--logout', '-o', is_flag=True, help='Logout current user')
|
|
352
|
+
@click.option('--info', '-i', is_flag=True, help='Show session information')
|
|
353
|
+
def auth_command(login: bool, logout: bool, info: bool):
|
|
354
|
+
"""Manage user authentication (legacy)"""
|
|
355
|
+
async def _handle_auth():
|
|
356
|
+
if logout:
|
|
357
|
+
await auth_interface.logout()
|
|
358
|
+
elif info:
|
|
359
|
+
auth_interface.show_session_info()
|
|
360
|
+
elif login:
|
|
361
|
+
await auth_interface.ensure_authenticated()
|
|
362
|
+
else:
|
|
363
|
+
auth_interface.show_session_info()
|
|
364
|
+
|
|
365
|
+
asyncio.run(_handle_auth())
|