nuvu-scan 2.0.0__tar.gz → 2.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 (48) hide show
  1. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/.github/workflows/ci.yml +21 -12
  2. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/PKG-INFO +7 -30
  3. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/README.md +5 -29
  4. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/commands/scan.py +67 -43
  5. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/aws_scanner.py +9 -45
  6. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/gcp_scanner.py +3 -0
  7. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/pyproject.toml +3 -1
  8. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  9. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/.github/workflows/release.yml +0 -0
  10. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/.gitignore +0 -0
  11. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/.pre-commit-config.yaml +0 -0
  12. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/CONTRIBUTING.md +0 -0
  13. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/DEVELOPMENT_STATUS.md +0 -0
  14. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/Makefile +0 -0
  15. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/RELEASE.md +0 -0
  16. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/__init__.py +0 -0
  17. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/__init__.py +0 -0
  18. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/commands/__init__.py +0 -0
  19. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/formatters/__init__.py +0 -0
  20. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/formatters/csv.py +0 -0
  21. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/formatters/html.py +0 -0
  22. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/formatters/json.py +0 -0
  23. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/cli/main.py +0 -0
  24. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/__init__.py +0 -0
  25. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/analyzers/__init__.py +0 -0
  26. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/base.py +0 -0
  27. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/models/__init__.py +0 -0
  28. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/__init__.py +0 -0
  29. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/__init__.py +0 -0
  30. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/__init__.py +0 -0
  31. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/athena.py +0 -0
  32. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/cost_explorer.py +0 -0
  33. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/glue.py +0 -0
  34. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/iam.py +0 -0
  35. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/mwaa.py +0 -0
  36. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/redshift.py +0 -0
  37. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/aws/collectors/s3.py +0 -0
  38. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/__init__.py +0 -0
  39. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/__init__.py +0 -0
  40. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/bigquery.py +0 -0
  41. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/billing.py +0 -0
  42. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/dataproc.py +0 -0
  43. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/gcs.py +0 -0
  44. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/gemini.py +0 -0
  45. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/iam.py +0 -0
  46. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/nuvu_scan/core/providers/gcp/collectors/pubsub.py +0 -0
  47. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/tests/__init__.py +0 -0
  48. {nuvu_scan-2.0.0 → nuvu_scan-2.0.1}/tests/test_base.py +0 -0
@@ -9,6 +9,8 @@ on:
9
9
  jobs:
10
10
  lint:
11
11
  runs-on: ubuntu-latest
12
+ # Only run lint on PRs - already passed before merge
13
+ if: github.event_name == 'pull_request'
12
14
  steps:
13
15
  - uses: actions/checkout@v4
14
16
 
@@ -28,37 +30,44 @@ jobs:
28
30
 
29
31
  test:
30
32
  runs-on: ubuntu-latest
33
+ # On PRs: wait for lint. On push to main: run directly
31
34
  needs: lint
35
+ if: always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped')
32
36
  strategy:
33
37
  matrix:
34
38
  python-version: ["3.10", "3.11", "3.12", "3.13"]
35
-
39
+
36
40
  steps:
37
41
  - uses: actions/checkout@v4
38
-
42
+
39
43
  - name: Install uv
40
44
  uses: astral-sh/setup-uv@v4
41
45
  with:
42
46
  version: "latest"
43
-
47
+
44
48
  - name: Set up Python ${{ matrix.python-version }}
45
49
  run: uv python install ${{ matrix.python-version }}
46
-
50
+
47
51
  - name: Install dependencies
48
52
  run: |
49
53
  uv sync --dev
50
-
54
+
55
+ - name: Run linter
56
+ run: |
57
+ uv run ruff check .
58
+ uv run black --check .
59
+
51
60
  - name: Run type checker
52
61
  run: |
53
62
  uv run mypy nuvu_scan || true # Allow failures for now
54
-
63
+
55
64
  - name: Run tests
56
65
  run: |
57
66
  uv run pytest --cov=nuvu_scan --cov-report=xml
58
67
  env:
59
68
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
60
69
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
61
-
70
+
62
71
  - name: Upload coverage
