compose-runner 0.6.5__tar.gz → 0.7.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.
Files changed (38) hide show
  1. {compose_runner-0.6.5 → compose_runner-0.7.0}/PKG-INFO +2 -2
  2. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/_version.py +2 -2
  3. compose_runner-0.7.0/compose_runner/aws_lambda/cost_check_handler.py +71 -0
  4. {compose_runner-0.6.5 → compose_runner-0.7.0}/infra/cdk/stacks/compose_runner_stack.py +49 -1
  5. {compose_runner-0.6.5 → compose_runner-0.7.0}/pyproject.toml +1 -1
  6. {compose_runner-0.6.5 → compose_runner-0.7.0}/.gitignore +0 -0
  7. {compose_runner-0.6.5 → compose_runner-0.7.0}/Dockerfile +0 -0
  8. {compose_runner-0.6.5 → compose_runner-0.7.0}/LICENSE +0 -0
  9. {compose_runner-0.6.5 → compose_runner-0.7.0}/README.md +0 -0
  10. {compose_runner-0.6.5 → compose_runner-0.7.0}/aws_lambda/.dockerignore +0 -0
  11. {compose_runner-0.6.5 → compose_runner-0.7.0}/aws_lambda/Dockerfile +0 -0
  12. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/__init__.py +0 -0
  13. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/__init__.py +0 -0
  14. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/common.py +0 -0
  15. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/log_poll_handler.py +0 -0
  16. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/results_handler.py +0 -0
  17. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/run_handler.py +0 -0
  18. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/aws_lambda/status_handler.py +0 -0
  19. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/cli.py +0 -0
  20. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/ecs_task.py +0 -0
  21. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/run.py +0 -0
  22. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/sentry.py +0 -0
  23. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_lambda_handlers/test_select_task_size_uses_large_for_montecarlo.yaml +0 -0
  24. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_lambda_handlers/test_select_task_size_uses_standard_for_fdr.yaml +0 -0
  25. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_run/test_download_bundle.yaml +0 -0
  26. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_run/test_run_database_workflow.yaml +0 -0
  27. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_run/test_run_group_comparison_workflow.yaml +0 -0
  28. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_run/test_run_string_group_comparison_workflow.yaml +0 -0
  29. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/cassettes/test_run/test_run_workflow.yaml +0 -0
  30. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/conftest.py +0 -0
  31. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/test_cli.py +0 -0
  32. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/test_ecs_task.py +0 -0
  33. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/test_lambda_handlers.py +0 -0
  34. {compose_runner-0.6.5 → compose_runner-0.7.0}/compose_runner/tests/test_run.py +0 -0
  35. {compose_runner-0.6.5 → compose_runner-0.7.0}/infra/cdk/app.py +0 -0
  36. {compose_runner-0.6.5 → compose_runner-0.7.0}/infra/cdk/cdk.json +0 -0
  37. {compose_runner-0.6.5 → compose_runner-0.7.0}/infra/cdk/requirements.txt +0 -0
  38. {compose_runner-0.6.5 → compose_runner-0.7.0}/infra/cdk/stacks/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compose-runner
3
- Version: 0.6.5
3
+ Version: 0.7.0
4
4
  Summary: A package for running neurosynth-compose analyses
5
5
  Project-URL: Repository, https://github.com/neurostuff/compose-runner
6
6
  Author-email: James Kent <jamesdkent21@gmail.com>
@@ -10,7 +10,7 @@ Keywords: meta-analysis,neuroimaging,neurosynth,neurosynth-compose
10
10
  Classifier: License :: OSI Approved :: BSD License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Dist: click
13
- Requires-Dist: nimare
13
+ Requires-Dist: nimare>=0.7.0
14
14
  Requires-Dist: numpy
15
15
  Requires-Dist: sentry-sdk
