cmt-cli 0.2.0__tar.gz → 1.0.1__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 (38) hide show
  1. cmt_cli-1.0.1/.github/workflows/test.yml +30 -0
  2. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/.gitignore +3 -1
  3. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/PKG-INFO +2 -1
  4. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/README.md +1 -0
  5. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/pyproject.toml +8 -13
  6. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/ai/cache.py +2 -2
  7. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/ai/openai.py +26 -7
  8. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/ai/prompt.py +7 -1
  9. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/cli.py +57 -12
  10. cmt_cli-1.0.1/src/cmt/core/__init__.py +4 -0
  11. cmt_cli-1.0.1/src/cmt/core/logging.py +18 -0
  12. {cmt_cli-0.2.0/src/cmt/config → cmt_cli-1.0.1/src/cmt/core}/settings.py +9 -10
  13. cmt_cli-1.0.1/src/cmt/enums/config.py +6 -0
  14. cmt_cli-1.0.1/src/cmt/schemas/config.py +6 -0
  15. cmt_cli-1.0.1/src/cmt/schemas/general.py +7 -0
  16. cmt_cli-1.0.1/src/cmt/services/__init__.py +3 -0
  17. cmt_cli-1.0.1/src/cmt/services/pypi.py +34 -0
  18. cmt_cli-1.0.1/tests/__init__.py +0 -0
  19. cmt_cli-0.2.0/.github/workflows/test.yml +0 -27
  20. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/.python-version +0 -0
  21. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/LICENSE +0 -0
  22. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/art/cmt-cli.svg +0 -0
  23. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/__init__.py +0 -0
  24. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/ai/__init__.py +0 -0
  25. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/ai/provider.py +0 -0
  26. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/analysis/__init__.py +0 -0
  27. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/analysis/analyzer.py +0 -0
  28. {cmt_cli-0.2.0/src/cmt/config → cmt_cli-1.0.1/src/cmt/enums}/__init__.py +0 -0
  29. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/exceptions.py +0 -0
  30. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/git/__init__.py +0 -0
  31. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/git/repository.py +0 -0
  32. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/models/__init__.py +0 -0
  33. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/models/changes.py +0 -0
  34. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/models/suggestion.py +0 -0
  35. {cmt_cli-0.2.0/tests → cmt_cli-1.0.1/src/cmt/schemas}/__init__.py +0 -0
  36. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/src/cmt/utils.py +0 -0
  37. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/tests/unit/test_repository.py +0 -0
  38. {cmt_cli-0.2.0 → cmt_cli-1.0.1}/uv.lock +0 -0
@@ -0,0 +1,30 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'src/**'
8
+ - 'tests/**'
9
+ - 'pyproject.toml'
10
+ pull_request:
11
+ branches: [main]
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v3
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Install dependencies
27
+ run: uv sync --all-groups
28
+
29
+ - name: Run tests
30
+ run: uv run pytest
@@ -10,4 +10,6 @@ wheels/
10
10
  .venv
11
11
 
12
12
  # IDE files
13
- .idea/
13
+ .idea/
14
+
15
+ scripts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmt-cli
3
- Version: 0.2.0
3
+ Version: 1.0.1
4
4
  Summary: AI-powered Git commit message generator
5
5
  Project-URL: Homepage, https://github.com/bijaydas/cmt
6
6
  Project-URL: Documentation, https://github.com/bijaydas/cmt#readme
@@ -33,6 +33,7 @@ Description-Content-Type: text/markdown
33
33
  <a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/pyversions/cmt-cli.svg" alt="Python versions"></a>
34
34
  <a href="https://github.com/bijaydas/cmt/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
35
35
  <img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Alpha status">
36
+ <img src="https://img.shields.io/badge/tests-passing-green.svg" alt="Tests">
36
37
  </p>
37
38
 
38
39
  <p>
@@ -9,6 +9,7 @@
9
9
  <a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/pyversions/cmt-cli.svg" alt="Python versions"></a>
10
10
  <a href="https://github.com/bijaydas/cmt/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
11
11
  <img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Alpha status">
12
+ <img src="https://img.shields.io/badge/tests-passing-green.svg" alt="Tests">
12
13
  </p>
13
14
 
14
15
  <p>
@@ -1,14 +1,12 @@
1
1
  [project]
2
2
  name = "cmt-cli"
3
- version = "0.2.0"
3
+ version = "1.0.1"
4
4
  description = "AI-powered Git commit message generator"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = { text = "MIT" }
