tinysemver 2.1.0__tar.gz → 3.0.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.
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: tinysemver
3
- Version: 2.1.0
4
- Summary: Semantic-Versioning with LLMs and without 300,000 lines of JS
5
- Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
3
+ Version: 3.0.0
4
+ Summary: Semantic-Versioning... that doesn't need 300,000 lines of JS
5
+ Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>
6
6
  License: Apache License
7
7
  Version 2.0, January 2004
8
8
  http://www.apache.org/licenses/
@@ -206,10 +206,10 @@ License: Apache License
206
206
  limitations under the License.
207
207
 
208
208
  Project-URL: Homepage, https://pypi.org/project/tinysemver/
209
- Project-URL: Documentation, https://github.com/ashvardanian/tinysemver/blob/main/README.md
210
- Project-URL: Repository, https://github.com/ashvardanian/tinysemver.git
211
- Project-URL: Issues, https://github.com/ashvardanian/tinysemver/issues
212
- Project-URL: Changelog, https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md
209
+ Project-URL: Documentation, https://github.com/ashvardanian/TinySemVer/blob/main/README.md
210
+ Project-URL: Repository, https://github.com/ashvardanian/TinySemVer.git
211
+ Project-URL: Issues, https://github.com/ashvardanian/TinySemVer/issues
212
+ Project-URL: Changelog, https://github.com/ashvardanian/TinySemVer/blob/main/CHANGELOG.md
213
213
  Classifier: Development Status :: 5 - Production/Stable
214
214
  Classifier: Intended Audience :: Developers
215
215
  Classifier: Topic :: Software Development :: Build Tools
@@ -223,12 +223,23 @@ Classifier: Programming Language :: Python :: 3.10
223
223
  Classifier: Programming Language :: Python :: 3.11
224
224
  Description-Content-Type: text/markdown
225
225
  License-File: LICENSE
226
- Requires-Dist: openai>=1.54.4
227
226
  Requires-Dist: rich>=13.8.0
227
+ Provides-Extra: test
228
+ Requires-Dist: pytest>=7.0.0; extra == "test"
229
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
230
+ Requires-Dist: pytest-mock>=3.10.0; extra == "test"
231
+ Provides-Extra: dev
232
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
233
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
234
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
235
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
236
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
237
+ Requires-Dist: black>=23.0.0; extra == "dev"
238
+ Dynamic: license-file
228
239
 
