rnow 0.3.17__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.
Files changed (74) hide show
  1. rnow-0.3.17/LICENSE +21 -0
  2. rnow-0.3.17/PKG-INFO +150 -0
  3. rnow-0.3.17/README.md +117 -0
  4. rnow-0.3.17/pyproject.toml +86 -0
  5. rnow-0.3.17/rnow/__init__.py +5 -0
  6. rnow-0.3.17/rnow/__main__.py +7 -0
  7. rnow-0.3.17/rnow/cli/__init__.py +6 -0
  8. rnow-0.3.17/rnow/cli/auth.py +67 -0
  9. rnow-0.3.17/rnow/cli/blob.py +98 -0
  10. rnow-0.3.17/rnow/cli/commands.py +2474 -0
  11. rnow-0.3.17/rnow/cli/common.py +28 -0
  12. rnow-0.3.17/rnow/cli/cube.py +255 -0
  13. rnow-0.3.17/rnow/cli/main.py +49 -0
  14. rnow-0.3.17/rnow/cli/test.py +899 -0
  15. rnow-0.3.17/rnow/cli/token_count.py +295 -0
  16. rnow-0.3.17/rnow/cli/upload.py +180 -0
  17. rnow-0.3.17/rnow/core/__init__.py +37 -0
  18. rnow-0.3.17/rnow/core/reward.py +364 -0
  19. rnow-0.3.17/rnow/core/tool.py +516 -0
  20. rnow-0.3.17/rnow/models.py +444 -0
  21. rnow-0.3.17/rnow/templates/deepseek-aha/config.yml +26 -0
  22. rnow-0.3.17/rnow/templates/deepseek-aha/rewards.py +35 -0
  23. rnow-0.3.17/rnow/templates/deepseek-aha/train.jsonl +1000 -0
  24. rnow-0.3.17/rnow/templates/finqa/README.md +60 -0
  25. rnow-0.3.17/rnow/templates/finqa/config.yml +23 -0
  26. rnow-0.3.17/rnow/templates/finqa/requirements.txt +1 -0
  27. rnow-0.3.17/rnow/templates/finqa/rewards.py +8 -0
  28. rnow-0.3.17/rnow/templates/finqa/train.jsonl +6251 -0
  29. rnow-0.3.17/rnow/templates/food-extract/README.md +24 -0
  30. rnow-0.3.17/rnow/templates/food-extract/config.yml +18 -0
  31. rnow-0.3.17/rnow/templates/food-extract/images/00000.png +0 -0
  32. rnow-0.3.17/rnow/templates/food-extract/images/00001.png +0 -0
  33. rnow-0.3.17/rnow/templates/food-extract/rewards.py +54 -0
  34. rnow-0.3.17/rnow/templates/food-extract/setup.py +69 -0
  35. rnow-0.3.17/rnow/templates/food-extract/train.jsonl +2 -0
  36. rnow-0.3.17/rnow/templates/mcp-tavily/config.yml +29 -0
  37. rnow-0.3.17/rnow/templates/mcp-tavily/requirements.txt +1 -0
  38. rnow-0.3.17/rnow/templates/mcp-tavily/rewards.py +25 -0
  39. rnow-0.3.17/rnow/templates/mcp-tavily/train.jsonl +500 -0
  40. rnow-0.3.17/rnow/templates/new/config.yml +26 -0
  41. rnow-0.3.17/rnow/templates/new/requirements.txt +1 -0
  42. rnow-0.3.17/rnow/templates/new/rewards.py +0 -0
  43. rnow-0.3.17/rnow/templates/new/train.jsonl +0 -0
  44. rnow-0.3.17/rnow/templates/rl-nextjs/config.yml +27 -0
  45. rnow-0.3.17/rnow/templates/rl-nextjs/requirements.txt +2 -0
  46. rnow-0.3.17/rnow/templates/rl-nextjs/rewards.py +446 -0
  47. rnow-0.3.17/rnow/templates/rl-nextjs/train.jsonl +1000 -0
  48. rnow-0.3.17/rnow/templates/rl-single/config.yml +27 -0
  49. rnow-0.3.17/rnow/templates/rl-single/requirements.txt +1 -0
  50. rnow-0.3.17/rnow/templates/rl-single/rewards.py +14 -0
  51. rnow-0.3.17/rnow/templates/rl-single/train.jsonl +92 -0
  52. rnow-0.3.17/rnow/templates/rl-tools/config.yml +27 -0
  53. rnow-0.3.17/rnow/templates/rl-tools/requirements.txt +3 -0
  54. rnow-0.3.17/rnow/templates/rl-tools/rewards.py +25 -0
  55. rnow-0.3.17/rnow/templates/rl-tools/tools.py +38 -0
  56. rnow-0.3.17/rnow/templates/rl-tools/train.jsonl +500 -0
  57. rnow-0.3.17/rnow/templates/sft/config.yml +20 -0
  58. rnow-0.3.17/rnow/templates/sft/train.jsonl +100 -0
  59. rnow-0.3.17/rnow/templates/tutorial-reward/config.yml +27 -0
  60. rnow-0.3.17/rnow/templates/tutorial-reward/requirements.txt +1 -0
  61. rnow-0.3.17/rnow/templates/tutorial-reward/rewards.py +15 -0
  62. rnow-0.3.17/rnow/templates/tutorial-reward/train.jsonl +92 -0
  63. rnow-0.3.17/rnow/templates/tutorial-tool/config.yml +27 -0
  64. rnow-0.3.17/rnow/templates/tutorial-tool/requirements.txt +3 -0
  65. rnow-0.3.17/rnow/templates/tutorial-tool/rewards.py +7 -0
  66. rnow-0.3.17/rnow/templates/tutorial-tool/tools.py +7 -0
  67. rnow-0.3.17/rnow/templates/tutorial-tool/train.jsonl +1266 -0
  68. rnow-0.3.17/rnow.egg-info/PKG-INFO +150 -0
  69. rnow-0.3.17/rnow.egg-info/SOURCES.txt +72 -0
  70. rnow-0.3.17/rnow.egg-info/dependency_links.txt +1 -0
  71. rnow-0.3.17/rnow.egg-info/entry_points.txt +2 -0
  72. rnow-0.3.17/rnow.egg-info/requires.txt +28 -0
  73. rnow-0.3.17/rnow.egg-info/top_level.txt +1 -0
  74. rnow-0.3.17/setup.cfg +4 -0
