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