storia 0.4.0__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.
storia-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.3
2
+ Name: storia
3
+ Version: 0.4.0
4
+ Summary: A simple cli tool to help languages by using power of ai stories
5
+ Author: Pratham Das
6
+ Author-email: Pratham Das <prathamdas003@gmail.com>
7
+ Requires-Dist: groq>=1.1.2
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
storia-0.4.0/README.md ADDED
File without changes
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "storia"
3
+ version = "0.4.0"
4
+ description = "A simple cli tool to help languages by using power of ai stories"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Pratham Das", email = "prathamdas003@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "groq>=1.1.2",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.11.3,<0.12.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [project.scripts]
19
+ storia= "storia.main:main"
20
+
21
+ [tool.setuptools.package-data]
22
+ "storia.laguages" = ["*.json"]
23
+
24
+
25
+ [pypi]
26
+ username= "__token__"
27
+ password="pypi-AgEIcHlwaS5vcmcCJGE3YjFkMjMwLTdlZWEtNGFjMy1iMTFhLTg4OWM2MTQ1MWI1ZQACD1sxLFsic3F1aWxpbyJdXQACLFsyLFsiNmY0NDZmYjItNWU0Yy00MjFhLWFjMGItZDE3NzcxNmJmNzg1Il1dAAAGICLsuNIGwUoYeZXv_qkHyaxSEFb9BQQu-jxpDAlHG0lj"
@@ -0,0 +1,12 @@
1
+ from .constant import Mode, DefaultConfig
2
+ from .main import main
3
+ from .helper import config_to_dict, write_config, get_random_topics
4
+
5
+ __all__ = [
6
+ "main",
7
+ "Mode",
8
+ "DefaultConfig",
9
+ "config_to_dict",
10
+ "write_config",
11
+ "get_random_topics",
12
+ ]
@@ -0,0 +1,79 @@
1
+ from groq import AsyncGroq
2
+ from .constant import Stories
3
+ from .api_key_manager import get_api_key_from_config
4
+ from .prompt import prompt_text
5
+ import sys
6
+
7
+
8
+ def get_client() -> AsyncGroq:
9
+ """Get Groq client with API key from config."""
10
+ api_key = get_api_key_from_config()
11
+ return AsyncGroq(api_key=api_key)
12
+
13
+
14
+ async def generate_story(topics: list[tuple[str, str]]) -> str:
15
+ client = get_client()
16
+ models = ["openai/gpt-oss-20b", "llama-3.3-70b-versatile"]
17
+
18
+ last_error = None
19
+ for model in models:
20
+ try:
21
+ words = [word for word, _ in topics]
22
+ response = await client.chat.completions.create(
23
+ model=model,
24
+ messages=[
25
+ {"role": "system", "content": prompt_text},
26
+ {
27
+ "role": "user",
28
+ "content": f"Target language: Italian\nWord list: {words}",
29
+ },
30
+ ],
31
+ temperature=1,
32
+ max_tokens=4000,
33
+ top_p=0.95,
34
+ )
35
+ story = response.choices[0].message.content
36
+ if story:
37
+ return story
38
+ except Exception as e:
39
+ last_error = str(e)
40
+ print(f"Model {model} failed: {e}", file=sys.stderr)
41
+ continue
42
+
43
+ print("Error: All AI models failed to generate a story.", file=sys.stderr)
44
+ print(f"Last error: {last_error}", file=sys.stderr)
45
+ print("Please check your API key and try again.", file=sys.stderr)
46
+ sys.exit(1)
47
+
48
+
49
+ def write_story(story: str, date: str) -> None:
50
+ parts = story.split("\n\n", 1)
51
+
52
+ if len(parts) != 2:
53
+ print("Error: AI returned an unexpected response format.", file=sys.stderr)
54
+ print(
55
+ "The story must have two parts separated by a blank line.", file=sys.stderr
56
+ )
57
+ print("Please try again.", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+ target_language_story = parts[0]
61
+ english_retelling = parts[1]
62
+ output = (
63
+ f"--------\n"
64
+ f"[{date}]\n\n"
65
+ f"{target_language_story}\n\n"
66
+ f"{english_retelling}\n"
67
+ f"--------\n\n"
68
+ )
69
+ print(output)
70
+ try:
71
+ with Stories.open("a", encoding="utf-8") as f:
72
+ f.write(output)
73
+ except FileNotFoundError:
74
+ print("Error: Could not save story to file.", file=sys.stderr)
75
+ print(f"Check that the directory exists: {Stories.parent}", file=sys.stderr)
76
+ sys.exit(1)
77
+ except Exception as e:
78
+ print(f"Error: Failed to save story: {e}", file=sys.stderr)
79
+ sys.exit(1)
@@ -0,0 +1,62 @@
1
+ from .constant import config
2
+ import json
3
+ import sys
4
+
5
+
6
+ def get_api_key_from_config() -> str:
7
+ """Read API key from config, raise if missing."""
8
+ if not config.exists():
9
+ print("Error: Config file not found.", file=sys.stderr)
10
+ print("Please run 'storia' to initialize your configuration.", file=sys.stderr)
11
+ sys.exit(1)
12
+
13
+ try:
14
+ data = json.loads(config.read_text())
15
+ except json.JSONDecodeError:
16
+ print("Error: Config file is corrupted.", file=sys.stderr)
17
+ print("Try running 'storia --reset' to fix this.", file=sys.stderr)
18
+ sys.exit(1)
19
+
20
+ key = data.get("api_key")
21
+ if not key:
22
+ print("Error: API key not configured.", file=sys.stderr)
23
+ print("Please run 'storia' to add your Groq API key.", file=sys.stderr)
24
+ sys.exit(1)
25
+ return key
26
+
27
+
28
+ def prompt_for_api_key() -> str:
29
+ """Prompt user for API key with validation."""
30
+ while True:
31
+ key = input("Enter your Groq API key: ").strip()
32
+ if not key:
33
+ print("API key cannot be empty. Please try again.")
34
+ continue
35
+ if len(key) < 10:
36
+ print("API key seems too short. Please check and try again.")
37
+ continue
38
+ return key
39
+
40
+
41
+ def get_or_prompt_api_key() -> str:
42
+ """Get existing API key from config or prompt user if missing."""
43
+ if config.exists():
44
+ try:
45
+ data = json.loads(config.read_text())
46
+ if data.get("api_key"):
47
+ return data["api_key"]
48
+ except (json.JSONDecodeError, FileNotFoundError):
49
+ pass
50
+
51
+ print("API key not found. Please enter your Groq API key.")
52
+ api_key = prompt_for_api_key()
53
+
54
+ if config.exists():
55
+ try:
56
+ data = json.loads(config.read_text())
57
+ data["api_key"] = api_key
58
+ config.write_text(json.dumps(data, indent=2))
59
+ except (json.JSONDecodeError, FileNotFoundError):
60
+ pass
61
+
62
+ return api_key
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+ from importlib.resources import files
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ HOME = Path.home()
7
+ DATA_DIR = HOME / ".storia"
8
+ config = DATA_DIR / "config.json"
9
+ Stories = DATA_DIR / "stories.txt"
10
+ content_path = files("storia.languages").joinpath("italian.json")
11
+
12
+
13
+ class Mode(Enum):
14
+ RANDOM = 0
15
+ TOPIC_BASED = 1
16
+
17
+
18
+ @dataclass
19
+ class DefaultConfig:
20
+ today: str
21
+ content: list
22
+ length: int
23
+ mode: Mode
24
+ api_key: str | None = None
@@ -0,0 +1,38 @@
1
+ import json
2
+ import random
3
+ from .constant import config, DATA_DIR, DefaultConfig
4
+
5
+
6
+ def config_to_dict(cfg: DefaultConfig) -> dict:
7
+ return {
8
+ "today": cfg.today,
9
+ "content": cfg.content,
10
+ "length": cfg.length,
11
+ "mode": cfg.mode.value,
12
+ "api_key": cfg.api_key,
13
+ }
14
+
15
+
16
+ def write_config(cfg: DefaultConfig, preserve_api_key: bool = True) -> None:
17
+ if not DATA_DIR.exists():
18
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
19
+
20
+ if preserve_api_key and config.exists():
21
+ try:
22
+ existing = json.loads(config.read_text())
23
+ except (json.JSONDecodeError, FileNotFoundError):
24
+ existing = {}
25
+
26
+ # Update with new values, but only include api_key if it's not None
27
+ new_data = config_to_dict(cfg)
28
+ if cfg.api_key is None:
29
+ new_data.pop("api_key", None)
30
+ existing.update(new_data)
31
+ else:
32
+ existing = config_to_dict(cfg)
33
+
34
+ config.write_text(json.dumps(existing, indent=2))
35
+
36
+
37
+ def get_random_topics(length: int, n: int = 10) -> list[int]:
38
+ return [random.randint(0, length - 1) for _ in range(n)]
File without changes