stackport 0.3.8__tar.gz → 0.3.9__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 (110) hide show
  1. {stackport-0.3.8/stackport.egg-info → stackport-0.3.9}/PKG-INFO +1 -1
  2. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/logs.py +94 -1
  3. stackport-0.3.9/backend/schemas/logs.py +21 -0
  4. {stackport-0.3.8 → stackport-0.3.9}/pyproject.toml +1 -1
  5. {stackport-0.3.8 → stackport-0.3.9/stackport.egg-info}/PKG-INFO +1 -1
  6. {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/SOURCES.txt +5 -4
  7. {stackport-0.3.8 → stackport-0.3.9}/tests/test_logs_routes.py +159 -0
  8. stackport-0.3.8/ui/dist/assets/StateMachineGraph-CmFIj15P.js → stackport-0.3.9/ui/dist/assets/StateMachineGraph-BGBIYpUV.js +1 -1
  9. stackport-0.3.9/ui/dist/assets/index-iwgw2OEV.js +652 -0
  10. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/index.html +1 -1
  11. stackport-0.3.8/ui/dist/assets/index-Dvr-Ft0R.js +0 -652
  12. {stackport-0.3.8 → stackport-0.3.9}/LICENSE +0 -0
  13. {stackport-0.3.8 → stackport-0.3.9}/MANIFEST.in +0 -0
  14. {stackport-0.3.8 → stackport-0.3.9}/README.md +0 -0
  15. {stackport-0.3.8 → stackport-0.3.9}/backend/__init__.py +0 -0
  16. {stackport-0.3.8 → stackport-0.3.9}/backend/aws_client.py +0 -0
  17. {stackport-0.3.8 → stackport-0.3.9}/backend/cache.py +0 -0
  18. {stackport-0.3.8 → stackport-0.3.9}/backend/cli.py +0 -0
  19. {stackport-0.3.8 → stackport-0.3.9}/backend/config.py +0 -0
  20. {stackport-0.3.8 → stackport-0.3.9}/backend/endpoint_store.py +0 -0
  21. {stackport-0.3.8 → stackport-0.3.9}/backend/main.py +0 -0
  22. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/__init__.py +0 -0
  23. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/common.py +0 -0
  24. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/dynamodb.py +0 -0
  25. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/ec2.py +0 -0
  26. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/endpoints.py +0 -0
  27. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/iam.py +0 -0
  28. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/lambda_svc.py +0 -0
  29. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/rds.py +0 -0
  30. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/resources.py +0 -0
  31. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/s3.py +0 -0
  32. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/secretsmanager.py +0 -0
  33. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/sqs.py +0 -0
  34. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/stats.py +0 -0
  35. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/stepfunctions.py +0 -0
  36. {stackport-0.3.8 → stackport-0.3.9}/backend/routes/tags.py +0 -0
  37. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/__init__.py +0 -0
  38. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/dynamodb.py +0 -0
  39. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/endpoints.py +0 -0
  40. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/lambda_svc.py +0 -0
  41. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/s3.py +0 -0
  42. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/secretsmanager.py +0 -0
  43. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/sqs.py +0 -0
  44. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/stepfunctions.py +0 -0
  45. {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/tags.py +0 -0
  46. {stackport-0.3.8 → stackport-0.3.9}/backend/websocket.py +0 -0
  47. {stackport-0.3.8 → stackport-0.3.9}/setup.cfg +0 -0
  48. {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/dependency_links.txt +0 -0
  49. {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/entry_points.txt +0 -0
  50. {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/requires.txt +0 -0
  51. {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/top_level.txt +0 -0
  52. {stackport-0.3.8 → stackport-0.3.9}/tests/test_cache.py +0 -0
  53. {stackport-0.3.8 → stackport-0.3.9}/tests/test_cli.py +0 -0
  54. {stackport-0.3.8 → stackport-0.3.9}/tests/test_client.py +0 -0
  55. {stackport-0.3.8 → stackport-0.3.9}/tests/test_config.py +0 -0
  56. {stackport-0.3.8 → stackport-0.3.9}/tests/test_dynamodb_routes.py +0 -0
  57. {stackport-0.3.8 → stackport-0.3.9}/tests/test_ec2_routes.py +0 -0
  58. {stackport-0.3.8 → stackport-0.3.9}/tests/test_endpoint_store.py +0 -0
  59. {stackport-0.3.8 → stackport-0.3.9}/tests/test_endpoints.py +0 -0
  60. {stackport-0.3.8 → stackport-0.3.9}/tests/test_iam_routes.py +0 -0
  61. {stackport-0.3.8 → stackport-0.3.9}/tests/test_lambda_routes.py +0 -0
  62. {stackport-0.3.8 → stackport-0.3.9}/tests/test_multi_endpoint.py +0 -0
  63. {stackport-0.3.8 → stackport-0.3.9}/tests/test_rds_routes.py +0 -0
  64. {stackport-0.3.8 → stackport-0.3.9}/tests/test_readonly_middleware.py +0 -0
  65. {stackport-0.3.8 → stackport-0.3.9}/tests/test_registries.py +0 -0
  66. {stackport-0.3.8 → stackport-0.3.9}/tests/test_routes.py +0 -0
  67. {stackport-0.3.8 → stackport-0.3.9}/tests/test_s3_routes.py +0 -0
  68. {stackport-0.3.8 → stackport-0.3.9}/tests/test_s3_upload_limit_env.py +0 -0
  69. {stackport-0.3.8 → stackport-0.3.9}/tests/test_secretsmanager_routes.py +0 -0
  70. {stackport-0.3.8 → stackport-0.3.9}/tests/test_sqs_routes.py +0 -0
  71. {stackport-0.3.8 → stackport-0.3.9}/tests/test_tags_routes.py +0 -0
  72. {stackport-0.3.8 → stackport-0.3.9}/tests/test_websocket.py +0 -0
  73. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/assets/index-BZr5kWvs.css +0 -0
  74. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  75. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/acm.svg +0 -0
  76. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/apigateway.svg +0 -0
  77. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/appsync.svg +0 -0
  78. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/athena.svg +0 -0
  79. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cloudformation.svg +0 -0
  80. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cloudfront.svg +0 -0
  81. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  82. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/dynamodb.svg +0 -0
  83. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ec2.svg +0 -0
  84. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ecr.svg +0 -0
  85. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ecs.svg +0 -0
  86. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticache.svg +0 -0
  87. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  88. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  89. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  90. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/events.svg +0 -0
  91. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/firehose.svg +0 -0
  92. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/glue.svg +0 -0
  93. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/iam.svg +0 -0
  94. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/kinesis.svg +0 -0
  95. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/kms.svg +0 -0
  96. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/lambda.svg +0 -0
  97. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/logs.svg +0 -0
  98. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/monitoring.svg +0 -0
  99. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/rds.svg +0 -0
  100. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/route53.svg +0 -0
  101. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/s3.svg +0 -0
  102. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  103. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ses.svg +0 -0
  104. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/sns.svg +0 -0
  105. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/sqs.svg +0 -0
  106. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ssm.svg +0 -0
  107. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  108. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/wafv2.svg +0 -0
  109. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/favicon.png +0 -0
  110. {stackport-0.3.8 → stackport-0.3.9}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -2,16 +2,19 @@ import logging
2
2
  from datetime import datetime
3
3
  from typing import Optional
4
4
 
5
- from fastapi import APIRouter, Depends, Query
5
+ from fastapi import APIRouter, Depends, HTTPException, Query
6
6
 
7
7
  from backend.aws_client import get_client
8
8
  from backend.cache import cache
9
9
  from backend.routes.common import EndpointInfo, get_endpoint_info
10
+ from backend.schemas.logs import CreateLogGroupBody, SetRetentionBody
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
13
14
  router = APIRouter()
14
15
 
16
+ VALID_RETENTION_DAYS = {1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653}
17
+
15
18
 
16
19
  def _epoch_millis_to_iso(timestamp: Optional[int]) -> Optional[str]:
17
20
  """Convert epoch milliseconds to ISO string."""
@@ -218,3 +221,93 @@ def get_log_events(
218
221
  "events": events,
219
222
  "next_token": response.get("nextForwardToken"),
220
223
  }
224
+
225
+
226
+ @router.post("/groups", status_code=201)
227
+ def create_log_group(body: CreateLogGroupBody, ep: EndpointInfo = Depends(get_endpoint_info)):
228
+ """Create a new CloudWatch log group."""
229
+ if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
230
+ raise HTTPException(
231
+ status_code=400,
232
+ detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
233
+ )
234
+
235
+ logs = get_client("logs", **ep.client_kwargs())
236
+ try:
237
+ kwargs: dict = {"logGroupName": body.name}
238
+ if body.tags:
239
+ kwargs["tags"] = body.tags
240
+ logs.create_log_group(**kwargs)
241
+
242
+ if body.retention_in_days is not None:
243
+ logs.put_retention_policy(
244
+ logGroupName=body.name, retentionInDays=body.retention_in_days
245
+ )
246
+ except logs.exceptions.ResourceAlreadyExistsException:
247
+ raise HTTPException(status_code=409, detail=f"Log group '{body.name}' already exists")
248
+ except Exception as e:
249
+ raise HTTPException(status_code=500, detail=str(e))
250
+
251
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
252
+ return {"name": body.name, "retention_in_days": body.retention_in_days}
253
+
254
+
255
+ @router.delete("/groups/{name:path}/streams/{stream:path}")
256
+ def delete_log_stream(name: str, stream: str, ep: EndpointInfo = Depends(get_endpoint_info)):
257
+ """Delete a log stream from a log group.
258
+
259
+ Registered before delete_log_group: {name:path} matches slashes greedily,
260
+ so if the bare /groups/{name:path} DELETE route were checked first it would
261
+ swallow this whole path (including "/streams/{stream}") as the group name
262
+ and never reach this handler. The more specific route has to come first.
263
+ """
264
+ logs = get_client("logs", **ep.client_kwargs())
265
+ try:
266
+ logs.delete_log_stream(logGroupName=name, logStreamName=stream)
267
+ except logs.exceptions.ResourceNotFoundException:
268
+ raise HTTPException(status_code=404, detail=f"Log stream '{stream}' not found")
269
+ except Exception as e:
270
+ raise HTTPException(status_code=500, detail=str(e))
271
+
272
+ cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
273
+ return {"success": True, "message": f"Log stream '{stream}' deleted"}
274
+
275
+
276
+ @router.put("/groups/{name:path}/retention")
277
+ def set_log_group_retention(name: str, body: SetRetentionBody, ep: EndpointInfo = Depends(get_endpoint_info)):
278
+ """Set or remove a log group's retention policy."""
279
+ if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
280
+ raise HTTPException(
281
+ status_code=400,
282
+ detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
283
+ )
284
+
285
+ logs = get_client("logs", **ep.client_kwargs())
286
+ try:
287
+ if body.retention_in_days is None:
288
+ logs.delete_retention_policy(logGroupName=name)
289
+ else:
290
+ logs.put_retention_policy(logGroupName=name, retentionInDays=body.retention_in_days)
291
+ except logs.exceptions.ResourceNotFoundException:
292
+ raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
293
+ except Exception as e:
294
+ raise HTTPException(status_code=500, detail=str(e))
295
+
296
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
297
+ return {"name": name, "retention_in_days": body.retention_in_days}
298
+
299
+
300
+ @router.delete("/groups/{name:path}")
301
+ def delete_log_group(name: str, ep: EndpointInfo = Depends(get_endpoint_info)):
302
+ """Delete a CloudWatch log group."""
303
+ logs = get_client("logs", **ep.client_kwargs())
304
+ try:
305
+ logs.delete_log_group(logGroupName=name)
306
+ except logs.exceptions.ResourceNotFoundException:
307
+ raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
308
+ except Exception as e:
309
+ raise HTTPException(status_code=500, detail=str(e))
310
+
311
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
312
+ cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
313
+ return {"success": True, "message": f"Log group '{name}' deleted"}
@@ -0,0 +1,21 @@
1
+ """Pydantic schemas for CloudWatch Logs API requests."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CreateLogGroupBody(BaseModel):
7
+ """Request body for creating a new log group."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True)
10
+
11
+ name: str = Field(..., description="Log group name")
12
+ retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days")
13
+ tags: dict[str, str] = Field(default_factory=dict, description="Resource tags")
14
+
15
+
16
+ class SetRetentionBody(BaseModel):
17
+ """Request body for setting a log group's retention policy."""
18
+
19
+ model_config = ConfigDict(populate_by_name=True)
20
+
21
+ retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days, or null to never expire")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.8"
3
+ version = "0.3.9"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -13,9 +13,9 @@ backend/websocket.py
13
13
  backend/../ui/dist/favicon.png
14
14
  backend/../ui/dist/favicon.svg
15
15
  backend/../ui/dist/index.html
16
- backend/../ui/dist/assets/StateMachineGraph-CmFIj15P.js
16
+ backend/../ui/dist/assets/StateMachineGraph-BGBIYpUV.js
17
17
  backend/../ui/dist/assets/index-BZr5kWvs.css
18
- backend/../ui/dist/assets/index-Dvr-Ft0R.js
18
+ backend/../ui/dist/assets/index-iwgw2OEV.js
19
19
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
20
20
  backend/../ui/dist/aws-icons/acm.svg
21
21
  backend/../ui/dist/aws-icons/apigateway.svg
@@ -71,6 +71,7 @@ backend/schemas/__init__.py
71
71
  backend/schemas/dynamodb.py
72
72
  backend/schemas/endpoints.py
73
73
  backend/schemas/lambda_svc.py
74
+ backend/schemas/logs.py
74
75
  backend/schemas/s3.py
75
76
  backend/schemas/secretsmanager.py
76
77
  backend/schemas/sqs.py
@@ -107,9 +108,9 @@ tests/test_websocket.py
107
108
  ui/dist/favicon.png
108
109
  ui/dist/favicon.svg
109
110
  ui/dist/index.html
110
- ui/dist/assets/StateMachineGraph-CmFIj15P.js
111
+ ui/dist/assets/StateMachineGraph-BGBIYpUV.js
111
112
  ui/dist/assets/index-BZr5kWvs.css
112
- ui/dist/assets/index-Dvr-Ft0R.js
113
+ ui/dist/assets/index-iwgw2OEV.js
113
114
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
114
115
  ui/dist/aws-icons/acm.svg
115
116
  ui/dist/aws-icons/apigateway.svg
@@ -272,3 +272,162 @@ class TestGetLogEvents:
272
272
  assert resp.status_code == 200
273
273
  data = resp.json()
274
274
  assert data["events"] == []
275
+
276
+
277
+ class TestCreateLogGroup:
278
+ @patch("backend.routes.logs.cache")
279
+ @patch("backend.routes.logs.get_client")
280
+ def test_create_log_group(self, mock_get_client, mock_cache):
281
+ mock_logs = MagicMock()
282
+ mock_get_client.return_value = mock_logs
283
+
284
+ resp = client.post("/api/logs/groups", json={"name": "/aws/lambda/new-fn"})
285
+ assert resp.status_code == 201
286
+ data = resp.json()
287
+ assert data["name"] == "/aws/lambda/new-fn"
288
+ assert data["retention_in_days"] is None
289
+ mock_logs.create_log_group.assert_called_once_with(logGroupName="/aws/lambda/new-fn")
290
+ mock_logs.put_retention_policy.assert_not_called()
291
+
292
+ @patch("backend.routes.logs.cache")
293
+ @patch("backend.routes.logs.get_client")
294
+ def test_create_log_group_with_retention_and_tags(self, mock_get_client, mock_cache):
295
+ mock_logs = MagicMock()
296
+ mock_get_client.return_value = mock_logs
297
+
298
+ resp = client.post(
299
+ "/api/logs/groups",
300
+ json={"name": "/aws/lambda/new-fn", "retentionInDays": 14, "tags": {"env": "prod"}},
301
+ )
302
+ assert resp.status_code == 201
303
+ data = resp.json()
304
+ assert data["retention_in_days"] == 14
305
+
306
+ mock_logs.create_log_group.assert_called_once_with(
307
+ logGroupName="/aws/lambda/new-fn", tags={"env": "prod"}
308
+ )
309
+ mock_logs.put_retention_policy.assert_called_once_with(
310
+ logGroupName="/aws/lambda/new-fn", retentionInDays=14
311
+ )
312
+
313
+ @patch("backend.routes.logs.get_client")
314
+ def test_create_log_group_invalid_retention(self, mock_get_client):
315
+ resp = client.post(
316
+ "/api/logs/groups", json={"name": "/aws/lambda/new-fn", "retentionInDays": 13}
317
+ )
318
+ assert resp.status_code == 400
319
+
320
+ @patch("backend.routes.logs.get_client")
321
+ def test_create_log_group_already_exists(self, mock_get_client):
322
+ mock_logs = MagicMock()
323
+ mock_get_client.return_value = mock_logs
324
+ mock_logs.exceptions.ResourceAlreadyExistsException = type(
325
+ "ResourceAlreadyExistsException", (Exception,), {}
326
+ )
327
+ mock_logs.create_log_group.side_effect = mock_logs.exceptions.ResourceAlreadyExistsException()
328
+
329
+ resp = client.post("/api/logs/groups", json={"name": "existing-group"})
330
+ assert resp.status_code == 409
331
+
332
+
333
+ class TestDeleteLogGroup:
334
+ @patch("backend.routes.logs.cache")
335
+ @patch("backend.routes.logs.get_client")
336
+ def test_delete_log_group(self, mock_get_client, mock_cache):
337
+ mock_logs = MagicMock()
338
+ mock_get_client.return_value = mock_logs
339
+
340
+ resp = client.delete("/api/logs/groups/my-log-group")
341
+ assert resp.status_code == 200
342
+ assert resp.json()["success"] is True
343
+ mock_logs.delete_log_group.assert_called_once_with(logGroupName="my-log-group")
344
+
345
+ @patch("backend.routes.logs.get_client")
346
+ def test_delete_log_group_not_found(self, mock_get_client):
347
+ mock_logs = MagicMock()
348
+ mock_get_client.return_value = mock_logs
349
+ mock_logs.exceptions.ResourceNotFoundException = type(
350
+ "ResourceNotFoundException", (Exception,), {}
351
+ )
352
+ mock_logs.delete_log_group.side_effect = mock_logs.exceptions.ResourceNotFoundException()
353
+
354
+ resp = client.delete("/api/logs/groups/missing-group")
355
+ assert resp.status_code == 404
356
+
357
+
358
+ class TestSetLogGroupRetention:
359
+ @patch("backend.routes.logs.cache")
360
+ @patch("backend.routes.logs.get_client")
361
+ def test_set_retention(self, mock_get_client, mock_cache):
362
+ mock_logs = MagicMock()
363
+ mock_get_client.return_value = mock_logs
364
+
365
+ resp = client.put(
366
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": 30}
367
+ )
368
+ assert resp.status_code == 200
369
+ assert resp.json()["retention_in_days"] == 30
370
+ mock_logs.put_retention_policy.assert_called_once_with(
371
+ logGroupName="my-log-group", retentionInDays=30
372
+ )
373
+
374
+ @patch("backend.routes.logs.cache")
375
+ @patch("backend.routes.logs.get_client")
376
+ def test_clear_retention(self, mock_get_client, mock_cache):
377
+ mock_logs = MagicMock()
378
+ mock_get_client.return_value = mock_logs
379
+
380
+ resp = client.put(
381
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": None}
382
+ )
383
+ assert resp.status_code == 200
384
+ assert resp.json()["retention_in_days"] is None
385
+ mock_logs.delete_retention_policy.assert_called_once_with(logGroupName="my-log-group")
386
+
387
+ @patch("backend.routes.logs.get_client")
388
+ def test_set_retention_invalid_value(self, mock_get_client):
389
+ resp = client.put(
390
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": 999}
391
+ )
392
+ assert resp.status_code == 400
393
+
394
+ @patch("backend.routes.logs.get_client")
395
+ def test_set_retention_group_not_found(self, mock_get_client):
396
+ mock_logs = MagicMock()
397
+ mock_get_client.return_value = mock_logs
398
+ mock_logs.exceptions.ResourceNotFoundException = type(
399
+ "ResourceNotFoundException", (Exception,), {}
400
+ )
401
+ mock_logs.put_retention_policy.side_effect = mock_logs.exceptions.ResourceNotFoundException()
402
+
403
+ resp = client.put(
404
+ "/api/logs/groups/missing-group/retention", json={"retentionInDays": 30}
405
+ )
406
+ assert resp.status_code == 404
407
+
408
+
409
+ class TestDeleteLogStream:
410
+ @patch("backend.routes.logs.cache")
411
+ @patch("backend.routes.logs.get_client")
412
+ def test_delete_log_stream(self, mock_get_client, mock_cache):
413
+ mock_logs = MagicMock()
414
+ mock_get_client.return_value = mock_logs
415
+
416
+ resp = client.delete("/api/logs/groups/my-log-group/streams/my-stream")
417
+ assert resp.status_code == 200
418
+ assert resp.json()["success"] is True
419
+ mock_logs.delete_log_stream.assert_called_once_with(
420
+ logGroupName="my-log-group", logStreamName="my-stream"
421
+ )
422
+
423
+ @patch("backend.routes.logs.get_client")
424
+ def test_delete_log_stream_not_found(self, mock_get_client):
425
+ mock_logs = MagicMock()
426
+ mock_get_client.return_value = mock_logs
427
+ mock_logs.exceptions.ResourceNotFoundException = type(
428
+ "ResourceNotFoundException", (Exception,), {}
429
+ )
430
+ mock_logs.delete_log_stream.side_effect = mock_logs.exceptions.ResourceNotFoundException()
431
+
432
+ resp = client.delete("/api/logs/groups/my-log-group/streams/missing-stream")
433
+ assert resp.status_code == 404
@@ -1,4 +1,4 @@
1
- import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-Dvr-Ft0R.js";import{T as B}from"./stepfunctions-graph-B6nvU4OB.js";/**
1
+ import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-iwgw2OEV.js";import{T as B}from"./stepfunctions-graph-B6nvU4OB.js";/**
2
2
  * @license lucide-react v1.7.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.