librecoding 0.3.0__py3-none-any.whl → 0.4.0__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.
librecoding/__init__.py CHANGED
@@ -1,24 +1,103 @@
1
- """librecoding - модуль, который можно вызвать как функцию."""
1
+ """librecoding - вызываемый модуль: спрашивает промпт, шлёт в OpenRouter, пишет .txt."""
2
2
 
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
3
7
  import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from datetime import datetime
11
+ from pathlib import Path
4
12
  from types import ModuleType
5
13
 
6
14
  from .core import greet
7
15
 
8
- __version__ = "0.3.0"
16
+ __version__ = "0.4.0"
17
+
18
+ API_URL = "https://openrouter.ai/api/v1/chat/completions"
19
+ DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash"
20
+ KEY_FILE = Path.home() / ".config" / "librecoding" / "api_key"
21
+ PROMPT = "Введи промпт: "
22
+ PAUSE_TEXT = "Нажми Enter, чтобы закрыть..."
23
+
24
+ _state: dict = {"key": None, "model": DEFAULT_MODEL}
25
+
26
+
27
+ def set_api_key(key: str) -> None:
28
+ """Задать ключ OpenRouter на время работы процесса."""
29
+ _state["key"] = key.strip()
30
+
31
+
32
+ def set_model(model: str) -> None:
33
+ """Сменить модель, например 'deepseek/deepseek-chat'."""
34
+ _state["model"] = model
35
+
36
+
37
+ def get_model() -> str:
38
+ return _state["model"]
39
+
40
+
41
+ def _api_key() -> str:
42
+ """Ключ: set_api_key() -> OPENROUTER_API_KEY -> ~/.config/librecoding/api_key."""
43
+ if _state["key"]:
44
+ return _state["key"]
45
+
46
+ env = os.environ.get("OPENROUTER_API_KEY", "").strip()
47
+ if env:
48
+ return env
49
+
50
+ if KEY_FILE.is_file():
51
+ for line in KEY_FILE.read_text(encoding="utf-8").splitlines():
52
+ line = line.strip()
53
+ if line and not line.startswith("#"):
54
+ return line
55
+
56
+ raise RuntimeError(
57
+ "API-ключ OpenRouter не найден. Задай его так:\n"
58
+ f" echo 'sk-or-v1-...' > {KEY_FILE}\n"
59
+ f" chmod 600 {KEY_FILE}\n"
60
+ " либо: export OPENROUTER_API_KEY=sk-or-v1-...\n"
61
+ " либо: librecoding.set_api_key('sk-or-v1-...')"
62
+ )
9
63
 
10
- BANNER = "Привет, мир! (из librecoding)"
11
- PROMPT = "Нажми Enter, чтобы закрыть..."
12
64
 
65
+ def ask(prompt, model=None, temperature=0.7, timeout=180) -> str:
66
+ """Отправить промпт в OpenRouter, вернуть текст ответа."""
67
+ payload = {
68
+ "model": model or _state["model"],
69
+ "messages": [{"role": "user", "content": prompt}],
70
+ "temperature": temperature,
71
+ }
72
+ req = urllib.request.Request(
73
+ API_URL,
74
+ data=json.dumps(payload).encode("utf-8"),
75
+ headers={
76
+ "Authorization": f"Bearer {_api_key()}",
77
+ "Content-Type": "application/json",
78
+ "HTTP-Referer": "https://pypi.org/project/librecoding/",
79
+ "X-Title": "librecoding",
80
+ },
81
+ method="POST",
82
+ )
13
83
 
14
- def hello() -> str:
15
- """Явный способ попросить приветствие."""
16
- print(BANNER)
17
- return BANNER
84
+ try:
85
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
86
+ data = json.loads(resp.read().decode("utf-8"))
87
+ except urllib.error.HTTPError as exc:
88
+ body = exc.read().decode("utf-8", "replace")[:400]
89
+ raise RuntimeError(f"OpenRouter HTTP {exc.code}: {body}") from None
90
+ except urllib.error.URLError as exc:
91
+ raise RuntimeError(f"нет сети / таймаут: {exc.reason}") from None
92
+
93
+ try:
94
+ return data["choices"][0]["message"]["content"] or ""
95
+ except (KeyError, IndexError, TypeError):
96
+ raise RuntimeError(f"неожиданный ответ: {str(data)[:400]}") from None
18
97
 
19
98
 
20
- def pause(prompt: str = PROMPT) -> None:
21
- """Ждёт Enter, чтобы терминал не закрылся сразу."""
99
+ def pause(prompt: str = PAUSE_TEXT) -> None:
100
+ """Ждёт Enter, чтобы терминал не закрылся. Молчит без tty."""
22
101
  if not sys.stdin.isatty():
