dexter2389 commited on
Commit
558414b
·
1 Parent(s): 8bbbb8b

Restructure project and use 'uv'

Browse files
.gitignore ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule
102
+ celerybeat.pid
103
+
104
+ # SageMath parsed files
105
+ *.sage.py
106
+
107
+ # Environments
108
+ .venv
109
+ env/
110
+ venv/
111
+ ENV/
112
+ env.bak/
113
+ venv.bak/
114
+ .env
115
+
116
+ # Spyder project settings
117
+ .spyderproject
118
+ .spyproject
119
+
120
+ # Rope project settings
121
+ .ropeproject
122
+
123
+ # mkdocs documentation
124
+ /site
125
+
126
+ # mypy
127
+ .mypy_cache/
128
+ .dmypy.json
129
+ dmypy.json
130
+
131
+ # Ruff
132
+ .ruff_cache/
133
+
134
+ # Pyre type checker
135
+ .pyre/
136
+
137
+ # pytype static type analyzer
138
+ .pytype/
139
+
140
+ # IDE
141
+ .idea
142
+
143
+ # Cython debug symbols
144
+ cython_debug/
145
+
146
+ # Custom ignore files/folders
147
+ .DS_Store
148
+ .pypirc
149
+ .vscode
150
+ *credentials*.json
151
+ scratch*
.pre-commit-config.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff-check
5
+ name: ruff-check
6
+ entry: ruff check --fix --force-exclude app etc
7
+ language: system
8
+ always_run: true
9
+ pass_filenames: false
10
+ - id: ruff-format
11
+ name: ruff-format
12
+ entry: ruff format --force-exclude app etc
13
+ language: system
14
+ always_run: true
15
+ pass_filenames: false
16
+ - id: branch-name-and-commit-permission-check
17
+ name: branch name and commit permission check
18
+ entry: bash ./etc/branch-name-check.sh
19
+ language: system
20
+ always_run: true
21
+ pass_filenames: false
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
LICENSE ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2025, Saurabh Ghanekar
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
compose.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ arcana-in-action:
3
+ build:
4
+ context: .
5
+ target: runtime
6
+ ports:
7
+ - 8698:7860
8
+ networks:
9
+ - arcana-in-action-net
10
+ - arcana-gateway-net
11
+ volumes:
12
+ - ./app:/app
13
+ environment:
14
+ ARCANA_API_KEY: "${ARCANA_API_KEY}"
15
+ MONGO_URI: "${MONGO_URI}"
16
+ OPENBLAS_NUM_THREADS: "${OPENBLAS_NUM_THREADS:-4}"
17
+ healthcheck:
18
+ test: ["CMD", "curl", "-f", "http://arcana-in-action/health"]
19
+ interval: 60s
20
+ timeout: 5s
21
+ retries: 3
22
+
23
+ networks:
24
+ arcana-in-action-net:
25
+ name: arcana-in-action-network
26
+ arcana-gateway-net:
27
+ name: arcana-gateway-network
28
+ external: true
etc/branch-name-check.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
2
+
3
+ if [[ $BRANCH =~ (master|main|develop) ]]; then
4
+ echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
5
+ echo "If so, commit with -n to bypass this pre-commit hook."
6
+ exit 1
7
+ fi
8
+
9
+ exit 0
etc/key_generator.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import secrets
2
+ import string
3
+
4
+
5
+ def generate_key(length=64):
6
+ alphabet = string.ascii_letters + string.digits
7
+ key = "".join(secrets.choice(alphabet) for _ in range(length))
8
+ return key
9
+
10
+
11
+ key = generate_key()
12
+ print(key)
etc/versioning.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import tomllib
3
+ from enum import StrEnum
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from rich.traceback import install
12
+
13
+ install(extra_lines=0, max_frames=10)
14
+
15
+
16
+ class VersionLevel(StrEnum):
17
+ MAJOR = "major"
18
+ MINOR = "minor"
19
+ PATCH = "patch"
20
+ ALPHA = "alpha"
21
+ BETA = "beta"
22
+ RC = "rc"
23
+ DEV = "dev"
24
+ RELEASE = "release"
25
+
26
+
27
+ class SupportedBackend(StrEnum):
28
+ POETRY = "poetry"
29
+ UV = "uv"
30
+
31
+
32
+ def load_config():
33
+ pyproject_path = Path.cwd() / "pyproject.toml"
34
+ if pyproject_path.exists():
35
+ with open(pyproject_path, "rb") as f:
36
+ pyproject_data = tomllib.load(f)
37
+ config = pyproject_data.get("tool", {}).get("versioning", {}).get("files")
38
+
39
+ backend = (
40
+ pyproject_data.get("tool", {}).get("versioning", {}).get("backend")
41
+ )
42
+ if backend is None:
43
+ raise RuntimeError(
44
+ "`pyproject.toml` file doesn't contain `tool.versioning.backend` property."
45
+ )
46
+ match SupportedBackend(backend):
47
+ case SupportedBackend.POETRY:
48
+ version = (
49
+ pyproject_data.get("tool", {}).get("poetry", {}).get("version")
50
+ )
51
+ case SupportedBackend.UV:
52
+ version = pyproject_data.get("project", {}).get("version")
53
+
54
+ if version is None:
55
+ raise RuntimeError(
56
+ "`pyproject.toml` file doesn't contain `version` property."
57
+ )
58
+
59
+ return version, config
60
+ else:
61
+ raise RuntimeError("Unable to locate `pyproject.toml` file.")
62
+
63
+
64
+ def parse_version(version) -> dict[str, str | Any]:
65
+ VERSION_PATTERN = re.compile(
66
+ r"^"
67
+ r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
68
+ r"(?:(?P<pre_type>a|b|rc)(?P<pre_num>\d+))?"
69
+ r"(?:\.post(?P<post>\d+))?"
70
+ r"(?:\.dev(?P<dev>\d+))?"
71
+ r"$"
72
+ )
73
+
74
+ pattern_match = VERSION_PATTERN.match(version)
75
+ if pattern_match:
76
+ return pattern_match.groupdict()
77
+ else:
78
+ raise ValueError(
79
+ f"Invalid version format {version}. Expected format: MAJOR.MINOR.PATCH[preTYPE[preNUM][.dev]]"
80
+ )
81
+
82
+
83
+ def find_substring_index(strings: list[str], substring: str) -> int:
84
+ return next(i for i, string in enumerate(strings) if substring in string)
85
+
86
+
87
+ def find_variable_and_replace_value_in_file(
88
+ file_name: str,
89
+ variable_name: str,
90
+ new_value: str,
91
+ dry_run: bool,
92
+ return_changed_line_number: bool = False,
93
+ ) -> None | int:
94
+ file = Path.cwd() / file_name
95
+ text = file.read_text().splitlines(keepends=True)
96
+ line_index = find_substring_index(text, variable_name)
97
+ text[line_index] = text[line_index].replace(
98
+ re.search(f"{variable_name}.*$", text[line_index]).group(0),
99
+ variable_name + " = " + f'"{new_value}"',
100
+ )
101
+
102
+ if not dry_run:
103
+ file.write_text("".join(text))
104
+
105
+ if return_changed_line_number:
106
+ return line_index + 1
107
+
108
+
109
+ def sync_version_in_different_files(
110
+ version, config, dry_run: bool
111
+ ) -> list[dict[str, int | str]]:
112
+ files_synced = []
113
+ if "version_variable" in config:
114
+ for value in config.get("version_variable"):
115
+ file_name = value.split(":")[0]
116
+ variable_name = value.split(":")[1]
117
+
118
+ line_number = find_variable_and_replace_value_in_file(
119
+ file_name,
120
+ variable_name,
121
+ version,
122
+ dry_run,
123
+ return_changed_line_number=True,
124
+ )
125
+
126
+ files_synced.append(
127
+ {
128
+ "file_path": file_name,
129
+ "line_number": line_number,
130
+ }
131
+ )
132
+
133
+ return files_synced
134
+
135
+
136
+ def versioning(level: VersionLevel, current_version: str, dry_run: bool) -> str:
137
+ version_parts = parse_version(current_version)
138
+
139
+ match level:
140
+ case VersionLevel.MAJOR:
141
+ new_version = f"{int(version_parts['major']) + 1}.0.0"
142
+ case VersionLevel.MINOR:
143
+ new_version = (
144
+ f"{version_parts['major']}.{int(version_parts['minor']) + 1}.0"
145
+ )
146
+ case VersionLevel.PATCH:
147
+ new_version = f"{version_parts['major']}.{version_parts['minor']}.{int(version_parts['patch']) + 1}"
148
+ case VersionLevel.ALPHA | VersionLevel.BETA | VersionLevel.RC:
149
+ pre_type_map = {
150
+ VersionLevel.ALPHA: "a",
151
+ VersionLevel.BETA: "b",
152
+ VersionLevel.RC: "rc",
153
+ }
154
+
155
+ if version_parts["pre_type"] == pre_type_map[level]:
156
+ pre_num = int(version_parts["pre_num"] or 0) + 1
157
+ else:
158
+ pre_num = 1
159
+
160
+ new_version = f"{version_parts['major']}.{version_parts['minor']}.{version_parts['patch']}{pre_type_map[level]}{pre_num}"
161
+ case VersionLevel.DEV:
162
+ if version_parts["pre_type"] and version_parts["pre_num"]:
163
+ pre_parts = f"{version_parts['pre_type']}{version_parts['pre_num']}"
164
+ else:
165
+ pre_parts = ""
166
+
167
+ dev_num = int(version_parts["dev"] or 0) + 1
168
+ new_version = f"{version_parts['major']}.{version_parts['minor']}.{version_parts['patch']}{pre_parts}.dev{dev_num}"
169
+ case VersionLevel.RELEASE:
170
+ if (
171
+ version_parts["pre_type"]
172
+ or version_parts["pre_num"]
173
+ or version_parts["dev"]
174
+ ):
175
+ new_version = f"{version_parts['major']}.{version_parts['minor']}.{version_parts['patch']}"
176
+ else:
177
+ raise ValueError(
178
+ f"The project is already on release version {current_version}"
179
+ )
180
+
181
+ if not dry_run:
182
+ find_variable_and_replace_value_in_file(
183
+ "pyproject.toml", "version", new_version, dry_run=False
184
+ )
185
+
186
+ return new_version
187
+
188
+
189
+ def display_update_summary(
190
+ console: Console, files_synced: list[dict[str, int | str]]
191
+ ) -> None:
192
+ grid = Table.grid()
193
+ grid.add_column(justify="center")
194
+
195
+ for sync_infomation in files_synced:
196
+ grid.add_row(
197
+ f"Synced version updates in file [cyan]`{sync_infomation['file_path']}`[/cyan] at line [cyan]{sync_infomation['line_number']}[/cyan]"
198
+ )
199
+
200
+ console.print(
201
+ Panel(grid, title="Version Update Summary", border_style="blue", padding=(1, 2))
202
+ )
203
+
204
+
205
+ @click.command()
206
+ @click.argument(
207
+ "level", type=click.Choice([vl.value for vl in VersionLevel], case_sensitive=False)
208
+ )
209
+ @click.option(
210
+ "--show-summary",
211
+ "-s",
212
+ is_flag=True,
213
+ help="Show what files changed",
214
+ )
215
+ @click.option(
216
+ "--dry-run",
217
+ "-d",
218
+ is_flag=True,
219
+ help="Show what would be done without making changes",
220
+ )
221
+ def main(level: str, show_summary: bool, dry_run: bool):
222
+ console = Console()
223
+
224
+ current_version, versioning_config = load_config()
225
+ new_version = versioning(VersionLevel(level), current_version, dry_run)
226
+ files_synced = sync_version_in_different_files(
227
+ new_version, versioning_config, dry_run
228
+ )
229
+
230
+ if dry_run:
231
+ console.print(
232
+ f"[yellow]Dry Run[/yellow]: Bumped version: [bold cyan]{current_version}[/bold cyan] :arrow_right: [bold green]{new_version}[/bold green]"
233
+ )
234
+ else:
235
+ console.print(
236
+ f"Bumped version: [bold cyan]{current_version}[/bold cyan] :arrow_right: [bold green]{new_version}[/bold green]"
237
+ )
238
+
239
+ if show_summary:
240
+ display_update_summary(console, files_synced)
241
+
242
+
243
+ if __name__ == "__main__":
244
+ main()
pyproject.toml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "arcana-in-action"
3
+ version = "0.0.0"
4
+ description = "Huggingface Demo showcasing how Arcana works"
5
+ authors = [{ name = "Saurabh Ghanekar", email = "[email protected]" }]
6
+ license = "BSD-2-Clause"
7
+ license-files = ["LICENSE"]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "fastapi-slim>=0.115",
12
+ "uvicorn>=0.34",
13
+ "pydantic>=2.10",
14
+ "pillow>=11.1",
15
+ "arcana-codex>=0.2",
16
+ "llama-cpp-python>=0.3",
17
+ "huggingface-hub>=0.29",
18
+ "pymongo>=4.11",
19
+ "email-validator>=2.2",
20
+ ]
21
+
22
+
23
+ [dependency-groups]
24
+ dev = ["click>=8.1", "pre-commit>=4.0", "rich>=13.9", "ruff>=0.9"]
25
+ test = ["pytest>=8.3", "pytest-cov>=6.0"]
26
+
27
+
28
+ [tool.uv]
29
+ default-groups = ["dev", "test"]
30
+
31
+
32
+ [tool.ruff]
33
+ line-length = 88
34
+
35
+
36
+ [tool.ruff.lint]
37
+ select = [
38
+ # "D", # pydocstyle
39
+ "E", # pycodestyle errors
40
+ "W", # pycodestyle warnings
41
+ "F", # pyflakes
42
+ "I", # isort
43
+ "B", # flake8-bugbear
44
+ "C4", # flake8-comprehensions
45
+ "UP", # pyupgrade
46
+ "ARG001", # unused arguments in functions
47
+ ]
48
+ ignore = [
49
+ "E501", # line too long, handled by black
50
+ "B008", # do not perform function calls in argument defaults
51
+ "W191", # indentation contains tabs
52
+ "B904", # Allow raising exceptions without from e, for HTTPException
53
+ ]
54
+
55
+
56
+ [tool.ruff.lint.pydocstyle]
57
+ convention = "google"
58
+
59
+ [tool.versioning]
60
+ backend = "uv"
61
+
62
+ [tool.versioning.files]
63
+ version_variable = ["app/app/main.py:__version__"]
requirements.txt DELETED
@@ -1,7 +0,0 @@
1
- fastapi-slim
2
- uvicorn[standard]
3
- arcana-codex
4
- pydantic
5
- pillow
6
- llama-cpp-python
7
- huggingface-hub
 
 
 
 
 
 
 
 
uv.lock ADDED
The diff for this file is too large to render. See raw diff