rnow-0.3.17/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ReinforceNow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rnow-0.3.17/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: rnow
3
+ Version: 0.3.17
4
+ Summary: ReinforceNow CLI - Reinforcement Learning platform command-line interface
5
+ Requires-Python: <3.15,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: click>=8.0.0
9
+ Requires-Dist: requests>=2.25.0
10
+ Requires-Dist: httpx>=0.24.0
11
+ Requires-Dist: pydantic>=2.0.0
12
+ Requires-Dist: pyyaml>=5.4.0
13
+ Requires-Dist: packaging>=21.0
14
+ Requires-Dist: prompt_toolkit>=3.0.0
15
+ Requires-Dist: tokenizers>=0.15.0
16
+ Requires-Dist: openai-harmony>=0.0.8
17
+ Requires-Dist: boto3>=1.28.0
18
+ Provides-Extra: test
19
+ Requires-Dist: tinker-cookbook>=0.1.0; extra == "test"
20
+ Requires-Dist: transformers>=4.40.0; extra == "test"
21
+ Provides-Extra: api
22
+ Requires-Dist: fastapi>=0.68.0; extra == "api"
23
+ Requires-Dist: uvicorn>=0.15.0; extra == "api"
24
+ Provides-Extra: mcp
25
+ Requires-Dist: fastmcp>=0.1.0; extra == "mcp"
26
+ Provides-Extra: all
27
+ Requires-Dist: tinker-cookbook>=0.1.0; extra == "all"
28
+ Requires-Dist: transformers>=4.40.0; extra == "all"
29
+ Requires-Dist: fastapi>=0.68.0; extra == "all"
30
+ Requires-Dist: uvicorn>=0.15.0; extra == "all"
31
+ Requires-Dist: fastmcp>=0.1.0; extra == "all"
32
+ Dynamic: license-file
33
+
34
+ <div align="center">
35
+ <img
36
+ alt="ReinforceNow CLI"
37
+ src="./assets/header.png"
38
+ width="100%"
39
+ >
40
+ <br><br>
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/rnow?color=blue)](https://pypi.org/project/rnow/)
43
+ [![Docs](https://img.shields.io/badge/docs-reinforcenow.ai-blue)](https://reinforcenow.ai/docs)
44
+ [![Follow on X](https://img.shields.io/badge/Follow_on_X-@reinforcenow-black?labelColor=white)](https://x.com/reinforcenow)
45
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
46
+
47
+ </div>
48
+
49
+ # Documentation
50
+
51
+ See the [documentation](https://www.reinforcenow.ai/docs/getting-started/quickstart) for a technical overview of the platform and [train your first agent](https://www.reinforcenow.ai/docs/getting-started/first-agent)
52
+
53
+ # Quick Start
54
+
55
+ ### 1. Install uv (Python package manager)
56
+
57
+ ```bash
58
+ # macOS/Linux:
59
+ $ curl -LsSf https://astral.sh/uv/install.sh | sh
60
+
61
+ # Windows:
62
+ PS> powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
63
+ ```
64
+
65
+ ### 2. Install ReinforceNow
66
+
67
+ ```bash
68
+ uv init && uv venv --python 3.11
69
+ source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
70
+ uv pip install rnow
71
+ ```
72
+
73
+ ### 3. Authenticate
74
+
75
+ ```bash
76
+ rnow login
77
+ ```
78
+
79
+ ### 4. Create & Run Your First Project
80
+
81
+ ```bash
82
+ rnow init --template sft
83
+ rnow run
84
+ ```
85
+
86
+ That's it! Your training run will start on ReinforceNow's infrastructure. Monitor progress in the [dashboard](https://reinforcenow.ai/home).
87
+
88
+ ![ReinforceNow Graph](./assets/reinforcenow-graph.png)
89
+
90
+ # Core Concepts
91
+
92
+ Go from raw data to a reliable AI agent in production. ReinforceNow gives you the flexibility to define:
93
+
94
+ ### 1. Reward Functions
95
+
96
+ Define how your model should be evaluated using the `@reward` decorator:
97
+
98
+ ```python
99
+ from rnow.core import reward, RewardArgs
100
+
101
+ @reward
102
+ async def accuracy(args: RewardArgs, messages: list) -> float:
103
+ """Check if the model's answer matches ground truth."""
104
+ response = messages[-1]["content"]
105
+ expected = args.metadata["answer"]
106
+ return 1.0 if expected in response else 0.0
107
+ ```
108
+
109
+ → [Write your first reward function](https://www.reinforcenow.ai/docs/getting-started/first-reward)
110
+
111
+ ### 2. Tools (for Agents)
112
+
113
+ Give your model the ability to call functions during training:
114
+
115
+ ```python
116
+ from rnow.core import tool
117
+
118
+ @tool
119
+ def search(query: str, max_results: int = 5) -> dict:
120
+ """Search the web for information."""
121
+ # Your implementation here
122
+ return {"results": [...]}
123
+ ```
124
+
125
+ → [Train an agent with custom tools](https://www.reinforcenow.ai/docs/getting-started/first-agent)
126
+
127
+ ### 3. Training Data
128
+
129
+ Create a `train.jsonl` file with your prompts and reward assignments:
130
+
131
+ ```json
132
+ {"messages": [{"role": "user", "content": "Balance the equation: Fe + O2 → Fe2O3"}], "rewards": ["accuracy"], "metadata": {"answer": "4Fe + 3O2 → 2Fe2O3"}}
133
+ {"messages": [{"role": "user", "content": "Balance the equation: H2 + O2 → H2O"}], "rewards": ["accuracy"], "metadata": {"answer": "2H2 + O2 → 2H2O"}}
134
+ {"messages": [{"role": "user", "content": "Balance the equation: N2 + H2 → NH3"}], "rewards": ["accuracy"], "metadata": {"answer": "N2 + 3H2 → 2NH3"}}
135
+ ```
136
+
137
+ → [Learn about training data format](https://www.reinforcenow.ai/docs/cli-reference/train-data)
138
+
139
+ # Contributing
140
+
141
+ We welcome contributions! ❤️ Please open an issue to discuss your ideas before submitting a PR
142
+
143
+ <br>
144
+ <div align="center">
145
+ <img
146
+ alt="ReinforceNow"
147
+ src="./assets/footer.png"
148
+ width="100%"
149
+ >
150
+ </div>
rnow-0.3.17/README.md ADDED
@@ -0,0 +1,117 @@
1
+ <div align="center">
2
+ <img
3
+ alt="ReinforceNow CLI"
4
+ src="./assets/header.png"
5
+ width="100%"
6
+ >
7
+ <br><br>
8
+
9
+ [![PyPI version](https://img.shields.io/pypi/v/rnow?color=blue)](https://pypi.org/project/rnow/)
10
+ [![Docs](https://img.shields.io/badge/docs-reinforcenow.ai-blue)](https://reinforcenow.ai/docs)
11
+ [![Follow on X](https://img.shields.io/badge/Follow_on_X-@reinforcenow-black?labelColor=white)](https://x.com/reinforcenow)
12
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
13
+
14
+ </div>
15
+
16
+ # Documentation
17
+
18
+ See the [documentation](https://www.reinforcenow.ai/docs/getting-started/quickstart) for a technical overview of the platform and [train your first agent](https://www.reinforcenow.ai/docs/getting-started/first-agent)
19
+
20
+ # Quick Start
21
+
22
+ ### 1. Install uv (Python package manager)
23
+
24
+ ```bash
25
+ # macOS/Linux:
26
+ $ curl -LsSf https://astral.sh/uv/install.sh | sh
27
+
28
+ # Windows:
29
+ PS> powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
30
+ ```
31
+
32
+ ### 2. Install ReinforceNow
33
+
34
+ ```bash
35
+ uv init && uv venv --python 3.11
36
+ source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
37
+ uv pip install rnow
38
+ ```
39
+
40
+ ### 3. Authenticate
41
+
42
+ ```bash
43
+ rnow login
44
+ ```
45
+
46
+ ### 4. Create & Run Your First Project
47
+
48
+ ```bash
49
+ rnow init --template sft
50
+ rnow run
51
+ ```
52
+
53
+ That's it! Your training run will start on ReinforceNow's infrastructure. Monitor progress in the [dashboard](https://reinforcenow.ai/home).
54
+
55
+ ![ReinforceNow Graph](./assets/reinforcenow-graph.png)
56
+
57
+ # Core Concepts
58
+
59
+ Go from raw data to a reliable AI agent in production. ReinforceNow gives you the flexibility to define:
60
+
61
+ ### 1. Reward Functions
62
+
63
+ Define how your model should be evaluated using the `@reward` decorator:
64
+
65
+ ```python
66
+ from rnow.core import reward, RewardArgs
67
+
68
+ @reward
69
+ async def accuracy(args: RewardArgs, messages: list) -> float:
70
+ """Check if the model's answer matches ground truth."""
71
+ response = messages[-1]["content"]
72
+ expected = args.metadata["answer"]
73
+ return 1.0 if expected in response else 0.0
74
+ ```
75
+
76
+ → [Write your first reward function](https://www.reinforcenow.ai/docs/getting-started/first-reward)
77
+
78
+ ### 2. Tools (for Agents)
79
+
80
+ Give your model the ability to call functions during training:
81
+
82
+ ```python
83
+ from rnow.core import tool
84
+
85
+ @tool
86
+ def search(query: str, max_results: int = 5) -> dict:
87
+ """Search the web for information."""
88
+ # Your implementation here
89
+ return {"results": [...]}
90
+ ```
91
+
92
+ → [Train an agent with custom tools](https://www.reinforcenow.ai/docs/getting-started/first-agent)
93
+
94
+ ### 3. Training Data
95
+
96
+ Create a `train.jsonl` file with your prompts and reward assignments:
97
+
98
+ ```json
99
+ {"messages": [{"role": "user", "content": "Balance the equation: Fe + O2 → Fe2O3"}], "rewards": ["accuracy"], "metadata": {"answer": "4Fe + 3O2 → 2Fe2O3"}}
100
+ {"messages": [{"role": "user", "content": "Balance the equation: H2 + O2 → H2O"}], "rewards": ["accuracy"], "metadata": {"answer": "2H2 + O2 → 2H2O"}}
101
+ {"messages": [{"role": "user", "content": "Balance the equation: N2 + H2 → NH3"}], "rewards": ["accuracy"], "metadata": {"answer": "N2 + 3H2 → 2NH3"}}
102
+ ```
103
+
104
+ → [Learn about training data format](https://www.reinforcenow.ai/docs/cli-reference/train-data)
105
+
106
+ # Contributing
107
+
108
+ We welcome contributions! ❤️ Please open an issue to discuss your ideas before submitting a PR
109
+
110
+ <br>
111
+ <div align="center">
112
+ <img
113
+ alt="ReinforceNow"
114
+ src="./assets/footer.png"
115
+ width="100%"
116
+ >
117
+ </div>
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where = ["."]
7
+ include = ["rnow*"]
8
+
9
+ [tool.setuptools.package-data]
10
+ rnow = ["templates/**/*"]
11
+
12
+ [project]
13
+ name = "rnow"
14
+ version = "0.3.17"
15
+ description = "ReinforceNow CLI - Reinforcement Learning platform command-line interface"
16
+ readme = "README.md"
17
+ requires-python = ">=3.11,<3.15"
18
+ dependencies = [
19
+ # Core CLI deps - lightweight, cross-platform
20
+ "click>=8.0.0",
21
+ "requests>=2.25.0",
22
+ "httpx>=0.24.0",
23
+ "pydantic>=2.0.0",
24
+ "pyyaml>=5.4.0",
25
+ "packaging>=21.0",
26
+ "prompt_toolkit>=3.0.0",
27
+ # Tokenizers for accurate token counting
28
+ "tokenizers>=0.15.0",
29
+ "openai-harmony>=0.0.8",
30
+ # AWS S3 uploads via STS
31
+ "boto3>=1.28.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ # Local testing with ML inference (requires torch)
36
+ test = [
37
+ "tinker-cookbook>=0.1.0",
38
+ "transformers>=4.40.0", # Ensure modern version (2.x is incompatible)
39
+ ]
40
+ # API server mode
41
+ api = ["fastapi>=0.68.0", "uvicorn>=0.15.0"]
42
+ # MCP server support (for fetching tool schemas)
43
+ mcp = ["fastmcp>=0.1.0"]
44
+ # All optional features
45
+ all = ["tinker-cookbook>=0.1.0", "transformers>=4.40.0", "fastapi>=0.68.0", "uvicorn>=0.15.0", "fastmcp>=0.1.0"]
46
+
47
+ [project.scripts]
48
+ rnow = "rnow.cli.main:main"
49
+
50
+ # =============================================================================
51
+ # Ruff configuration (linting + formatting)
52
+ # =============================================================================
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py310"
56
+
57
+ [tool.ruff.lint]
58
+ select = [
59
+ "E", # pycodestyle errors
60
+ "F", # pyflakes
61
+ "I", # isort
62
+ "W", # pycodestyle warnings
63
+ "UP", # pyupgrade
64
+ "B", # flake8-bugbear
65
+ "SIM", # flake8-simplify
66
+ ]
67
+ ignore = [
68
+ "E501", # line too long (formatter handles)
69
+ "E402", # imports not at top
70
+ "E722", # bare except
71
+ "B008", # function call in default arg
72
+ "B904", # raise from err
73
+ "SIM115", # context manager for open
74
+ ]
75
+
76
+ [tool.ruff.lint.isort]
77
+ known-first-party = ["rnow"]
78
+
79
+ # =============================================================================
80
+ # Mypy configuration (type checking)
81
+ # =============================================================================
82
+ [tool.mypy]
83
+ python_version = "3.10"
84
+ warn_return_any = true
85
+ warn_unused_ignores = true
86
+ ignore_missing_imports = true
@@ -0,0 +1,5 @@
1
+ """
2
+ ReinforceNow CLI - Command-line interface for ReinforceNow RLHF platform.
3
+ """
4
+
5
+ __version__ = "0.8.2"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python
2
+ """Entry point for running rnow as a module."""
3
+
4
+ from rnow.cli.main import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,6 @@
1
+ # reinforcenow/cli/__init__.py
2
+ # CLI package exports
3
+
4
+ from rnow.cli.main import cli
5
+
6
+ __all__ = ["cli"]
@@ -0,0 +1,67 @@
1
+ # reinforcenow/cli/auth.py
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ # Simple home directory paths
9
+ DATA_DIR = Path.home() / ".reinforcenow"
10
+ CREDS_FILE = DATA_DIR / "credentials.json"
11
+ CONFIG_FILE = DATA_DIR / "config.json"
12
+
13
+
14
+ def is_authenticated() -> bool:
15
+ """Check if authenticated."""
16
+ try:
17
+ with open(CREDS_FILE) as f:
18
+ return "api_key" in json.load(f)
19
+ except (FileNotFoundError, json.JSONDecodeError, KeyError):
20
+ return False
21
+
22
+
23
+ def get_auth_headers() -> dict[str, str]:
24
+ """Get auth headers."""
25
+ try:
26
+ with open(CREDS_FILE) as f:
27
+ creds = json.load(f)
28
+ return {
29
+ "Content-Type": "application/json",
30
+ "Authorization": f"Bearer {creds['api_key']}",
31
+ }
32
+ except (FileNotFoundError, json.JSONDecodeError, KeyError):
33
+ raise click.ClickException("Not authenticated. Run 'reinforcenow login'")
34
+
35
+
36
+ def get_active_org_from_config() -> str | None:
37
+ """Get active organization."""
38
+ try:
39
+ with open(CONFIG_FILE) as f:
40
+ return json.load(f).get("active_organization_id")
41
+ except (FileNotFoundError, json.JSONDecodeError):
42
+ return None
43
+
44
+
45
+ def set_active_organization(org_id: str) -> None:
46
+ """Set active organization."""
47
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
48
+
49
+ try:
50
+ with open(CONFIG_FILE) as f:
51
+ config = json.load(f)
52
+ except (FileNotFoundError, json.JSONDecodeError):
53
+ config = {}
54
+
55
+ config["active_organization_id"] = org_id
56
+
57
+ with open(CONFIG_FILE, "w") as f:
58
+ json.dump(config, f, indent=2)
59
+
60
+
61
+ def logout() -> None:
62
+ """Remove credentials."""
63
+ if CREDS_FILE.exists():
64
+ CREDS_FILE.unlink()
65
+ click.echo("✓ Logged out")
66
+ else:
67
+ click.echo("Not logged in")
@@ -0,0 +1,98 @@
1
+ # reinforcenow/cli/blob.py
2
+ """Vercel Blob upload support for large files."""
3
+
4
+ from pathlib import Path
5
+
6
+ import requests
7
+
8
+ from rnow.cli import auth
9
+
10
+ # Size threshold for blob uploads (4MB to stay under 4.5MB limit)
11
+ MAX_INLINE_BYTES = 4 * 1024 * 1024
12
+
13
+ BLOB_API_URL = "https://blob.vercel-storage.com"
14
+ BLOB_API_VERSION = "7"
15
+
16
+
17
+ def request_blob_client_token(base_url: str, pathname: str) -> str:
18
+ """
19
+ Request a client upload token from the backend.
20
+ This token allows direct upload to Vercel Blob.
21
+ """
22
+ headers = auth.get_auth_headers()
23
+ headers["Content-Type"] = "application/json"
24
+
25
+ payload = {
26
+ "type": "blob.generate-client-token",
27
+ "payload": {
28
+ "pathname": pathname,
29
+ "callbackUrl": f"{base_url}/dataset/upload",
30
+ },
31
+ }
32
+
33
+ resp = requests.post(
34
+ f"{base_url}/dataset/upload",
35
+ headers=headers,
36
+ json=payload,
37
+ timeout=30,
38
+ )
39
+ resp.raise_for_status()
40
+ data = resp.json()
41
+
42
+ if data.get("type") != "blob.generate-client-token":
43
+ raise RuntimeError(f"Unexpected response from blob token endpoint: {data}")
44
+
45
+ client_token = data.get("clientToken")
46
+ if not client_token:
47
+ raise RuntimeError("No clientToken returned from blob token endpoint")
48
+
49
+ return client_token
50
+
51
+
52
+ def upload_file_to_blob(base_url: str, local_path: Path, blob_pathname: str) -> dict:
53
+ """
54
+ Upload a file directly to Vercel Blob using a client token.
55
+ Returns the blob JSON (contains url, pathname, etc).
56
+ """
57
+ client_token = request_blob_client_token(base_url, blob_pathname)
58
+
59
+ url = f"{BLOB_API_URL}/{blob_pathname.lstrip('/')}"
60
+ headers = {
61
+ "Authorization": f"Bearer {client_token}",
62
+ "x-api-version": BLOB_API_VERSION,
63
+ "x-content-type": "application/jsonl",
64
+ }
65
+
66
+ with open(local_path, "rb") as f:
67
+ resp = requests.put(url, headers=headers, data=f, timeout=300)
68
+
69
+ resp.raise_for_status()
70
+ return resp.json()
71
+
72
+
73
+ def maybe_upload_to_blob(
74
+ base_url: str,
75
+ file_path: Path,
76
+ dataset_id: str,
77
+ ) -> tuple[str | None, dict | None]:
78
+ """
79
+ Check if file needs blob upload and handle it.
80
+
81
+ Returns:
82
+ (inline_contents, blob_info)
83
+ - If small: inline_contents is file content, blob_info is None
84
+ - If large: inline_contents is None, blob_info has url/pathname
85
+ """
86
+ size = file_path.stat().st_size
87
+
88
+ if size <= MAX_INLINE_BYTES:
89
+ # Small file - return contents for inline upload
90
+ return None, None
91
+
92
+ # Large file - upload to blob
93
+ import uuid
94
+
95
+ blob_pathname = f"datasets/{dataset_id}/{uuid.uuid4().hex[:8]}-{file_path.name}"
96
+
97
+ blob = upload_file_to_blob(base_url, file_path, blob_pathname)
98
+ return None, blob