23
102
  return
24
103
  try:
@@ -27,17 +106,72 @@ def pause(prompt: str = PROMPT) -> None:
27
106
  print()
28
107
 
29
108
 
30
- class _LibreCodingModule(ModuleType):
31
- """Модуль, который ведёт себя как функция: librecoding()."""
109
+ def _caller_dir() -> Path:
110
+ """Папка запущенного скрипта (test.py), а не библиотеки."""
111
+ path = getattr(sys.modules.get("__main__"), "__file__", None)
112
+ if path:
113
+ return Path(path).resolve().parent
114
+ if sys.argv and sys.argv[0]:
115
+ return Path(sys.argv[0]).resolve().parent
116
+ return Path.cwd()
117
+
118
+
119
+ def interactive(prompt=None, filename=None, model=None):
120
+ """Спросить промпт -> запросить OpenRouter -> сохранить .txt рядом со скриптом."""
121
+ if not sys.stdin.isatty():
122
+ raise RuntimeError("нужен интерактивный терминал: запусти файл в консоли")
32
123
 
124
+ if prompt is None:
125
+ try:
126
+ prompt = input(PROMPT).strip()
127
+ except (EOFError, KeyboardInterrupt):
128
+ print()
129
+ return None
130
+
131
+ if not prompt:
132
+ print("Пустой промпт — выход.")
133
+ return None
134
+
135
+ mdl = model or _state["model"]
136
+ print(f"[{mdl}] думаю...")
137
+ answer = ask(prompt, model=mdl)
138
+
139
+ print("\n--- ответ ---")
140
+ print(answer.strip())
141
+ print("--- конец ---")
142
+
143
+ name = filename or f"response_{datetime.now():%Y-%m-%d_%H-%M-%S}.txt"
144
+ path = _caller_dir() / name
145
+ path.write_text(
146
+ f"Модель: {mdl}\n"
147
+ f"Дата: {datetime.now():%Y-%m-%d %H:%M:%S}\n\n"
148
+ f"Промпт:\n{prompt}\n\n"
149
+ f"Ответ:\n{answer.strip()}\n",
150
+ encoding="utf-8",
151
+ )
152
+ print(f"\nСохранено: {path}")
153
+ return path
154
+
155
+
156
+ class _LibreCodingModule(ModuleType):
33
157
  def __call__(self, obj=None, *args, **kwargs):
34
- print(BANNER)
35
158
  if callable(obj) and not args and not kwargs:
36
159
  return obj
160
+ try:
161
+ if isinstance(obj, str):
162
+ kwargs["prompt"] = obj
163
+ interactive(*args, **kwargs)
164
+ except Exception as exc:
165
+ print(f"Ошибка: {exc}", file=sys.stderr)
166
+ if os.environ.get("LIBRECODING_DEBUG"):
167
+ raise
37
168
  pause()
38
- return BANNER
169
+ return None
39
170
 
40
171
 
41
172
  sys.modules[__name__].__class__ = _LibreCodingModule
42
173
 
43
- __all__ = ["greet", "hello", "pause", "BANNER"]
174
+ __all__ = [
175
+ "greet", "ask", "interactive", "pause",
176
+ "set_api_key", "set_model", "get_model",
177
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librecoding
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Free and open-source coding library
5
5
  Author: ar
6
6
  License-Expression: MIT
@@ -0,0 +1,5 @@
1
+ librecoding/__init__.py,sha256=FuRSgReAs8eZkNLZR5E2kohoktr1SYjDZcuWsH_gd34,5705
2
+ librecoding/core.py,sha256=UO5R_K8a-XwaJAhwl3cybnCSbk2tkpcnqrT0d-PIdTA,180
3
+ librecoding-0.4.0.dist-info/METADATA,sha256=TthfoOQuCqPhV3Q-WyVPYdieV6vtz0QyeYDWL28cZFM,331
4
+ librecoding-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ librecoding-0.4.0.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- librecoding/__init__.py,sha256=AcfE0XB4s3mv7vZRq1ndw-i3JrV1ZYQycd5gZZ_dwLE,1129
2
- librecoding/core.py,sha256=UO5R_K8a-XwaJAhwl3cybnCSbk2tkpcnqrT0d-PIdTA,180
3
- librecoding-0.3.0.dist-info/METADATA,sha256=hD3JW0D4NfZ7IMIYzBsmIJLm3CzUYAcZyyjQVHPf6t4,331
4
- librecoding-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
- librecoding-0.3.0.dist-info/RECORD,,