63
72
  uses: codecov/codecov-action@v3
64
73
  with:
@@ -69,22 +78,22 @@ jobs:
69
78
  runs-on: ubuntu-latest
70
79
  needs: test
71
80
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
72
-
81
+
73
82
  steps:
74
83
  - uses: actions/checkout@v4
75
-
84
+
76
85
  - name: Install uv
77
86
  uses: astral-sh/setup-uv@v4
78
87
  with:
79
88
  version: "latest"
80
-
89
+
81
90
  - name: Set up Python
82
91
  run: uv python install 3.11
83
-
92
+
84
93
  - name: Build package
85
94
  run: |
86
95
  uv build
87
-
96
+
88
97
  - name: Upload artifacts
89
98
  uses: actions/upload-artifact@v4
90
99
  with:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nuvu-scan
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Multi-Cloud Data Asset Control - Discover, govern, and optimize your cloud data assets across AWS and GCP
5
5
  Project-URL: Homepage, https://nuvu.dev
6
6
  Project-URL: Documentation, https://github.com/nuvudev/nuvu-scan#readme
@@ -35,6 +35,7 @@ Requires-Dist: google-cloud-dataproc>=5.4.0
35
35
  Requires-Dist: google-cloud-monitoring>=2.16.0
36
36
  Requires-Dist: google-cloud-pubsub>=2.18.0
37
37
  Requires-Dist: google-cloud-storage>=2.10.0
38
+ Requires-Dist: httpx>=0.25.0
38
39
  Provides-Extra: dev
39
40
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
40
41
  Requires-Dist: boto3-stubs[essential]>=1.28.0; extra == 'dev'
@@ -195,40 +196,16 @@ nuvu scan --provider gcp --gcp-credentials /path/to/service-account-key.json --g
195
196
  nuvu scan --provider gcp --gcp-project your-project-id --output-format json --output-file gcp-report.json
196
197
  ```
197
198
 
198
- ### Selective Scanning (Collectors)
199
+ ### Push to Remote API (Optional)
199
200
 
200
- Run focused scans on specific services instead of a full scan:
201
+ You can optionally push scan results to a remote API for centralized tracking:
201
202
 
202
203
  ```bash
203
- # List available collectors for a provider
204
- nuvu scan --provider aws --list-collectors
205
- # Output: athena, glue, iam, mwaa, redshift, s3
206
-
207
- nuvu scan --provider gcp --list-collectors
208
- # Output: bigquery, dataproc, gcs, gemini, iam, pubsub
209
-
210
- # Scan only Redshift
211
- nuvu scan --provider aws -c redshift --region us-west-2
212
-
213
- # Scan multiple specific collectors
214
- nuvu scan --provider aws -c redshift -c glue --region us-west-2
215
-
216
- # Scan only S3 buckets
217
- nuvu scan --provider aws -c s3 --output-format html
218
-
219
- # Full scan (default - all collectors)
220
- nuvu scan --provider aws # Runs all collectors
221
-
222
- # GCP: Scan only BigQuery
223
- nuvu scan --provider gcp -c bigquery --gcp-project your-project
204
+ # Push results to a remote endpoint
205
+ nuvu scan --provider aws --push --api-key your-api-key --api-url https://your-api.example.com
224
206
  ```
225
207
 
226
- **Benefits of selective scanning:**
227
- - **Faster scans** - Focus on services you care about
228
- - **Reduced API calls** - Only query the services you need
229
- - **Targeted reports** - Generate reports for specific areas
230
-
231
- ---
208
+ This is useful for integrating with your own data governance platforms or CI/CD pipelines.
232
209
 
233
210
  ## Features
234
211
 
@@ -148,40 +148,16 @@ nuvu scan --provider gcp --gcp-credentials /path/to/service-account-key.json --g
148
148
  nuvu scan --provider gcp --gcp-project your-project-id --output-format json --output-file gcp-report.json
149
149
  ```
150
150
 
151
- ### Selective Scanning (Collectors)
151
+ ### Push to Remote API (Optional)
152
152
 