8
8
  license-files = ["LICENSE"]
9
- authors = [
10
- { name = "Bijay Das", email = "me@bijaydas.com" },
11
- ]
9
+ authors = [{ name = "Bijay Das", email = "me@bijaydas.com" }]
12
10
  dependencies = [
13
11
  "langchain>=1.3.18",
14
12
  "langchain-openai>=1.6.0",
@@ -39,10 +37,7 @@ build-backend = "hatchling.build"
39
37
  keywords = ["ai", "git", "commit", "message", "generator"]
40
38
 
41
39
  [dependency-groups]
42
- dev = [
43
- "pytest>=9.1.1",
44
- "ruff>=0.16.5",
45
- ]
40
+ dev = ["pytest>=9.1.1", "ruff>=0.16.5"]
46
41
  [tool.ruff]
47
42
  line-length = 100
48
43
  target-version = "py312"
@@ -50,11 +45,11 @@ src = ["src"]
50
45
 
51
46
  [tool.ruff.lint]
52
47
  select = [
53
- "E", # pycodestyle errors
54
- "F", # pyflakes
55
- "I", # isort (import sorting)
56
- "UP", # pyupgrade (modern syntax)
57
- "B", # flake8-bugbear (common bugs)
48
+ "E", # pycodestyle errors
49
+ "F", # pyflakes
50
+ "I", # isort (import sorting)
51
+ "UP", # pyupgrade (modern syntax)
52
+ "B", # flake8-bugbear (common bugs)
58
53
  "SIM", # flake8-simplify
59
54
  ]
60
55
  ignore = []
@@ -2,13 +2,13 @@ import hashlib
2
2
  import json
3
3
  from pathlib import Path
4
4
 
5
- from cmt.config.settings import Settings
5
+ from cmt.core.settings import settings
6
6
  from cmt.models.suggestion import CommitSuggestion
7
7
 
8
8
 
9
9
  class CommitMessageCache:
10
10
  def __init__(self):
11
- self.cache_dir = Path(Settings().CACHE_DIR)
11
+ self.cache_dir = Path(settings.CACHE_DIR)
12
12
  self.cache_dir.mkdir(parents=True, exist_ok=True)
13
13
 
14
14
  @staticmethod
@@ -1,21 +1,29 @@
1
+ import logging
2
+ from typing import cast
3
+
1
4
  from langchain.agents import create_agent
5
+ from langchain.agents.middleware.types import InputAgentState
6
+ from langchain_core.messages import BaseMessage, get_buffer_string
2
7
  from langchain_openai import ChatOpenAI
3
8
  from pydantic import SecretStr
4
9
 
5
10
  from cmt.ai.cache import CommitMessageCache
6
11
  from cmt.ai.prompt import COMMIT_PROMPT, COMMIT_SYSTEM_PROMPT
7
12
  from cmt.ai.provider import AIProvider
8
- from cmt.config.settings import Settings
13
+ from cmt.core.settings import settings
9
14
  from cmt.models.changes import AnalysisResult, StagedChangeSet, StagedFile
10
15
  from cmt.models.suggestion import CommitSuggestion
11
16
 
17
+ logger = logging.getLogger(__name__)
18
+
12
19
 
13
20
  class OpenAIProvider(AIProvider):
14
21
  def __init__(self) -> None:
15
- settings = Settings()
16
22
  self.config = settings.get()
17
23
 
18
- def _build_prompt(self, changes: StagedChangeSet, analysis: AnalysisResult) -> str:
24
+ def _build_prompt(
25
+ self, changes: StagedChangeSet, analysis: AnalysisResult
26
+ ) -> list[BaseMessage]:
19
27
  staged_files = self._process_files(changes.files)
20
28
  staged_diffs = changes.diff
21
29
 
@@ -37,12 +45,14 @@ class OpenAIProvider(AIProvider):
37
45
 
38
46
  return output.strip()
39
47
 
40
- def _invoke(self, prompt: str) -> CommitSuggestion:
48
+ def _invoke(self, prompt: list[BaseMessage]) -> CommitSuggestion:
41
49
  model = ChatOpenAI(
42
50
  model=self.config.model,
43
51
  api_key=SecretStr(self.config.api_key),
44
52
  timeout=30,
45
- max_tokens=1200,
53
+ )
54
+ logger.info(
55
+ "Invoking %s model with prompt:\n%s", self.config.model, get_buffer_string(prompt)
46
56
  )
47
57
 
48
58
  agent = create_agent(
@@ -51,7 +61,7 @@ class OpenAIProvider(AIProvider):
51
61
  response_format=CommitSuggestion,
52
62
  )
53
63
 
54
- result = agent.invoke({"messages": prompt})
64
+ result = agent.invoke(cast(InputAgentState, prompt))
55
65
 
56
66
  return result["structured_response"]
57
67
 
@@ -64,13 +74,22 @@ class OpenAIProvider(AIProvider):
64
74
  cached_message = cache.get(change_set.diff, self.config.model)
65
75
 
66
76
  if cached_message:
67
- return CommitSuggestion(
77
+ suggestion = CommitSuggestion(
68
78
  message=cached_message.message, description=cached_message.description
69
79
  )
80
+ logger.info(
81
+ "Returning cached commit suggestion for the following changes:\n\n%s\n\n"
82
+ "Commit message: %s",
83
+ change_set.diff,
84
+ suggestion.message,
85
+ )
86
+ return suggestion
70
87
 
71
88
  suggestion = self._invoke(prompt)
89
+ logger.info("Generated new commit suggestion.\n%s", suggestion)
72
90
 
73
91
  cache.set(change_set.diff, self.config.model, suggestion)
92
+ logger.info("Cached new commit suggestion.")
74
93
 
75
94
  return suggestion
76
95
 
@@ -59,7 +59,13 @@ Renamed: {renamed_files}
59
59
  Generate the commit message now.
60
60
  """
61
61
 
62
- COMMIT_PROMPT = ChatPromptTemplate.from_messages(
62
+ COMMIT_PROMPT1 = ChatPromptTemplate.from_messages(
63
+ [
64
+ ("human", COMMIT_DATA_PROMPT),
65
+ ]
66
+ )
67
+
68
+ COMMIT_PROMPT = ChatPromptTemplate(
63
69
  [
64
70
  ("human", COMMIT_DATA_PROMPT),
65
71
  ]
@@ -1,15 +1,21 @@
1
- from enum import StrEnum
1
+ import logging
2
+ import sys
2
3
  from importlib.metadata import version
3
4
 
4
5
  import typer
5
6
 
6
7
  from cmt.ai.openai import OpenAIProvider
7
8
  from cmt.analysis.analyzer import Analyzer
8
- from cmt.config.settings import Config, Settings
9
+ from cmt.core import settings, setup_logging
10
+ from cmt.enums.config import ConfigTask
9
11
  from cmt.exceptions import CmtError
10
12
  from cmt.git.repository import Repository
13
+ from cmt.schemas.config import AIConfig
14
+ from cmt.services import PyPIService
11
15
  from cmt.utils import edit_with_vim
12
16
 
17
+ logger = logging.getLogger(__name__)
18
+
13
19
  app = typer.Typer(
14
20
  add_completion=False,
15
21
  )
@@ -17,7 +23,7 @@ app = typer.Typer(
17
23
 
18
24
  def version_callback(value: bool) -> None:
19
25
  if value:
20
- typer.echo(f"cmt-cli version: {version('cmt-cli')}")
26
+ typer.echo(f"cmt-cli version: {version(settings.APP_NAME)}")
21
27
  raise typer.Exit()
22
28
 
23
29
 
@@ -26,7 +32,9 @@ def main(
26
32
  ctx: typer.Context,
27
33
  flag: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True),
28
34
  ) -> None:
35
+ setup_logging()
29
36
  if ctx.invoked_subcommand is None:
37
+ logger.info("No subcommand invoked, displaying help.")
30
38
  typer.echo(ctx.get_help())
31
39
  raise typer.Exit(code=0)
32
40
 
@@ -39,11 +47,15 @@ def suggest() -> None:
39
47
  analyzer = Analyzer()
40
48
 
41
49
  if not repository.is_git_repository():
50
+ logger.error("Not a git repository.")
42
51
  raise CmtError("Not a git repository.")
43
52
 
44
53
  staged_files = repository.get_staged_changes()
45
54
 
46
55
  if not staged_files.files:
56
+ logger.warning(
57
+ "No staged files found. Please stage your changes before running this command."
58
+ )
47
59
  typer.echo(
48
60
  "No staged files found. Please stage your changes before running this command."
49
61
  )
@@ -56,6 +68,7 @@ def suggest() -> None:
56
68
  commit = open_ai.generate_commit_message(staged_files, analysis_result)
57
69
  commit_command = OpenAIProvider.commit_command(commit)
58
70
 
71
+ logger.info("Suggested commit:\n\n%s", commit_command)
59
72
  typer.echo(f"Suggested commit:\n\n{commit_command}")
60
73
 
61
74
  while True:
@@ -74,7 +87,10 @@ def suggest() -> None:
74
87
  typer.echo(f"Edited commit:\n\n{commit_command}")
75
88
 
76
89
  if action == "y":
90
+ logger.info("Committing with message:\n\n%s", commit_command)
77
91
  result = repository.commit(commit_command)
92
+
93
+ logger.info("Commit result:\n\n%s", result.stdout)
78
94
  typer.echo(f"Commit result:\n\n{result.stdout}")
79
95
  break
80
96
 
@@ -84,32 +100,27 @@ def suggest() -> None:
84
100
  except typer.Exit:
85
101
  raise
86
102
  except KeyboardInterrupt:
103
+ logger.info("Operation cancelled by user.")
87
104
  typer.echo("\nOperation cancelled by user.", err=True)
88
105
  raise typer.Exit(code=1) from None
89
106
  except Exception as e:
107
+ logger.error("Unexpected error: %s", e)
90
108
  typer.echo(f"Unexpected error: {e}", err=True)
91
109
  raise typer.Exit(code=1) from None
92
110
 
93
111
 
94
- class ConfigTask(StrEnum):
95
- set = "set"
96
- get = "get"
97
-
98
-
99
112
  @app.command()
100
113
  def config(task: ConfigTask) -> None:
101
114
  """Configure cmt-cli configuration for OpenAI API key and model."""
102
115
  if task == ConfigTask.set:
103
- settings = Settings()
104
-
105
116
  open_ai_key = typer.prompt("Enter your OpenAI API key", default=None)
106
117
  open_ai_model = typer.prompt(
107
118
  "Enter your OpenAI model", default=settings.OPEN_AI_DEFAULT_MODEL
108
119
  )
109
- settings.set(Config(api_key=open_ai_key, model=open_ai_model))
120
+ logger.info("Configuration updated with OpenAI API key and model.")
121
+ settings.set(AIConfig(api_key=open_ai_key, model=open_ai_model))
110
122
 
111
123
  if task == ConfigTask.get:
112
- settings = Settings()
113
124
  setting_values = settings.get()
114
125
 
115
126
  if setting_values.api_key:
@@ -118,8 +129,42 @@ def config(task: ConfigTask) -> None:
118
129
  if setting_values.model:
119
130
  typer.echo(f"OpenAI model: {setting_values.model}")
120
131
 
132
+ logger.info("Retrieved current configuration.")
133
+
121
134
  raise typer.Exit(code=0)
122
135
 
123
136
 
137
+ @app.command()
138
+ def update() -> None:
139
+ """Update cmt-cli to the latest version."""
140
+ logger.info("Starting update process for %s.", settings.APP_NAME)
141
+
142
+ current_version = version(settings.APP_NAME)
143
+ logger.info("Current %s version: %s", settings.APP_NAME, current_version)
144
+
145
+ pypi_service = PyPIService()
146
+ latest_info_response = pypi_service.any_update(current_version=current_version)
147
+
148
+ if latest_info_response.updated_required:
149
+ logger.info(
150
+ "Update required: current version %s, latest version %s",
151
+ latest_info_response.current_version,
152
+ latest_info_response.latest_version,
153
+ )
154
+ typer.echo(
155
+ f"A new version of {settings.APP_NAME} is available: "
156
+ f"{latest_info_response.latest_version}.\n\n"
157
+ f"upgrade {settings.APP_NAME} using your package manager.",
158
+ )
159
+ else:
160
+ logger.info(
161
+ "No update required. You are using the latest version: %s",
162
+ latest_info_response.current_version,
163
+ )
164
+ typer.echo(f"You are using the latest version of {settings.APP_NAME}")
165
+
166
+ raise typer.Exit(code=0)
167
+
168
+
124
169
  if __name__ == "__main__":
125
170
  app()
@@ -0,0 +1,4 @@
1
+ from cmt.core.logging import setup_logging
2
+ from cmt.core.settings import settings
3
+
4
+ __all__ = ["settings", "setup_logging"]
@@ -0,0 +1,18 @@
1
+ import logging
2
+ from logging.handlers import RotatingFileHandler
3
+
4
+ from cmt.core.settings import settings
5
+
6
+
7
+ def setup_logging():
8
+ settings.LOGS_DIR.mkdir(parents=True, exist_ok=True)
9
+
10
+ handler = RotatingFileHandler(
11
+ settings.LOGS_DIR / f"{settings.APP_NAME}.log", maxBytes=1024 * 1024, backupCount=9
12
+ )
13
+
14
+ logging.basicConfig(
15
+ level=logging.DEBUG,
16
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
17
+ handlers=[handler],
18
+ )
@@ -1,24 +1,20 @@
1
1
  from configparser import ConfigParser
2
2
  from pathlib import Path
3
3
 
4
- from pydantic import BaseModel
5
-
6
4
  from cmt.exceptions import ConfigurationError
7
-
8
-
9
- class Config(BaseModel):
10
- api_key: str
11
- model: str
5
+ from cmt.schemas.config import AIConfig
12
6
 
13
7
 
14
8
  class Settings:
9
+ APP_NAME: str = "cmt-cli"
15
10
  CONFIG_DIR: Path = Path.home() / ".config" / "cmt"
16
11
  CACHE_DIR: Path = CONFIG_DIR / "cache"
12
+ LOGS_DIR: Path = CONFIG_DIR / "logs"
17
13
 
18
14
  CONFIG_FILE: Path = CONFIG_DIR / "config.ini"
19
15
  OPEN_AI_DEFAULT_MODEL: str = "gpt-4o-mini"
20
16
 
21
- def set(self, openai_config: Config):
17
+ def set(self, openai_config: AIConfig):
22
18
  self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
23
19
 
24
20
  config = ConfigParser()
@@ -35,7 +31,7 @@ class Settings:
35
31
  with open(self.CONFIG_FILE, "w") as f:
36
32
  config.write(f)
37
33
 
38
- def get(self) -> Config:
34
+ def get(self) -> AIConfig:
39
35
  if not self.CONFIG_FILE.exists():
40
36
  raise FileNotFoundError(
41
37
  "Configuration file not found. Run `cmt config set` to set it up."
@@ -57,4 +53,7 @@ class Settings:
57
53
  "Model not found in the configuration. Run `cmt config set` to set it up."
58
54
  )
59
55
 
60
- return Config(api_key=api_key, model=model)
56
+ return AIConfig(api_key=api_key, model=model)
57
+
58
+
59
+ settings = Settings()
@@ -0,0 +1,6 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class ConfigTask(StrEnum):
5
+ set = "set"
6
+ get = "get"
@@ -0,0 +1,6 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class AIConfig(BaseModel):
5
+ api_key: str
6
+ model: str
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class VersionInfo(BaseModel):
5
+ current_version: str
6
+ latest_version: str
7
+ updated_required: bool
@@ -0,0 +1,3 @@
1
+ from cmt.services.pypi import PyPIService
2
+
3
+ __all__ = ["PyPIService"]
@@ -0,0 +1,34 @@
1
+ import logging
2
+
3
+ import httpx
4
+
5
+ from cmt.core import settings
6
+ from cmt.schemas.general import VersionInfo
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class PyPIService:
12
+ def __init__(self):
13
+ self._base_url = "https://pypi.org/pypi"
14
+
15
+ def _get_package_info(self) -> dict:
16
+ url = f"{self._base_url}/{settings.APP_NAME}/json"
17
+ response = httpx.get(url)
18
+
19
+ logger.debug("Received response with status code: %s", response.status_code)
20
+ response.raise_for_status()
21
+
22
+ return response.json()
23
+
24
+ def any_update(self, current_version: str) -> VersionInfo:
25
+ latest_info = self._get_package_info()
26
+ latest_version = latest_info["info"]["version"]
27
+
28
+ logger.debug("Current version: %s, Latest version: %s", current_version, latest_version)
29
+
30
+ return VersionInfo(
31
+ current_version=current_version,
32
+ latest_version=latest_version,
33
+ updated_required=latest_version != current_version,
34
+ )
File without changes
@@ -1,27 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout code
15
- uses: actions/checkout@v4
16
-
17
- - name: Install uv
18
- uses: astral-sh/setup-uv@v3
19
- with:
20
- python-version: "3.12"
21
- enable-cache: true
22
-
23
- - name: Install dependencies
24
- run: uv sync --all-groups
25
-
26
- - name: Run tests
27
- run: uv run pytest
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes