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.
Files changed (69) hide show
  1. arionxiv/__init__.py +40 -0
  2. arionxiv/__main__.py +10 -0
  3. arionxiv/arxiv_operations/__init__.py +0 -0
  4. arionxiv/arxiv_operations/client.py +225 -0
  5. arionxiv/arxiv_operations/fetcher.py +173 -0
  6. arionxiv/arxiv_operations/searcher.py +122 -0
  7. arionxiv/arxiv_operations/utils.py +293 -0
  8. arionxiv/cli/__init__.py +4 -0
  9. arionxiv/cli/commands/__init__.py +1 -0
  10. arionxiv/cli/commands/analyze.py +587 -0
  11. arionxiv/cli/commands/auth.py +365 -0
  12. arionxiv/cli/commands/chat.py +714 -0
  13. arionxiv/cli/commands/daily.py +482 -0
  14. arionxiv/cli/commands/fetch.py +217 -0
  15. arionxiv/cli/commands/library.py +295 -0
  16. arionxiv/cli/commands/preferences.py +426 -0
  17. arionxiv/cli/commands/search.py +254 -0
  18. arionxiv/cli/commands/settings_unified.py +1407 -0
  19. arionxiv/cli/commands/trending.py +41 -0
  20. arionxiv/cli/commands/welcome.py +168 -0
  21. arionxiv/cli/main.py +407 -0
  22. arionxiv/cli/ui/__init__.py +1 -0
  23. arionxiv/cli/ui/global_theme_manager.py +173 -0
  24. arionxiv/cli/ui/logo.py +127 -0
  25. arionxiv/cli/ui/splash.py +89 -0
  26. arionxiv/cli/ui/theme.py +32 -0
  27. arionxiv/cli/ui/theme_system.py +391 -0
  28. arionxiv/cli/utils/__init__.py +54 -0
  29. arionxiv/cli/utils/animations.py +522 -0
  30. arionxiv/cli/utils/api_client.py +583 -0
  31. arionxiv/cli/utils/api_config.py +505 -0
  32. arionxiv/cli/utils/command_suggestions.py +147 -0
  33. arionxiv/cli/utils/db_config_manager.py +254 -0
  34. arionxiv/github_actions_runner.py +206 -0
  35. arionxiv/main.py +23 -0
  36. arionxiv/prompts/__init__.py +9 -0
  37. arionxiv/prompts/prompts.py +247 -0
  38. arionxiv/rag_techniques/__init__.py +8 -0
  39. arionxiv/rag_techniques/basic_rag.py +1531 -0
  40. arionxiv/scheduler_daemon.py +139 -0
  41. arionxiv/server.py +1000 -0
  42. arionxiv/server_main.py +24 -0
  43. arionxiv/services/__init__.py +73 -0
  44. arionxiv/services/llm_client.py +30 -0
  45. arionxiv/services/llm_inference/__init__.py +58 -0
  46. arionxiv/services/llm_inference/groq_client.py +469 -0
  47. arionxiv/services/llm_inference/llm_utils.py +250 -0
  48. arionxiv/services/llm_inference/openrouter_client.py +564 -0
  49. arionxiv/services/unified_analysis_service.py +872 -0
  50. arionxiv/services/unified_auth_service.py +457 -0
  51. arionxiv/services/unified_config_service.py +456 -0
  52. arionxiv/services/unified_daily_dose_service.py +823 -0
  53. arionxiv/services/unified_database_service.py +1633 -0
  54. arionxiv/services/unified_llm_service.py +366 -0
  55. arionxiv/services/unified_paper_service.py +604 -0
  56. arionxiv/services/unified_pdf_service.py +522 -0
  57. arionxiv/services/unified_prompt_service.py +344 -0
  58. arionxiv/services/unified_scheduler_service.py +589 -0
  59. arionxiv/services/unified_user_service.py +954 -0
  60. arionxiv/utils/__init__.py +51 -0
  61. arionxiv/utils/api_helpers.py +200 -0
  62. arionxiv/utils/file_cleanup.py +150 -0
  63. arionxiv/utils/ip_helper.py +96 -0
  64. arionxiv-1.0.32.dist-info/METADATA +336 -0
  65. arionxiv-1.0.32.dist-info/RECORD +69 -0
  66. arionxiv-1.0.32.dist-info/WHEEL +5 -0
  67. arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
  68. arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
  69. 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())