153
- Run focused scans on specific services instead of a full scan:
153
+ You can optionally push scan results to a remote API for centralized tracking:
154
154
 
155
155
  ```bash
156
- # List available collectors for a provider
157
- nuvu scan --provider aws --list-collectors
158
- # Output: athena, glue, iam, mwaa, redshift, s3
159
-
160
- nuvu scan --provider gcp --list-collectors
161
- # Output: bigquery, dataproc, gcs, gemini, iam, pubsub
162
-
163
- # Scan only Redshift
164
- nuvu scan --provider aws -c redshift --region us-west-2
165
-
166
- # Scan multiple specific collectors
167
- nuvu scan --provider aws -c redshift -c glue --region us-west-2
168
-
169
- # Scan only S3 buckets
170
- nuvu scan --provider aws -c s3 --output-format html
171
-
172
- # Full scan (default - all collectors)
173
- nuvu scan --provider aws # Runs all collectors
174
-
175
- # GCP: Scan only BigQuery
176
- nuvu scan --provider gcp -c bigquery --gcp-project your-project
156
+ # Push results to a remote endpoint
157
+ nuvu scan --provider aws --push --api-key your-api-key --api-url https://your-api.example.com
177
158
  ```
178
159
 
179
- **Benefits of selective scanning:**
180
- - **Faster scans** - Focus on services you care about
181
- - **Reduced API calls** - Only query the services you need
182
- - **Targeted reports** - Generate reports for specific areas
183
-
184
- ---
160
+ This is useful for integrating with your own data governance platforms or CI/CD pipelines.
185
161
 
186
162
  ## Features
187
163
 
@@ -98,24 +98,18 @@ from ..formatters.json import JSONFormatter
98
98
  @click.option(
99
99
  "--push",
100
100
  is_flag=True,
101
- help="Push scan results to Nuvu Cloud (requires API key)",
102
- )
103
- @click.option(
104
- "--nuvu-cloud-url",
105
- envvar="NUVU_CLOUD_URL",
106
- default="https://nuvu.dev",
107
- show_default=True,
108
- help="Nuvu Cloud base URL",
101
+ help="Push scan results to Nuvu Cloud (requires --api-key)",
109
102
  )
110
103
  @click.option(
111
104
  "--api-key",
112
105
  envvar="NUVU_API_KEY",
113
- help="Nuvu Cloud API key (from dashboard account settings)",
106
+ help="Nuvu Cloud API key for pushing results (default: from NUVU_API_KEY env var)",
114
107
  )
115
108
  @click.option(
116
- "--list-collectors",
117
- is_flag=True,
118
- help="List available collectors for the specified provider and exit",
109
+ "--api-url",
110
+ envvar="NUVU_API_URL",
111
+ default="https://nuvu.dev",
112
+ help="Nuvu Cloud API URL (default: https://nuvu.dev)",
119
113
  )