229
240
  ![TinySemVer Banner](https://github.com/ashvardanian/ashvardanian/blob/master/repositories/TinySemVer.jpg)
230
241
 
231
- __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
242
+ __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
232
243
  In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
233
244
  Here is how to integrate it into your project CI:
234
245
 
@@ -306,19 +317,6 @@ Alternatively, you can just ask for `--help`:
306
317
  $ tinysemver --help
307
318
  ```
308
319
 
309
- ## AI and Rock-n-Roll
310
-
311
- TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
312
-
313
- ```sh
314
- $ tinysemver --verbose \
315
- --github-repository 'ashvardanian/tinysemver' \
316
- --openai-base-url 'https://api.groq.com/openai/v1' \
317
- --openai-api-key 'GET_YOURSELF_A_KEY' \
318
- --openai-model 'llama-3.2-11b-text-preview' \
319
- --dry-run
320
- ```
321
-
322
320
  ## Usage Details for the GitHub CI Action
323
321
 
324
322
  TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
@@ -337,12 +335,12 @@ jobs:
337
335
 
338
336
  steps:
339
337
  - name: Checkout
340
- uses: actions/checkout@v4
338
+ uses: actions/checkout@v5
341
339
  with:
342
340
  persist-credentials: false # Only if main branch if protected
343
341
 
344
342
  - name: Run TinySemVer
345
- uses: ashvardanian/tinysemver@v2.0.1
343
+ uses: ashvardanian/tinysemver@v3 # Automatically receives non-breaking updates
346
344
  with:
347
345
  major-verbs: 'breaking,break,major'
348
346
  minor-verbs: 'feature,minor,add,new'
@@ -363,7 +361,7 @@ jobs:
363
361
  runs-on: ubuntu-latest
364
362
 
365
363
  steps:
366
- - uses: actions/checkout@v4
364
+ - uses: actions/checkout@v5
367
365
  with:
368
366
  ref: main # Take the most recent updated version
369
367
  ```
@@ -382,6 +380,30 @@ If you need to update the version in multiple files, pass a multiline string wit
382
380
 
383
381
  For examples, consider checking StringZilla, USearch, and other libraries using TinySemVer.
384
382
 
383
+ Since version 3.0.0, TinySemVer can optionally maintain moving version tags similar to `actions/checkout@v4`:
384
+
385
+ ```yaml
386
+ uses: ashvardanian/tinysemver@v3 # Automatically gets non-breaking updates
387
+ uses: ashvardanian/tinysemver@v3.0 # Automatically gets patch updates
388
+ uses: ashvardanian/tinysemver@v3.0.0 # Pinned, never changes
389
+ uses: ashvardanian/tinysemver@main # Bleeding edge
390
+ ```
391
+
392
+ This feature is disabled by default because it requires force-push permissions.
393
+ To enable it, add `push-moving-tags: 'true'` and ensure your token has force-push access:
394
+
395
+ ```yaml
396
+ - name: Run TinySemVer
397
+ uses: ashvardanian/tinysemver@v3
398
+ with:
399
+ push: 'true'
400
+ push-moving-tags: 'true'
401
+ github-token: ${{ secrets.TINYSEMVER_TOKEN }}
402
+ ```
403
+
404
+ When enabled, TinySemVer creates three tags: `v3` (major), `v3.0` (minor), and `v3.0.0` (specific).
405
+ The default `GITHUB_TOKEN` typically lacks force-push permissions for protected branches.
406
+
385
407
  ### Security Considerations
386
408
 
387
409
  If your default branch is protected with a "pull request before merging" rule:
@@ -1,6 +1,6 @@
1
1
  ![TinySemVer Banner](https://github.com/ashvardanian/ashvardanian/blob/master/repositories/TinySemVer.jpg)
2
2
 
3
- __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
3
+ __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
4
4
  In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
5
5
  Here is how to integrate it into your project CI:
6
6
 
@@ -78,19 +78,6 @@ Alternatively, you can just ask for `--help`:
78
78
  $ tinysemver --help
79
79
  ```
80
80
 
81
- ## AI and Rock-n-Roll
82
-
83
- TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
84
-
85
- ```sh
86
- $ tinysemver --verbose \
87
- --github-repository 'ashvardanian/tinysemver' \
88
- --openai-base-url 'https://api.groq.com/openai/v1' \
89
- --openai-api-key 'GET_YOURSELF_A_KEY' \
90
- --openai-model 'llama-3.2-11b-text-preview' \
91
- --dry-run
92
- ```
93
-
94
81
  ## Usage Details for the GitHub CI Action
95
82
 
96
83
  TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
@@ -109,12 +96,12 @@ jobs:
109
96
 
110
97
  steps:
111
98
  - name: Checkout
112
- uses: actions/checkout@v4
99
+ uses: actions/checkout@v5
113
100
  with:
114
101
  persist-credentials: false # Only if main branch if protected
115
102
 
116
103
  - name: Run TinySemVer
117
- uses: ashvardanian/tinysemver@v2.0.1
104
+ uses: ashvardanian/tinysemver@v3 # Automatically receives non-breaking updates
118
105
  with:
119
106
  major-verbs: 'breaking,break,major'
120
107
  minor-verbs: 'feature,minor,add,new'
@@ -135,7 +122,7 @@ jobs:
135
122
  runs-on: ubuntu-latest
136
123
 
137
124
  steps:
138
- - uses: actions/checkout@v4
125
+ - uses: actions/checkout@v5
139
126
  with:
140
127
  ref: main # Take the most recent updated version
141
128
  ```
@@ -154,6 +141,30 @@ If you need to update the version in multiple files, pass a multiline string wit
154
141
 
155
142
  For examples, consider checking StringZilla, USearch, and other libraries using TinySemVer.
156
143
 
144
+ Since version 3.0.0, TinySemVer can optionally maintain moving version tags similar to `actions/checkout@v4`:
145
+
146
+ ```yaml
147
+ uses: ashvardanian/tinysemver@v3 # Automatically gets non-breaking updates
148
+ uses: ashvardanian/tinysemver@v3.0 # Automatically gets patch updates
149
+ uses: ashvardanian/tinysemver@v3.0.0 # Pinned, never changes
150
+ uses: ashvardanian/tinysemver@main # Bleeding edge
151
+ ```
152
+
153
+ This feature is disabled by default because it requires force-push permissions.
154
+ To enable it, add `push-moving-tags: 'true'` and ensure your token has force-push access:
155
+
156
+ ```yaml
157
+ - name: Run TinySemVer
158
+ uses: ashvardanian/tinysemver@v3
159
+ with:
160
+ push: 'true'
161
+ push-moving-tags: 'true'
162
+ github-token: ${{ secrets.TINYSEMVER_TOKEN }}
163
+ ```
164
+
165
+ When enabled, TinySemVer creates three tags: `v3` (major), `v3.0` (minor), and `v3.0.0` (specific).
166
+ The default `GITHUB_TOKEN` typically lacks force-push permissions for protected branches.
167
+
157
168
  ### Security Considerations
158
169
 
159
170
  If your default branch is protected with a "pull request before merging" rule:
@@ -4,13 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinysemver"
7
- version = "2.1.0"
8
- description = "Semantic-Versioning with LLMs and without 300,000 lines of JS"
7
+ version = "3.0.0"
8
+ description = "Semantic-Versioning... that doesn't need 300,000 lines of JS"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  authors = [
12
- { name = "Ash Vardanian", email = "1983160+ashvardanian@users.noreply.github.com" },
13
- { name = "Guillaume de Rouville", email = "31691250+grouville@users.noreply.github.com" },
12
+ { name = "Ash Vardanian", email = "1983160+ashvardanian@users.noreply.github.com" }
14
13
  ]
15
14
  classifiers = [
16
15
  "Development Status :: 5 - Production/Stable",
@@ -25,17 +24,32 @@ classifiers = [
25
24
  "Programming Language :: Python :: 3.10",
26
25
  "Programming Language :: Python :: 3.11",
27
26
  ]
28
- dependencies = ["openai >= 1.54.4", "rich >= 13.8.0"]
27
+ dependencies = ["rich >= 13.8.0"]
28
+
29
+ [project.optional-dependencies]
30
+ test = [
31
+ "pytest >= 7.0.0",
32
+ "pytest-cov >= 4.0.0",
33
+ "pytest-mock >= 3.10.0",
34
+ ]
35
+ dev = [
36
+ "pytest >= 7.0.0",
37
+ "pytest-cov >= 4.0.0",
38
+ "pytest-mock >= 3.10.0",
39
+ "ruff >= 0.1.0",
40
+ "mypy >= 1.0.0",
41
+ "black >= 23.0.0",
42
+ ]
29
43
 
30
44
  [project.scripts]
31
45
  tinysemver = "tinysemver.tinysemver:main"
32
46
 
33
47
  [project.urls]
34
48
  Homepage = "https://pypi.org/project/tinysemver/"
35
- Documentation = "https://github.com/ashvardanian/tinysemver/blob/main/README.md"
36
- Repository = "https://github.com/ashvardanian/tinysemver.git"
37
- Issues = "https://github.com/ashvardanian/tinysemver/issues"
38
- Changelog = "https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md"
49
+ Documentation = "https://github.com/ashvardanian/TinySemVer/blob/main/README.md"
50
+ Repository = "https://github.com/ashvardanian/TinySemVer.git"
51
+ Issues = "https://github.com/ashvardanian/TinySemVer/issues"
52
+ Changelog = "https://github.com/ashvardanian/TinySemVer/blob/main/CHANGELOG.md"
39
53
 
40
54
  [tool.setuptools]
41
55
  package-dir = { "" = "." }
@@ -0,0 +1,75 @@
1
+ """TinySemVer - A tiny but mighty Semantic Versioning tool.
2
+
3
+ This package provides automated semantic versioning for Git repositories
4
+ following Conventional Commits. It can generate changelogs and release notes
5
+ with or without LLM assistance.
6
+
7
+ Example:
8
+ >>> from tinysemver import bump
9
+ >>> new_version = bump(
10
+ ... path=".",
11
+ ... dry_run=True,
12
+ ... verbose=True,
13
+ ... version_file="VERSION",
14
+ ... changelog_file="CHANGELOG.md"
15
+ ... )
16
+ """
17
+
18
+ from tinysemver.tinysemver import (
19
+ # Main function
20
+ bump,
21
+ # Version utilities
22
+ parse_version,
23
+ bump_version,
24
+ # Commit utilities
25
+ commit_starts_with_verb,
26
+ normalize_verbs,
27
+ group_commits,
28
+ convert_commits_to_message,
29
+ # Git utilities
30
+ get_last_tag,
31
+ get_commits_since_tag,
32
+ get_diff_for_commit,
33
+ # File utilities
34
+ patch_with_regex,
35
+ # Tag creation
36
+ create_tag,
37
+ # Types
38
+ SemVer,
39
+ BumpType,
40
+ PathLike,
41
+ Commit,
42
+ ChangeDiff,
43
+ # Exceptions
44
+ NoNewCommitsError,
45
+ )
46
+
47
+ __version__ = "2.1.1"
48
+ __all__ = [
49
+ # Main function
50
+ "bump",
51
+ # Version utilities
52
+ "parse_version",
53
+ "bump_version",
54
+ # Commit utilities
55
+ "commit_starts_with_verb",
56
+ "normalize_verbs",
57
+ "group_commits",
58
+ "convert_commits_to_message",
59
+ # Git utilities
60
+ "get_last_tag",
61
+ "get_commits_since_tag",
62
+ "get_diff_for_commit",
63
+ # File utilities
64
+ "patch_with_regex",
65
+ # Tag creation
66
+ "create_tag",
67
+ # Types
68
+ "SemVer",
69
+ "BumpType",
70
+ "PathLike",
71
+ "Commit",
72
+ "ChangeDiff",
73
+ # Exceptions
74
+ "NoNewCommitsError",
75
+ ]
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """TinySemVer is a tiny but mighty Semantic Versioning tool with AI, Drugs, and Rock-n-Roll.
3
+ """TinySemVer is a tiny minimalistic Semantic Versioning tool.
4
4
 
5
5
  This module traces the commit history of a Git repository after the last tag, and based
6
6
  on the commit messages, it determines the type of version bump (major, minor, or patch).
@@ -15,7 +15,7 @@ Example:
15
15
  --minor-verbs 'feature,minor,add,new' \
16
16
  --patch-verbs 'fix,patch,bug,improve,docs,make' \
17
17
  --changelog-file 'CHANGELOG.md' \
18
- --guide-file 'CONTIBUTING.md' \
18
+ --guide-file 'CONTRIBUTING.md' \
19
19
  --version-file 'VERSION' \
20
20
  --update-version-in 'package.json' '"version": "(.*)"' \
21
21
  --update-version-in 'CITATION.cff' '^version: (.*)' \
@@ -67,39 +67,10 @@ class NoNewCommitsError(Exception):
67
67
  pass
68
68
 
69
69
 
70
- class MayContainVulnerability(Warning):
71
- """Raised when the commit may contain a vulnerability."""
72
-
73
- pass
74
-
75
-
76
- class MayContainLogicalBugs(Warning):
77
- """Raised when the commit may contain logical bugs."""
78
-
79
- pass
80
-
81
-
82
- class MayContainBreakingChange(Warning):
83
- """Raised when the commit may contain a breaking change."""
84
-
85
- pass
86
-
87
-
88
- class MayLackDocumentation(Warning):
89
- """Raised when the commit may lack documentation updates."""
90
-
91
- pass
92
-
93
-
94
- class UnknownCommitWarning(Warning):
95
- """Something in the commit doesn't add up."""
96
-
97
- pass
98
70
 
99
71
 
100
72
  _console = None # Initialize global console variable
101
73
  _console_is_rich = True # Initialize global console type variable
102
- _openai_client = None # Initialize global OpenAI client variable
103
74
 
104
75
 
105
76
  def print_to_console(message: str) -> None:
@@ -120,16 +91,6 @@ def print_to_console(message: str) -> None:
120
91
  print(message)
121
92
 
122
93
 
123
- def get_open_ai_client(base_url: str, api_key: str):
124
- # Create a global variable for the client
125
- global _openai_client
126
- if not _openai_client:
127
- from openai import OpenAI
128
-
129
- _openai_client = OpenAI(base_url=base_url, api_key=api_key)
130
- return _openai_client
131
-
132
-
133
94
  def get_last_tag(repository_path: PathLike) -> str:
134
95
  """Retrieve the last Git tag name from the repository."""
135
96
  result = subprocess.run(
@@ -262,6 +223,7 @@ def create_tag(
262
223
  github_repository: Optional[str] = None,
263
224
  push: bool = False,
264
225
  create_release: bool = False,
226
+ push_moving_tags: bool = False,
265
227
  major_commits: List[Commit] = None,
266
228
  minor_commits: List[Commit] = None,
267
229
  patch_commits: List[Commit] = None,
@@ -274,7 +236,8 @@ def create_tag(
274
236
  env["GIT_COMMITTER_EMAIL"] = user_email
275
237
  env["GIT_AUTHOR_NAME"] = user_name
276
238
  env["GIT_AUTHOR_EMAIL"] = user_email
277
- env["GITHUB_TOKEN"] = github_token
239
+ if github_token:
240
+ env["GITHUB_TOKEN"] = github_token
278
241
 
279
242
  message = f"Release: {tag} [skip ci]"
280
243
  message += convert_commits_to_message(major_commits or [], minor_commits or [], patch_commits or [])
@@ -293,6 +256,29 @@ def create_tag(
293
256
 
294
257
  subprocess.run(["git", "tag", "-a", tag, "-m", message, new_commit_sha], cwd=repository_path, env=env)
295
258
  print_to_console(f"[bold green]Created new tag:[/bold green] {tag}")
259
+
260
+ # Optionally create moving major and minor version tags for GitHub Actions compatibility
261
+ # This allows users to reference @v2 or @v2.1 instead of @v2.1.1
262
+ # Note: This requires force-push permissions
263
+ if push_moving_tags:
264
+ major_tag = f"v{version[0]}"
265
+ minor_tag = f"v{version[0]}.{version[1]}"
266
+
267
+ # Force-update major version tag (e.g., v2 -> v2.1.1)
268
+ subprocess.run(
269
+ ["git", "tag", "-fa", major_tag, "-m", f"Update {major_tag} to {tag}", new_commit_sha],
270
+ cwd=repository_path,
271
+ env=env,
272
+ )
273
+ print_to_console(f"[bold green]Updated major version tag:[/bold green] {major_tag} -> {tag}")
274
+
275
+ # Force-update minor version tag (e.g., v2.1 -> v2.1.1)
276
+ subprocess.run(
277
+ ["git", "tag", "-fa", minor_tag, "-m", f"Update {minor_tag} to {tag}", new_commit_sha],
278
+ cwd=repository_path,
279
+ env=env,
280
+ )
281
+ print_to_console(f"[bold green]Updated minor version tag:[/bold green] {minor_tag} -> {tag}")
296
282
  if push:
297
283
  url = None
298
284
  if github_token and github_repository:
@@ -315,12 +301,41 @@ def create_tag(
315
301
  f"Failed to push the new commits to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
316
302
  )
317
303
 
318
- push_result = subprocess.run(["git", "push", url, "--tag"], cwd=repository_path, capture_output=True, env=env)
304
+ # Push the specific version tag (e.g., v2.1.1)
305
+ push_result = subprocess.run(["git", "push", url, tag], cwd=repository_path, capture_output=True, env=env)
319
306
  if push_result.returncode != 0:
320
307
  raise RuntimeError(
321
- f"Failed to push the new tag to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
308
+ f"Failed to push the tag '{tag}' to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
309
+ )
310
+
311
+ print_to_console(f"[bold green]Pushed tag:[/bold green] {tag}")
312
+
313
+ # Optionally force-push the moving major and minor version tags
314
+ if push_moving_tags:
315
+ major_tag = f"v{version[0]}"
316
+ minor_tag = f"v{version[0]}.{version[1]}"
317
+
318
+ # Force-push the major version tag (e.g., v2)
319
+ push_result = subprocess.run(
320
+ ["git", "push", url, major_tag, "--force"], cwd=repository_path, capture_output=True, env=env
321
+ )
322
+ if push_result.returncode != 0:
323
+ raise RuntimeError(
324
+ f"Failed to push major tag '{major_tag}' to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
325
+ )
326
+
327
+ # Force-push the minor version tag (e.g., v2.1)
328
+ push_result = subprocess.run(
329
+ ["git", "push", url, minor_tag, "--force"], cwd=repository_path, capture_output=True, env=env
322
330
  )
323
- print_to_console(f"[bold green]Pushed to:[/bold green] {url}")
331
+ if push_result.returncode != 0:
332
+ raise RuntimeError(
333
+ f"Failed to push minor tag '{minor_tag}' to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
334
+ )
335
+
336
+ print_to_console(f"[bold green]Pushed moving tags:[/bold green]")
337
+ print_to_console(f"[bold green] - Major:[/bold green] {major_tag}")
338
+ print_to_console(f"[bold green] - Minor:[/bold green] {minor_tag}")
324
339
 
325
340
  # Create a release using GitHub CLI if available
326
341
  if create_release and github_repository:
@@ -370,6 +385,7 @@ def patch_with_regex(
370
385
  ) -> None:
371
386
  """Update a file by replacing the first matched group of every RegEx match with a new version."""
372
387
 
388
+ assert os.path.exists(file_path), f"File missing: {file_path}"
373
389
  with open(file_path, "r") as file:
374
390
  old_content = file.read()
375
391
 
@@ -388,7 +404,7 @@ def patch_with_regex(
388
404
  # Compile the regex pattern with multiline support
389
405
  regex_pattern = re.compile(regex_pattern, re.MULTILINE)
390
406
  matches = list(re.finditer(regex_pattern, old_content))
391
- new_content = re.sub(regex_pattern, replace_first_group, old_content, 1)
407
+ new_content = re.sub(regex_pattern, replace_first_group, old_content, count=1)
392
408
 
393
409
  non_empty_matches = [m for m in matches if len(m.group(0).strip())]
394
410
  assert len(non_empty_matches) > 0, f"No matches found in: {file_path}"
@@ -396,7 +412,7 @@ def patch_with_regex(
396
412
  for match in non_empty_matches:
397
413
  match_line = old_content.count("\n", 0, match.pos) + 1
398
414
  old_slice = match.group(0)
399
- new_slice = re.sub(regex_pattern, replace_first_group, old_slice, 1)
415
+ new_slice = re.sub(regex_pattern, replace_first_group, old_slice, count=1)
400
416
 
401
417
  if verbose:
402
418
  print_to_console(f"[bold cyan]Will update file:[/bold cyan] {file_path}:{match_line}")
@@ -410,117 +426,12 @@ def patch_with_regex(
410
426
  print_to_console(f"[bold green]File updated successfully:[/bold green] {file_path}")
411
427
 
412
428
 
413
- def validate_commit_with_llms(
414
- base_url: str,
415
- api_key: str,
416
- model: Optional[str],
417
- commit: Commit,
418
- change: ChangeDiff,
419
- ) -> Optional[Warning]:
420
- prompt = f"""
421
- You are a professional coding assistant helping me to validate a commit message and its changes.
422
-
423
- 1. Check if the changed code is likely to introduce logical bugs.
424
- 2. Check if the commit message is likely to contain a vulnerability.
425
- 3. Check if it contains a breaking change that is likely to affect users.
426
- 4. Check if the documentation wasn't updated for the changes made.
427
-
428
- Don't let the commit message fool you, just look at the changes made in the code.
429
- Reply by choosing any number from 0 to 4 and describe the issue, if anything is found.
430
-
431
- 0. No issues found.
432
- 1. The commit may introduce logical bugs, such as the overflow in ...
433
- 2. The commit may contain a vulnerability, such as the SQL injection in ...
434
- 3. The commit may contain a breaking change, such as the removal of ...
435
- 4. The commit may lack documentation updates for the changes made in function ...
436
- """
437
- message = f"""
438
- #{commit.hash}: {commit.message}
439
-
440
- Changes:
441
-
442
- {change}
443
- """
444
- client = get_open_ai_client(base_url=base_url, api_key=api_key)
445
- response = client.chat.completions.create(
446
- messages=[
447
- {"role": "system", "content": prompt},
448
- {"role": "user", "content": message},
449
- ],
450
- max_tokens=256,
451
- model=model,
452
- stream=False,
453
- )
454
- response_text = response.choices[0].message.content
455
- if response_text.startswith("0."):
456
- return None
457
- if response_text.startswith("1."):
458
- return MayContainLogicalBugs(response_text)
459
- if response_text.startswith("2."):
460
- return MayContainVulnerability(response_text)
461
- if response_text.startswith("3."):
462
- return MayContainBreakingChange(response_text)
463
- if response_text.startswith("4."):
464
- return MayLackDocumentation(response_text)
465
- return UnknownCommitWarning(response_text)
466
-
467
-
468
- def aggregate_release_notes_with_llms(
469
- base_url: str,
470
- api_key: str,
471
- model: Optional[str],
472
- github_repository: str,
473
- commits: List[Commit],
474
- changes: List[ChangeDiff],
475
- ) -> str:
476
-
477
- commits_with_hashes = "\n".join(f"- {commit.hash}: {commit.message}" for commit in commits)
478
- prompt = f"""
479
- You are a release notes generator for an advanced software project.
480
-
481
- Aggregate the release notes for the upcoming version based on the commits and their
482
- changes replying in a Github-flavored Markdown format.
483
-
484
- - Mention the new features, improvements, and bug fixes.
485
- - Warn about potential breaking changes and vulnerabilities.
486
- - Tag people and teams responsible for the changes.
487
- - For the most important commits provide a link to the issue or pull request and
488
- a URL to that commit, like [commit](https://github.com/{github_repository}/commit/...).
489
- - Don't guess anything, only use the information from the commits and their changes.
490
- - Use inline math notation, like $\\beta=1$ or math blocks like the following, when needed:
491
-
492
- ```math
493
- \\S_i(A, B, \\alpha, \\beta) = \\alpha \\cdot A_i + \\beta \\cdot B_i
494
- ```
495
-
496
- - Use alerting quotes, like: [!CAUTION] or [!TIP], when needed.
497
- """
498
- header_message = f"""Commits:
499
-
500
- {commits_with_hashes}
501
- """
502
- changes_messages = [f"#{commit.hash}: {commit.message}\n{change}" for commit, change in zip(commits, changes)]
503
-
504
- client = OpenAI(base_url=base_url, api_key=api_key)
505
- response = client.chat.completions.create(
506
- messages=[
507
- {"role": "system", "content": prompt},
508
- {"role": "user", "content": header_message},
509
- *[{"role": "user", "content": message} for message in changes_messages],
510
- ],
511
- max_tokens=2048,
512
- model=model,
513
- stream=False,
514
- )
515
- return response.choices[0].message.content
516
-
517
-
518
429
  def bump(
519
430
  *, # enforce keyword-only arguments
520
431
  dry_run: bool = False,
521
432
  verbose: bool = False,
522
433
  major_verbs: List[str] = ["major", "breaking", "break"],
523
- minor_verbs: List[str] = ["minor", "feature", "add", "new"],
434
+ minor_verbs: List[str] = ["minor", "feature", "feat", "add", "new"],
524
435
  patch_verbs: List[str] = ["patch", "fix", "bug", "improve", "docs", "make"],
525
436
  path: Optional[PathLike] = None, # takes current directory as default
526
437
  changelog_file: Optional[PathLike] = None, # relative or absolute path to the changelog file
@@ -534,11 +445,9 @@ def bump(
534
445
  push: bool = True,
535
446
  github_token: Optional[str] = None,
536
447
  github_repository: Optional[str] = None,
537
- openai_base_url: str = "https://api.openai.com",
538
- openai_api_key: Optional[str] = None,
539
- openai_model: Optional[str] = None,
540
448
  default_branch: str = "main",
541
449
  create_release: bool = False,
450
+ push_moving_tags: bool = False,
542
451
  ) -> SemVer:
543
452
  """Primary function used to update the files (version, changelog, etc.), create a new Git tag, push it to a GitHub repository.
544
453
  It can be used for dry-runs to preview the changes without actually creating a new tag.
@@ -562,9 +471,6 @@ def bump(
562
471
  push (bool): If True, pushes the changes to the remote GitHub repository. Defaults to True.
563
472
  github_token (Optional[str]): The GitHub token for authentication. If None, it attempts to use the GH_TOKEN environment variable. Defaults to None.
564
473
  github_repository (Optional[str]): The GitHub repository in 'owner/repo' format. If None, it attempts to use the GH_REPOSITORY environment variable. Defaults to None.
565
- openai_base_url (str): The OpenAI API base URL. Defaults to "https://api.openai.com".
566
- openai_api_key (Optional[str]): The OpenAI API key. Defaults to None.
567
- openai_model (Optional[str]): The OpenAI model to use for text generation. Defaults to None.
568
474
  default_branch (str): The default branch to push the changes to. Defaults to "main".
569
475
 
570
476
  Returns:
@@ -574,12 +480,16 @@ def bump(
574
480
  repository_path = os.path.abspath(path) if path else os.getcwd()
575
481
  assert os.path.isdir(os.path.join(repository_path, ".git")), f"Not a Git repository: {repository_path}"
576
482
 
577
- def normalize_path(file_path: str) -> str:
578
- if not file_path or len(file_path) == 0:
483
+ def normalize_path(file_path) -> str:
484
+ if not file_path:
579
485
  return None
580
- if os.path.isabs(file_path):
581
- return file_path
582
- return os.path.join(repository_path, file_path)
486
+ # Convert `pathlib.Path` to `str`
487
+ file_path_str = str(file_path) if hasattr(file_path, '__fspath__') or isinstance(file_path, os.PathLike) else file_path
488
+ if not file_path_str or len(file_path_str) == 0:
489
+ return None
490
+ if os.path.isabs(file_path_str):
491
+ return file_path_str
492
+ return os.path.join(repository_path, file_path_str)
583
493
 
584
494
  changelog_file = normalize_path(changelog_file)
585
495
  version_file = normalize_path(version_file)
@@ -613,7 +523,7 @@ def bump(
613
523
  ), f"Changelog file is missing or isn't a regular file: {changelog_file}"
614
524
 
615
525
  major_verbs = normalize_verbs(major_verbs, ["major", "breaking", "break"])
616
- minor_verbs = normalize_verbs(minor_verbs, ["minor", "feature", "add", "new"])
526
+ minor_verbs = normalize_verbs(minor_verbs, ["minor", "feature", "feat", "add", "new"])
617
527
  patch_verbs = normalize_verbs(patch_verbs, ["patch", "fix", "bug", "improve", "docs", "make"])
618
528
 
619
529
  last_tag = get_last_tag(repository_path)
@@ -621,16 +531,16 @@ def bump(
621
531
 
622
532
  current_version = parse_version(last_tag)
623
533
  if verbose:
624
- print(f"Current version: {current_version[0]}.{current_version[1]}.{current_version[2]}")
534
+ print_to_console(f"Current version: {current_version[0]}.{current_version[1]}.{current_version[2]}")
625
535
 
626
536
  commits = get_commits_since_tag(repository_path, last_tag)
627
537
  if not len(commits):
628
538
  raise NoNewCommitsError(f"No new commits since the last {last_tag} tag")
629
539
 
630
540
  if verbose:
631
- print(f"? Commits since last tag: {len(commits)}")
541
+ print_to_console(f"? Commits since last tag: {len(commits)}")
632
542
  for hash, commit in commits:
633
- print(f"# {hash}: {commit}")
543
+ print_to_console(f"# {hash}: {commit}")
634
544
 
635
545
  major_commits, minor_commits, patch_commits = group_commits(commits, major_verbs, minor_verbs, patch_verbs)
636
546
  assert (
@@ -645,7 +555,7 @@ def bump(
645
555
  bump_type = "patch"
646
556
  new_version = bump_version(current_version, bump_type)
647
557
  if verbose:
648
- print(f"Next version: {new_version[0]}.{new_version[1]}.{new_version[2]} (type: {bump_type})")
558
+ print_to_console(f"Next version: {new_version[0]}.{new_version[1]}.{new_version[2]} (type: {bump_type})")
649
559
 
650
560
  new_version_str = f"{new_version[0]}.{new_version[1]}.{new_version[2]}"
651
561
  if version_file:
@@ -656,12 +566,12 @@ def bump(
656
566
  changes = f"\n## {now:%B %d, %Y}: v{new_version_str}\n"
657
567
  changes += convert_commits_to_message(major_commits, minor_commits, patch_commits)
658
568
 
659
- print(f"Will update file: {changelog_file}")
569
+ print_to_console(f"Will update file: {changelog_file}")
660
570
  if verbose:
661
571
  changes_lines = changes.count("\n") + 1
662
- print(f"? Appending {changes_lines} lines")
572
+ print_to_console(f"? Appending {changes_lines} lines")
663
573
  for line in changes.split("\n"):
664
- print(f"+ {line}")
574
+ print_to_console(f"+ {line}")
665
575
 
666
576
  if not dry_run:
667
577
  with open(changelog_file, "a") as file:
@@ -680,43 +590,6 @@ def bump(
680
590
  for file_path, regex_pattern in update_patch_version_in:
681
591
  patch_with_regex(file_path, regex_pattern, str(new_version[2]), dry_run=dry_run, verbose=verbose)
682
592
 
683
- # Now log the potential issues with the commits
684
- if openai_api_key:
685
- warnings_commits = []
686
- warnings = []
687
- changes = [get_diff_for_commit(repository_path, commit.hash) for commit in commits]
688
- for commit, change in zip(commits, changes):
689
- try:
690
- warning = validate_commit_with_llms(
691
- base_url=openai_base_url,
692
- api_key=openai_api_key,
693
- model=openai_model,
694
- commit=commit,
695
- change=change,
696
- )
697
- if warning:
698
- warnings_commits.append(commit)
699
- warnings.append(warning)
700
- except Exception as e:
701
- print(f"Failed to validate commit: {commit.hash} with error: {str(e)}")
702
- traceback.print_exc()
703
-
704
- if len(warnings):
705
- print("## Potential issues")
706
- for commit, warning in zip(warnings_commits, warnings):
707
- print(f"- Commit #{commit.hash}: {warning}")
708
-
709
- release_notes = aggregate_release_notes_with_llms(
710
- base_url=openai_base_url,
711
- api_key=openai_api_key,
712
- model=openai_model,
713
- github_repository=github_repository,
714
- commits=commits,
715
- changes=changes,
716
- )
717
- print("## Generated release notes:")
718
- print(release_notes)
719
-
720
593
  if not dry_run:
721
594
  create_tag(
722
595
  repository_path=repository_path,
@@ -728,11 +601,14 @@ def bump(
728
601
  github_repository=github_repository,
729
602
  push=push,
730
603
  create_release=create_release,
604
+ push_moving_tags=push_moving_tags,
731
605
  major_commits=major_commits,
732
606
  minor_commits=minor_commits,
733
607
  patch_commits=patch_commits,
734
608
  )
735
609
 
610
+ return new_version
611
+
736
612
 
737
613
  def main():
738
614
  """
@@ -834,25 +710,18 @@ def main():
834
710
  "--github-repository",
835
711
  help="GitHub repository in the 'owner/repo' format, if not set will use GH_REPOSITORY env var",
836
712
  )
837
- parser.add_argument(
838
- "--openai-base-url",
839
- default="https://api.openai.com",
840
- help="OpenAI API base URL",
841
- )
842
- parser.add_argument(
843
- "--openai-api-key",
844
- help="OpenAI API key for text generation",
845
- )
846
- parser.add_argument(
847
- "--openai-model",
848
- help="OpenAI model to use for text generation (e.g., 'text-davinci-003' or 'text-davinci-002')",
849
- )
850
713
  parser.add_argument(
851
714
  "--create-release",
852
715
  action="store_true",
853
716
  default=False,
854
717
  help="Create a GitHub release using the GitHub CLI",
855
718
  )
719
+ parser.add_argument(
720
+ "--push-moving-tags",
721
+ action="store_true",
722
+ default=False,
723
+ help="Push moving major and minor version tags (requires force-push permissions)",
724
+ )
856
725
  args = parser.parse_args()
857
726
  else:
858
727
 
@@ -864,7 +733,7 @@ def main():
864
733
  args.verbose = os.environ.get("TINYSEMVER_VERBOSE", "").lower() == "true"
865
734
  args.push = os.environ.get("TINYSEMVER_PUSH", "").lower() == "true"
866
735
  args.major_verbs = os.environ.get("TINYSEMVER_MAJOR_VERBS") or "breaking,break,major"
867
- args.minor_verbs = os.environ.get("TINYSEMVER_MINOR_VERBS") or "feature,minor,add,new"
736
+ args.minor_verbs = os.environ.get("TINYSEMVER_MINOR_VERBS") or "feature,feat,minor,add,new"
868
737
  args.patch_verbs = os.environ.get("TINYSEMVER_PATCH_VERBS") or "fix,patch,bug,improve,docs,make"
869
738
  args.default_branch = os.environ.get("TINYSEMVER_DEFAULT_BRANCH") or "main"
870
739
  args.changelog_file = os.environ.get("TINYSEMVER_CHANGELOG_FILE")
@@ -894,10 +763,8 @@ def main():
894
763
  args.git_user_email = os.environ.get("TINYSEMVER_GIT_USER_EMAIL", "tinysemver@ashvardanian.com")
895
764
  args.github_token = os.environ.get("GITHUB_TOKEN")
896
765
  args.github_repository = os.environ.get("GITHUB_REPOSITORY")
897
- args.openai_base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com")
898
- args.openai_api_key = os.environ.get("OPENAI_API_KEY")
899
- args.openai_model = os.environ.get("OPENAI_MODEL")
900
766
  args.create_release = os.environ.get("TINYSEMVER_CREATE_RELEASE", "").lower() == "true"
767
+ args.push_moving_tags = os.environ.get("TINYSEMVER_PUSH_MOVING_TAGS", "").lower() == "true"
901
768
 
902
769
  # It's common for a CI pipeline to have multiple broken settings or missing files.
903
770
  # For the best user experience, we want to catch all errors and print them at once,
@@ -925,11 +792,9 @@ def main():
925
792
  git_user_email=args.git_user_email,
926
793
  github_token=args.github_token,
927
794
  github_repository=args.github_repository,
928
- openai_base_url=args.openai_base_url,
929
- openai_api_key=args.openai_api_key,
930
- openai_model=args.openai_model,
931
795
  push=args.push,
932
796
  create_release=args.create_release,
797
+ push_moving_tags=args.push_moving_tags,
933
798
  )
934
799
  except NoNewCommitsError as e:
935
800
  print(f"! {e}")
@@ -938,7 +803,7 @@ def main():
938
803
  print(f"! {e}")
939
804
  exit(1)
940
805
  except Exception as e:
941
- traceback.print_exc(e)
806
+ traceback.print_exc()
942
807
  exit(1)
943
808
 
944
809
 
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: tinysemver
3
- Version: 2.1.0
4
- Summary: Semantic-Versioning with LLMs and without 300,000 lines of JS
5
- Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
3
+ Version: 3.0.0
4
+ Summary: Semantic-Versioning... that doesn't need 300,000 lines of JS
5
+ Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>
6
6
  License: Apache License
7
7
  Version 2.0, January 2004
8
8
  http://www.apache.org/licenses/
@@ -206,10 +206,10 @@ License: Apache License
206
206
  limitations under the License.
207
207
 
208
208
  Project-URL: Homepage, https://pypi.org/project/tinysemver/
209
- Project-URL: Documentation, https://github.com/ashvardanian/tinysemver/blob/main/README.md
210
- Project-URL: Repository, https://github.com/ashvardanian/tinysemver.git
211
- Project-URL: Issues, https://github.com/ashvardanian/tinysemver/issues
212
- Project-URL: Changelog, https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md
209
+ Project-URL: Documentation, https://github.com/ashvardanian/TinySemVer/blob/main/README.md
210
+ Project-URL: Repository, https://github.com/ashvardanian/TinySemVer.git
211
+ Project-URL: Issues, https://github.com/ashvardanian/TinySemVer/issues
212
+ Project-URL: Changelog, https://github.com/ashvardanian/TinySemVer/blob/main/CHANGELOG.md
213
213
  Classifier: Development Status :: 5 - Production/Stable
214
214
  Classifier: Intended Audience :: Developers
215
215
  Classifier: Topic :: Software Development :: Build Tools
@@ -223,12 +223,23 @@ Classifier: Programming Language :: Python :: 3.10
223
223
  Classifier: Programming Language :: Python :: 3.11
224
224
  Description-Content-Type: text/markdown
225
225
  License-File: LICENSE
226
- Requires-Dist: openai>=1.54.4
227
226
  Requires-Dist: rich>=13.8.0
227
+ Provides-Extra: test
228
+ Requires-Dist: pytest>=7.0.0; extra == "test"
229
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
230
+ Requires-Dist: pytest-mock>=3.10.0; extra == "test"
231
+ Provides-Extra: dev
232
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
233
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
234
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
235
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
236
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
237
+ Requires-Dist: black>=23.0.0; extra == "dev"
238
+ Dynamic: license-file
228
239
 
229
240
  ![TinySemVer Banner](https://github.com/ashvardanian/ashvardanian/blob/master/repositories/TinySemVer.jpg)
230
241
 
231
- __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
242
+ __TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
232
243
  In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
233
244
  Here is how to integrate it into your project CI:
234
245
 
@@ -306,19 +317,6 @@ Alternatively, you can just ask for `--help`:
306
317
  $ tinysemver --help
307
318
  ```
308
319
 
309
- ## AI and Rock-n-Roll
310
-
311
- TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
312
-
313
- ```sh
314
- $ tinysemver --verbose \
315
- --github-repository 'ashvardanian/tinysemver' \
316
- --openai-base-url 'https://api.groq.com/openai/v1' \
317
- --openai-api-key 'GET_YOURSELF_A_KEY' \
318
- --openai-model 'llama-3.2-11b-text-preview' \
319
- --dry-run
320
- ```
321
-
322
320
  ## Usage Details for the GitHub CI Action
323
321
 
324
322
  TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
@@ -337,12 +335,12 @@ jobs:
337
335
 
338
336
  steps:
339
337
  - name: Checkout
340
- uses: actions/checkout@v4
338
+ uses: actions/checkout@v5
341
339
  with:
342
340
  persist-credentials: false # Only if main branch if protected
343
341
 
344
342
  - name: Run TinySemVer
345
- uses: ashvardanian/tinysemver@v2.0.1
343
+ uses: ashvardanian/tinysemver@v3 # Automatically receives non-breaking updates
346
344
  with:
347
345
  major-verbs: 'breaking,break,major'
348
346
  minor-verbs: 'feature,minor,add,new'
@@ -363,7 +361,7 @@ jobs:
363
361
  runs-on: ubuntu-latest
364
362
 
365
363
  steps:
366
- - uses: actions/checkout@v4
364
+ - uses: actions/checkout@v5
367
365
  with:
368
366
  ref: main # Take the most recent updated version
369
367
  ```
@@ -382,6 +380,30 @@ If you need to update the version in multiple files, pass a multiline string wit
382
380
 
383
381
  For examples, consider checking StringZilla, USearch, and other libraries using TinySemVer.
384
382
 
383
+ Since version 3.0.0, TinySemVer can optionally maintain moving version tags similar to `actions/checkout@v4`:
384
+
385
+ ```yaml
386
+ uses: ashvardanian/tinysemver@v3 # Automatically gets non-breaking updates
387
+ uses: ashvardanian/tinysemver@v3.0 # Automatically gets patch updates
388
+ uses: ashvardanian/tinysemver@v3.0.0 # Pinned, never changes
389
+ uses: ashvardanian/tinysemver@main # Bleeding edge
390
+ ```
391
+
392
+ This feature is disabled by default because it requires force-push permissions.
393
+ To enable it, add `push-moving-tags: 'true'` and ensure your token has force-push access:
394
+
395
+ ```yaml
396
+ - name: Run TinySemVer
397
+ uses: ashvardanian/tinysemver@v3
398
+ with:
399
+ push: 'true'
400
+ push-moving-tags: 'true'
401
+ github-token: ${{ secrets.TINYSEMVER_TOKEN }}
402
+ ```
403
+
404
+ When enabled, TinySemVer creates three tags: `v3` (major), `v3.0` (minor), and `v3.0.0` (specific).
405
+ The default `GITHUB_TOKEN` typically lacks force-push permissions for protected branches.
406
+
385
407
  ### Security Considerations
386
408
 
387
409
  If your default branch is protected with a "pull request before merging" rule:
@@ -0,0 +1,14 @@
1
+ rich>=13.8.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0
5
+ pytest-cov>=4.0.0
6
+ pytest-mock>=3.10.0
7
+ ruff>=0.1.0
8
+ mypy>=1.0.0
9
+ black>=23.0.0
10
+
11
+ [test]
12
+ pytest>=7.0.0
13
+ pytest-cov>=4.0.0
14
+ pytest-mock>=3.10.0
File without changes
@@ -1,2 +0,0 @@
1
- openai>=1.54.4
2
- rich>=13.8.0
File without changes
File without changes