16
16
  Provides-Extra: aws
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.6.5'
32
- __version_tuple__ = version_tuple = (0, 6, 5)
31
+ __version__ = version = '0.7.0'
32
+ __version_tuple__ = version_tuple = (0, 7, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as _dt
4
+ import logging
5
+ import os
6
+ from decimal import Decimal
7
+ from typing import Any, Dict
8
+
9
+ import boto3
10
+ from botocore.exceptions import BotoCoreError, ClientError
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.setLevel(logging.INFO)
14
+
15
+ _CE_CLIENT = boto3.client("ce", region_name=os.environ.get("AWS_REGION", "us-east-1"))
16
+
17
+ COST_LIMIT_ENV = "COST_LIMIT_USD"
18
+
19
+
20
+ def _month_range(today: _dt.date) -> Dict[str, str]:
21
+ start = today.replace(day=1)
22
+ # Cost Explorer end date is exclusive; add a day to include today.
23
+ end = today + _dt.timedelta(days=1)
24
+ return {"Start": start.isoformat(), "End": end.isoformat()}
25
+
26
+
27
+ def _current_month_cost() -> Dict[str, Any]:
28
+ period = _month_range(_dt.date.today())
29
+ response = _CE_CLIENT.get_cost_and_usage(
30
+ TimePeriod=period,
31
+ Granularity="MONTHLY",
32
+ Metrics=["UnblendedCost"],
33
+ )
34
+ results = response.get("ResultsByTime", [])
35
+ total = results[0]["Total"]["UnblendedCost"] if results else {"Amount": "0", "Unit": "USD"}
36
+ amount = float(Decimal(total.get("Amount", "0")))
37
+ currency = total.get("Unit", "USD")
38
+ return {"amount": amount, "currency": currency, "time_period": period}
39
+
40
+
41
+ def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
42
+ limit_raw = os.environ.get(COST_LIMIT_ENV)
43
+ if not limit_raw:
44
+ raise RuntimeError(f"{COST_LIMIT_ENV} environment variable must be set.")
45
+
46
+ try:
47
+ limit = float(limit_raw)
48
+ except ValueError as exc: # noqa: PERF203
49
+ raise RuntimeError(f"Invalid {COST_LIMIT_ENV}: {limit_raw}") from exc
50
+
51
+ try:
52
+ cost = _current_month_cost()
53
+ except (ClientError, BotoCoreError) as exc:
54
+ logger.error("Failed to query Cost Explorer: %s", exc)
55
+ return {
56
+ "status": "ERROR",
57
+ "allowed": False,
58
+ "error": "cost_explorer_unavailable",
59
+ "limit": limit,
60
+ }
61
+
62
+ amount = cost["amount"]
63
+ allowed = amount < limit
64
+ return {
65
+ "status": "OK",
66
+ "allowed": allowed,
67
+ "current_spend": amount,
68
+ "limit": limit,
69
+ "currency": cost.get("currency", "USD"),
70
+ "time_period": cost.get("time_period"),
71
+ }
@@ -36,6 +36,7 @@ class ComposeRunnerStack(Stack):
36
36
  poll_memory_size = int(self.node.try_get_context("pollMemorySize") or 512)
37
37
  poll_timeout_seconds = int(self.node.try_get_context("pollTimeoutSeconds") or 30)
38
38
  poll_lookback_ms = int(self.node.try_get_context("pollLookbackMs") or 3600000)
39
+ monthly_spend_limit_usd = float(self.node.try_get_context("monthlySpendLimit") or 100)
39
40
 
40
41
  task_cpu = int(self.node.try_get_context("taskCpu") or 4096)
41
42
  task_memory_mib = int(self.node.try_get_context("taskMemoryMiB") or 30720)
@@ -243,6 +244,31 @@ class ComposeRunnerStack(Stack):
243
244
  max_attempts=2,
244
245
  )
245
246
 
247
+ cost_check_code = lambda_.DockerImageCode.from_image_asset(
248
+ str(project_root),
249
+ file="aws_lambda/Dockerfile",
250
+ cmd=["compose_runner.aws_lambda.cost_check_handler.handler"],
251
+ build_args=build_args,
252
+ )
253
+
254
+ cost_check_function = lambda_.DockerImageFunction(
255
+ self,
256
+ "ComposeRunnerCostCheck",
257
+ code=cost_check_code,
258
+ memory_size=256,
259
+ timeout=Duration.seconds(15),
260
+ environment={
261
+ "COST_LIMIT_USD": str(monthly_spend_limit_usd),
262
+ },
263
+ description="Blocks executions when monthly spend exceeds the configured limit.",
264
+ )
265
+ cost_check_function.add_to_role_policy(
266
+ iam.PolicyStatement(
267
+ actions=["ce:GetCostAndUsage"],
268
+ resources=["*"],
269
+ )
270
+ )
271
+
246
272
  run_output = sfn.Pass(
247
273
  self,
248
274
  "ComposeRunnerOutput",
@@ -256,7 +282,7 @@ class ComposeRunnerStack(Stack):
256
282
  },
257
283
  )
258
284
 
259
- definition_chain = sfn.Choice(
285
+ task_selection = sfn.Choice(
260
286
  self,
261
287
  "SelectFargateTask",
262
288
  ).when(
@@ -266,6 +292,28 @@ class ComposeRunnerStack(Stack):
266
292
  run_task_standard.next(run_output)
267
293
  )
268
294
 
295
+ cost_limit_exceeded = sfn.Fail(
296
+ self,
297
+ "CostLimitExceeded",
298
+ cause="Monthly spend limit exceeded.",
299
+ error="CostLimitExceeded",
300
+ )
301
+
302
+ enforce_cost_limit = sfn.Choice(self, "EnforceMonthlyCostLimit").when(
303
+ sfn.Condition.boolean_equals("$.cost_check.Payload.allowed", False),
304
+ cost_limit_exceeded,
305
+ ).otherwise(task_selection)
306
+
307
+ cost_check_step = tasks.LambdaInvoke(
308
+ self,
309
+ "CheckMonthlyCost",
310
+ lambda_function=cost_check_function,
311
+ payload=sfn.TaskInput.from_object({"stateInput.$": "$"}),
312
+ result_path="$.cost_check",
313
+ )
314
+
315
+ definition_chain = cost_check_step.next(enforce_cost_limit)
316
+
269
317
  state_machine = sfn.StateMachine(
270
318
  self,
271
319
  "ComposeRunnerStateMachine",
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  ]
16
16
  dynamic = ["version"]
17
- dependencies = ["nimare", "click", "sentry-sdk", "numpy"]
17
+ dependencies = ["nimare>=0.7.0", "click", "sentry-sdk", "numpy"]
18
18
 
19
19
  [project.urls]
20
20
  Repository = "https://github.com/neurostuff/compose-runner"
File without changes
File without changes