120
114
  def scan_command(
121
115
  provider: str,
@@ -134,8 +128,8 @@ def scan_command(
134
128
  gcp_credentials: str | None,
135
129
  gcp_project: str | None,
136
130
  push: bool,
137
- nuvu_cloud_url: str | None,
138
131
  api_key: str | None,
132
+ api_url: str,
139
133
  list_collectors: bool,
140
134
  ):
141
135
  """Scan cloud provider for data assets."""
@@ -311,41 +305,71 @@ def scan_command(
311
305
  f.write(content)
312
306
  click.echo(f"Report written to {output_file}", err=True)
313
307
 
308
+ # Push to Nuvu Cloud if requested
314
309
  if push:
315
- if not nuvu_cloud_url:
316
- click.echo("Error: --nuvu-cloud-url or NUVU_CLOUD_URL is required for --push", err=True)
317
- sys.exit(1)
318
310
  if not api_key:
319
- click.echo("Error: --api-key or NUVU_API_KEY is required for --push", err=True)
311
+ click.echo(
312
+ "Error: --api-key required for pushing results (or set NUVU_API_KEY env var)",
313
+ err=True,
314
+ )
320
315
  sys.exit(1)
321
316
 
322
- payload = json.loads(JSONFormatter().format(result))
323
- payload["scan_regions"] = list(region) if region else None
324
- payload["scan_all_regions"] = False if region else True
317
+ click.echo(f"Pushing results to Nuvu Cloud ({api_url})...", err=True)
318
+ try:
319
+ import httpx
325
320
 
326
- import_url = nuvu_cloud_url.rstrip("/") + "/api/scans/import"
327
- request = Request(
328
- import_url,
329
- data=json.dumps(payload).encode("utf-8"),
330
- headers={
331
- "Content-Type": "application/json",
332
- "Authorization": f"Bearer {api_key}",
333
- },
334
- method="POST",
335
- )
321
+ # Prepare payload matching ScanImport schema
322
+ # Extract unique regions from assets
323
+ scan_regions = list(set(asset.region for asset in result.assets if asset.region))
336
324
 
337
- try:
338
- with urlopen(request) as response:
339
- response_body = response.read().decode("utf-8")
340
- click.echo(f"Scan uploaded to Nuvu Cloud: {response.status}", err=True)
341
- if response_body:
342
- click.echo(response_body, err=True)
343
- except HTTPError as e:
344
- error_body = e.read().decode("utf-8")
345
- click.echo(f"Failed to upload scan: {e.code} {e.reason}", err=True)
346
- if error_body:
347
- click.echo(error_body, err=True)
325
+ payload = {
326
+ "provider": provider,
327
+ "account_id": result.account_id or "unknown",
328
+ "scan_timestamp": result.scan_timestamp or datetime.utcnow().isoformat(),
329
+ "total_cost_estimate_usd": result.total_cost_estimate_usd,
330
+ "scan_regions": scan_regions if scan_regions else None,
331
+ "scan_all_regions": not bool(region),
332
+ "assets": [
333
+ {
334
+ "provider": asset.provider,
335
+ "asset_type": asset.asset_type,
336
+ "normalized_category": asset.normalized_category.value if asset.normalized_category else "unknown",
337
+ "service": asset.service or asset.asset_type.split("_")[0] if asset.asset_type else "unknown",
338
+ "region": asset.region,
339
+ "arn": asset.arn,
340
+ "name": asset.name,
341
+ "created_at": asset.created_at,
342
+ "last_activity_at": asset.last_activity_at,
343
+ "size_bytes": asset.size_bytes,
344
+ "tags": asset.tags,
345
+ "cost_estimate_usd": asset.cost_estimate_usd,
346
+ "risk_flags": asset.risk_flags,
347
+ "ownership_confidence": asset.ownership_confidence or "unknown",
348
+ "suggested_owner": asset.suggested_owner,
349
+ }
350
+ for asset in result.assets
351
+ ],
352
+ }
353
+
354
+ # Push to API using the /api/scans/import endpoint
355
+ with httpx.Client(timeout=60) as client:
356
+ response = client.post(
357
+ f"{api_url.rstrip('/')}/api/scans/import",
358
+ json=payload,
359
+ headers={
360
+ "Authorization": f"Bearer {api_key}",
361
+ "Content-Type": "application/json",
362
+ },
363
+ )
364
+ response.raise_for_status()
365
+ result_data = response.json()
366
+ click.echo(
367
+ f"✓ Pushed {len(result.assets)} assets to Nuvu Cloud (scan_id: {result_data.get('id', 'N/A')})",
368
+ err=True,
369
+ )
370
+ except httpx.HTTPStatusError as e:
371
+ click.echo(f"Error pushing to Nuvu Cloud: {e.response.status_code} - {e.response.text}", err=True)
348
372
  sys.exit(1)
349
- except URLError as e:
350
- click.echo(f"Failed to upload scan: {e.reason}", err=True)
373
+ except Exception as e:
374
+ click.echo(f"Error pushing to Nuvu Cloud: {e}", err=True)
351
375
  sys.exit(1)
@@ -33,55 +33,19 @@ class AWSScanner(CloudProviderScan):
33
33
  def __init__(self, config: ScanConfig):
34
34
  super().__init__(config)
35
35
  self.session = self._create_session()
36
- if not self.config.regions:
37
- self.config.regions = self._resolve_regions()
38
- self.collectors = self._initialize_collectors()
39
- self.cost_explorer = CostExplorerCollector(self.session, self.config.regions)
40
-
41
36
  # Auto-detect account ID if not provided
42
37
  if not self.config.account_id:
43
38
  self.config.account_id = self._get_account_id()
39
+ self.collectors = self._initialize_collectors()
40
+ self.cost_explorer = CostExplorerCollector(self.session, self.config.regions)
44
41
 
45
- def scan(self):
46
- """Execute a full scan with AWS-specific cost handling."""
47
- from datetime import datetime
48
-
49
- # Discover assets
50
- assets = self.list_assets()
51
-
52
- # Analyze each asset
53
- total_estimated_cost = 0.0
54
- actual_total_cost = None
55
- actual_service_costs = None
56
-
57
- for asset in assets:
58
- if asset.asset_type == "cost_summary":
59
- actual_total_cost = asset.usage_metrics.get("total_actual_cost_30d")
60
- actual_service_costs = asset.usage_metrics.get("actual_costs_30d")
61
- continue
62
-
63
- asset.usage_metrics = self.get_usage_metrics(asset)
64
- asset.cost_estimate_usd = self.get_cost_estimate(asset)
65
- total_estimated_cost += asset.cost_estimate_usd or 0.0
66
-
67
- # Build summary
68
- summary = self._build_summary(assets)
69
- if actual_total_cost is not None:
70
- summary["total_actual_cost_30d"] = actual_total_cost
71
- summary["actual_costs_30d"] = actual_service_costs or {}
72
- summary["estimated_assets_cost_total"] = total_estimated_cost
73
-
74
- # Use actual 30-day cost if available, otherwise fallback to estimates
75
- total_cost = actual_total_cost if actual_total_cost is not None else total_estimated_cost
76
-
77
- return ScanResult(
78
- provider=self.provider,
79
- account_id=self.config.account_id or "unknown",
80
- scan_timestamp=datetime.utcnow().isoformat(),
81
- assets=assets,
82
- total_cost_estimate_usd=total_cost,
83
- summary=summary,
84
- )
42
+ def _get_account_id(self) -> str:
43
+ """Get the AWS account ID using STS."""
44
+ try:
45
+ sts_client = self.session.client("sts")
46
+ return sts_client.get_caller_identity()["Account"]
47
+ except Exception:
48
+ return "unknown"
85
49
 
86
50
  def _create_session(self) -> boto3.Session:
87
51
  """
@@ -27,6 +27,9 @@ class GCPScanner(CloudProviderScan):
27
27
  super().__init__(config)
28
28
  self.credentials = self._create_credentials()
29
29
  self.project_id = self._get_project_id()
30
+ # Set account_id to project_id for consistency in ScanResult
31
+ if not self.config.account_id:
32
+ self.config.account_id = self.project_id
30
33
  self.collectors = self._initialize_collectors()
31
34
 
32
35
  def _create_credentials(self):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nuvu-scan"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  description = "Multi-Cloud Data Asset Control - Discover, govern, and optimize your cloud data assets across AWS and GCP"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -43,6 +43,7 @@ dependencies = [
43
43
  "boto3>=1.28.0",
44
44
  "botocore>=1.31.0",
45
45
  "click>=8.0.0",
46
+ "httpx>=0.25.0",
46
47
  "google-cloud-storage>=2.10.0",
47
48
  "google-cloud-bigquery>=3.11.0",
48
49
  "google-cloud-dataproc>=5.4.0",
@@ -70,6 +71,7 @@ dev = [
70
71
  "pytest-cov>=4.0.0",
71
72
  "ruff>=0.8.0",
72
73
  "mypy>=1.0.0",
74
+ "pre-commit>=4.5.1",
73
75
  "boto3-stubs[essential]>=1.28.0",
74
76
  "pre-commit>=3.5.0",
75
77
  "bandit>=1.7.0",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes