langgraph-api 0.4.11__tar.gz → 0.4.15__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.

Potentially problematic release.


This version of langgraph-api might be problematic. Click here for more details.

Files changed (122) hide show
  1. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/PKG-INFO +2 -2
  2. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/ramp.js +18 -23
  3. langgraph_api-0.4.15/langgraph_api/__init__.py +1 -0
  4. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/a2a.py +1 -1
  5. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/meta.py +35 -23
  6. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/asyncio.py +4 -0
  7. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/backend.py +1 -1
  8. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/remote.py +1 -1
  9. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/queue_entrypoint.py +13 -3
  10. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/serde.py +0 -19
  11. langgraph_api-0.4.15/langgraph_api/utils/cache.py +95 -0
  12. langgraph_api-0.4.15/langgraph_api/utils/retriable_client.py +74 -0
  13. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/worker.py +1 -1
  14. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/pyproject.toml +1 -1
  15. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/uv.lock +38 -38
  16. langgraph_api-0.4.11/langgraph_api/__init__.py +0 -1
  17. langgraph_api-0.4.11/langgraph_api/utils/cache.py +0 -58
  18. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/.gitignore +0 -0
  19. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/LICENSE +0 -0
  20. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/Makefile +0 -0
  21. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/README.md +0 -0
  22. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/.gitignore +0 -0
  23. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/Makefile +0 -0
  24. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/README.md +0 -0
  25. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/burst.js +0 -0
  26. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/clean.js +0 -0
  27. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/graphs.js +0 -0
  28. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/package.json +0 -0
  29. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/update-revision.js +0 -0
  30. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/weather.js +0 -0
  31. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/constraints.txt +0 -0
  32. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/forbidden.txt +0 -0
  33. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/healthcheck.py +0 -0
  34. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/__init__.py +0 -0
  35. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/assistants.py +0 -0
  36. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/mcp.py +0 -0
  37. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/openapi.py +0 -0
  38. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/runs.py +0 -0
  39. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/store.py +0 -0
  40. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/threads.py +0 -0
  41. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/ui.py +0 -0
  42. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/asgi_transport.py +0 -0
  43. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/__init__.py +0 -0
  44. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/custom.py +0 -0
  45. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/__init__.py +0 -0
  46. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/client.py +0 -0
  47. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/middleware.py +0 -0
  48. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/noop.py +0 -0
  49. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/studio_user.py +0 -0
  50. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/cli.py +0 -0
  51. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/command.py +0 -0
  52. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/config.py +0 -0
  53. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/cron_scheduler.py +0 -0
  54. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/errors.py +0 -0
  55. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/executor_entrypoint.py +0 -0
  56. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/feature_flags.py +0 -0
  57. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/graph.py +0 -0
  58. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/http.py +0 -0
  59. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/http_metrics.py +0 -0
  60. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/.gitignore +0 -0
  61. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/.prettierrc +0 -0
  62. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/__init__.py +0 -0
  63. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/base.py +0 -0
  64. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/build.mts +0 -0
  65. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/client.http.mts +0 -0
  66. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/client.mts +0 -0
  67. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/errors.py +0 -0
  68. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/global.d.ts +0 -0
  69. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/package.json +0 -0
  70. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/schema.py +0 -0
  71. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/graph.mts +0 -0
  72. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/load.hooks.mjs +0 -0
  73. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/preload.mjs +0 -0
  74. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/files.mts +0 -0
  75. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/importMap.mts +0 -0
  76. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  77. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/serde.mts +0 -0
  78. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/sse.py +0 -0
  79. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/traceblock.mts +0 -0
  80. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/tsconfig.json +0 -0
  81. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/ui.py +0 -0
  82. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/yarn.lock +0 -0
  83. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/logging.py +0 -0
  84. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/metadata.py +0 -0
  85. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/__init__.py +0 -0
  86. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/http_logger.py +0 -0
  87. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/private_network.py +0 -0
  88. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/request_id.py +0 -0
  89. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/models/__init__.py +0 -0
  90. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/models/run.py +0 -0
  91. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/patch.py +0 -0
  92. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/route.py +0 -0
  93. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/schema.py +0 -0
  94. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/server.py +0 -0
  95. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/sse.py +0 -0
  96. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/state.py +0 -0
  97. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/store.py +0 -0
  98. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/stream.py +0 -0
  99. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/thread_ttl.py +0 -0
  100. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/traceblock.py +0 -0
  101. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/tunneling/cloudflare.py +0 -0
  102. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/__init__.py +0 -0
  103. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/config.py +0 -0
  104. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/future.py +0 -0
  105. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/headers.py +0 -0
  106. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/uuids.py +0 -0
  107. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/validation.py +0 -0
  108. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/webhook.py +0 -0
  109. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_license/__init__.py +0 -0
  110. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_license/validation.py +0 -0
  111. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/__init__.py +0 -0
  112. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/checkpoint.py +0 -0
  113. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/database.py +0 -0
  114. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/lifespan.py +0 -0
  115. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/metrics.py +0 -0
  116. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/ops.py +0 -0
  117. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/queue.py +0 -0
  118. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/retry.py +0 -0
  119. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/store.py +0 -0
  120. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/logging.json +0 -0
  121. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/openapi.json +0 -0
  122. {langgraph_api-0.4.11 → langgraph_api-0.4.15}/scripts/create_license.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.4.11
3
+ Version: 0.4.15
4
4
  Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ Requires-Dist: httpx>=0.25.0
11
11
  Requires-Dist: jsonschema-rs<0.30,>=0.20.0
12
12
  Requires-Dist: langchain-core>=0.3.64
13
13
  Requires-Dist: langgraph-checkpoint>=2.0.23
14
- Requires-Dist: langgraph-runtime-inmem<0.12.0,>=0.11.0
14
+ Requires-Dist: langgraph-runtime-inmem<0.13.0,>=0.12.0
15
15
  Requires-Dist: langgraph-sdk>=0.2.0
16
16
  Requires-Dist: langgraph>=0.4.0
17
17
  Requires-Dist: langsmith>=0.3.45
@@ -56,7 +56,7 @@ export let options = {
56
56
  },
57
57
  thresholds: {
58
58
  'run_duration': [`p(95)<${p95_run_duration[MODE]}`],
59
- 'successful_runs': [`count>${(PLATEAU_DURATION / (p95_run_duration[MODE] / 1000)) * LOAD_SIZE * LEVELS * 2}`], // Number of expected successful runs per user worst caseduring plateau * max number of users * 2 cause that feels about right
59
+ 'successful_runs': [`count>${(PLATEAU_DURATION / (p95_run_duration[MODE] / 1000)) * LOAD_SIZE * LEVELS * 2}`], // Number of expected successful runs per user worst case during plateau * max number of users * 2 cause that feels about right
60
60
  'http_req_failed': ['rate<0.01'], // Error rate should be less than 1%
61
61
  },
62
62
  };
@@ -109,10 +109,16 @@ export default function() {
109
109
 
110
110
  // Check the response
111
111
  const expected_length = MODE === 'single' ? 1 : EXPAND + 1;
112
- const success = check(response, {
113
- 'Run completed successfully': (r) => r.status === 200,
114
- 'Response contains expected number of messages': (r) => JSON.parse(r.body)?.messages?.length === expected_length,
115
- });
112
+ let success = false;
113
+ try {
114
+ success = check(response, {
115
+ 'Run completed successfully': (r) => r.status === 200,
116
+ 'Response contains expected number of messages': (r) => JSON.parse(r.body)?.messages?.length === expected_length,
117
+ });
118
+ } catch (error) {
119
+ console.log(`Error checking response: ${error}`);
120
+ }
121
+
116
122
 
117
123
  if (success) {
118
124
  // Record success metrics
@@ -126,34 +132,23 @@ export default function() {
126
132
  if (response.status >= 500) {
127
133
  serverErrors.add(1);
128
134
  console.log(`Server error: ${response.status}`);
129
- } else if (response.status === 408 || response.error === 'timeout') {
135
+ } else if (response.status === 408 || response.error?.includes('timeout')) {
130
136
  timeoutErrors.add(1);
131
137
  console.log(`Timeout error: ${response.error}`);
132
- } else if (response.status === 200 && response?.body?.messages?.length !== expected_length) {
138
+ } else if (response.status === 200 && response.body?.messages?.length !== expected_length) {
133
139
  missingMessageErrors.add(1);
134
- console.log(response);
135
- console.log(`Missing message error: Status ${response.status}, ${JSON.stringify(response.body)}`);
140
+ console.log(`Missing message error: Status ${response.status}, ${JSON.stringify(response.body)}, ${response.headers?.['Content-Location']}`);
136
141
  } else {
137
142
  otherErrors.add(1);
138
143
  console.log(`Other error: Status ${response.status}, ${JSON.stringify(response.body)}`);
139
144
  }
140
145
  }
141
146
  } catch (error) {
142
- // Handle exceptions (network errors, etc.)
147
+ // Handle truly unexpected errors
143
148
  failedRuns.add(1);
144
-
145
- if (error.message.includes('timeout')) {
146
- timeoutErrors.add(1);
147
- console.log(`Timeout error: ${error.message}`);
148
- } else if (error.message.includes('connection') || error.message.includes('network')) {
149
- connectionErrors.add(1);
150
- console.log(`Connection error: ${error.message}`);
151
- } else {
152
- otherErrors.add(1);
153
- // Usually we end up with HTML error pages here
154
- console.log(response);
155
- console.log(`Unexpected error: ${error.message}, Response Body: ${response?.body}`);
156
- }
149
+ otherErrors.add(1);
150
+ console.log(response);
151
+ console.log(`Unexpected error: ${error.message}, Response Body: ${response?.body}`);
157
152
  }
158
153
 
159
154
  // Add a small random sleep between iterations to prevent thundering herd
@@ -0,0 +1 @@
1
+ __version__ = "0.4.15"
@@ -171,7 +171,7 @@ async def _validate_supports_messages(
171
171
  """
172
172
  assistant_id = assistant["assistant_id"]
173
173
 
174
- cached_schemas = _assistant_schemas_cache.get(assistant_id)
174
+ cached_schemas = await _assistant_schemas_cache.get(assistant_id)
175
175
  if cached_schemas is not None:
176
176
  schemas = cached_schemas
177
177
  else:
@@ -1,6 +1,7 @@
1
1
  from typing import cast
2
2
 
3
3
  import langgraph.version
4
+ import structlog
4
5
  from starlette.responses import JSONResponse, PlainTextResponse
5
6
 
6
7
  from langgraph_api import __version__, config, metadata
@@ -13,6 +14,8 @@ from langgraph_runtime.ops import Runs
13
14
 
14
15
  METRICS_FORMATS = {"prometheus", "json"}
15
16
 
17
+ logger = structlog.stdlib.get_logger(__name__)
18
+
16
19
 
17
20
  async def meta_info(request: ApiRequest):
18
21
  plus = plus_features_enabled()
@@ -71,35 +74,44 @@ async def meta_metrics(request: ApiRequest):
71
74
  resp["workers"] = worker_metrics
72
75
  return JSONResponse(resp)
73
76
  elif metrics_format == "prometheus":
74
- async with connect() as conn:
75
- queue_stats = await Runs.stats(conn)
76
-
77
- metrics = [
78
- "# HELP lg_api_num_pending_runs The number of runs currently pending.",
79
- "# TYPE lg_api_num_pending_runs gauge",
80
- f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
81
- "# HELP lg_api_num_running_runs The number of runs currently running.",
82
- "# TYPE lg_api_num_running_runs gauge",
83
- f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
84
- ]
77
+ metrics = []
78
+ try:
79
+ async with connect() as conn:
80
+ queue_stats = await Runs.stats(conn)
85
81
 
86
- if config.N_JOBS_PER_WORKER > 0:
87
82
  metrics.extend(
88
83
  [
89
- "# HELP lg_api_workers_max The maximum number of workers available.",
90
- "# TYPE lg_api_workers_max gauge",
91
- f'lg_api_workers_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_max}',
92
- "# HELP lg_api_workers_active The number of currently active workers.",
93
- "# TYPE lg_api_workers_active gauge",
94
- f'lg_api_workers_active{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_active}',
95
- "# HELP lg_api_workers_available The number of available (idle) workers.",
96
- "# TYPE lg_api_workers_available gauge",
97
- f'lg_api_workers_available{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_available}',
84
+ "# HELP lg_api_num_pending_runs The number of runs currently pending.",
85
+ "# TYPE lg_api_num_pending_runs gauge",
86
+ f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
87
+ "# HELP lg_api_num_running_runs The number of runs currently running.",
88
+ "# TYPE lg_api_num_running_runs gauge",
89
+ f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
98
90
  ]
99
91
  )
92
+ except Exception as e:
93
+ # if we get a db connection error/timeout, just skip queue stats
94
+ await logger.awarning(
95
+ "Ignoring error while getting run stats for /metrics", exc_info=e
96
+ )
97
+
98
+ if config.N_JOBS_PER_WORKER > 0:
99
+ metrics.extend(
100
+ [
101
+ "# HELP lg_api_workers_max The maximum number of workers available.",
102
+ "# TYPE lg_api_workers_max gauge",
103
+ f'lg_api_workers_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_max}',
104
+ "# HELP lg_api_workers_active The number of currently active workers.",
105
+ "# TYPE lg_api_workers_active gauge",
106
+ f'lg_api_workers_active{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_active}',
107
+ "# HELP lg_api_workers_available The number of available (idle) workers.",
108
+ "# TYPE lg_api_workers_available gauge",
109
+ f'lg_api_workers_available{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_available}',
110
+ ]
111
+ )
100
112
 
101
- metrics.extend(http_metrics)
102
- metrics.extend(pg_redis_stats)
113
+ metrics.extend(http_metrics)
114
+ metrics.extend(pg_redis_stats)
103
115
 
104
116
  metrics_response = "\n".join(metrics)
105
117
  return PlainTextResponse(metrics_response)
@@ -158,6 +158,7 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
158
158
  self,
159
159
  *coros: Coroutine[Any, Any, T],
160
160
  cancel: bool = False,
161
+ cancel_event: asyncio.Event | None = None,
161
162
  wait: bool = True,
162
163
  taskset: set[asyncio.Task] | None = None,
163
164
  taskgroup_name: str | None = None,
@@ -165,6 +166,7 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
165
166
  # Copy the taskset to avoid modifying the original set unintentionally (like in lifespan)
166
167
  self.tasks = taskset.copy() if taskset is not None else set()
167
168
  self.cancel = cancel
169
+ self.cancel_event = cancel_event
168
170
  self.wait = wait
169
171
  if taskset:
170
172
  for task in tuple(taskset):
@@ -181,6 +183,8 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
181
183
  try:
182
184
  if (exc := task.exception()) and not isinstance(exc, ignore_exceptions):
183
185
  logger.exception("asyncio.task failed in task group", exc_info=exc)
186
+ if self.cancel_event:
187
+ self.cancel_event.set()
184
188
  except asyncio.CancelledError:
185
189
  pass
186
190
 
@@ -58,7 +58,7 @@ class LangsmithAuthBackend(AuthenticationBackend):
58
58
 
59
59
  # Check cache first
60
60
  cache_key = self._get_cache_key(headers)
61
- if cached_entry := self._cache.get(cache_key):
61
+ if cached_entry := await self._cache.get(cache_key):
62
62
  return cached_entry["credentials"], cached_entry["user"]
63
63
 
64
64
  async with auth_client() as auth:
@@ -895,7 +895,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
895
895
  if self.cache_keys:
896
896
  cache_key = tuple((k, headers[k]) for k in self.cache_keys if k in headers)
897
897
  if cache_key and self.ttl_cache is not None:
898
- cached = self.ttl_cache.get(cache_key)
898
+ cached = await self.ttl_cache.get(cache_key)
899
899
  if cached:
900
900
  return cached
901
901
 
@@ -86,6 +86,7 @@ async def health_and_metrics_server():
86
86
  log_level="error",
87
87
  access_log=False,
88
88
  )
89
+ # Server will run indefinitely until the process is terminated
89
90
  server = uvicorn.Server(config)
90
91
 
91
92
  logger.info(f"Health and metrics server started at http://0.0.0.0:{port}")
@@ -93,14 +94,15 @@ async def health_and_metrics_server():
93
94
 
94
95
 
95
96
  async def entrypoint(
96
- grpc_port: int | None = None, entrypoint_name: str = "python-queue"
97
+ grpc_port: int | None = None,
98
+ entrypoint_name: str = "python-queue",
99
+ cancel_event: asyncio.Event | None = None,
97
100
  ):
98
101
  from langgraph_api import logging as lg_logging
99
102
  from langgraph_api.api import user_router
100
103
 
101
104
  lg_logging.set_logging_context({"entrypoint": entrypoint_name})
102
105
  tasks: set[asyncio.Task] = set()
103
- tasks.add(asyncio.create_task(health_and_metrics_server()))
104
106
 
105
107
  original_lifespan = user_router.router.lifespan_context if user_router else None
106
108
 
@@ -113,6 +115,7 @@ async def entrypoint(
113
115
  with_cron_scheduler=with_cron_scheduler,
114
116
  grpc_port=grpc_port,
115
117
  taskset=taskset,
118
+ cancel_event=cancel_event,
116
119
  ):
117
120
  if original_lifespan:
118
121
  async with original_lifespan(app):
@@ -123,6 +126,7 @@ async def entrypoint(
123
126
  async with combined_lifespan(
124
127
  None, with_cron_scheduler=False, grpc_port=grpc_port, taskset=tasks
125
128
  ):
129
+ tasks.add(asyncio.create_task(health_and_metrics_server()))
126
130
  await asyncio.gather(*tasks)
127
131
 
128
132
 
@@ -141,8 +145,14 @@ async def main(grpc_port: int | None = None, entrypoint_name: str = "python-queu
141
145
  signal.signal(signal.SIGTERM, lambda *_: _handle_signal())
142
146
 
143
147
  entry_task = asyncio.create_task(
144
- entrypoint(grpc_port=grpc_port, entrypoint_name=entrypoint_name)
148
+ entrypoint(
149
+ grpc_port=grpc_port,
150
+ entrypoint_name=entrypoint_name,
151
+ cancel_event=stop_event,
152
+ )
145
153
  )
154
+ # Handle the case where the entrypoint errors out
155
+ entry_task.add_done_callback(lambda _: stop_event.set())
146
156
  await stop_event.wait()
147
157
 
148
158
  logger.warning("Cancelling queue entrypoint task")
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import base64
3
2
  import re
4
3
  import uuid
5
4
  from base64 import b64encode
@@ -178,21 +177,3 @@ class Serializer(JsonPlusSerializer):
178
177
 
179
178
  mpack_keys = {"method", "value"}
180
179
  SERIALIZER = Serializer()
181
-
182
-
183
- # TODO: Make more performant (by removing)
184
- async def reserialize_message(message: bytes) -> bytes:
185
- # Stream messages from golang runtime are a byte dict of StreamChunks.
186
- loaded = await ajson_loads(message)
187
- converted = {}
188
- for k, v in loaded.items():
189
- if isinstance(v, dict) and v.keys() == mpack_keys:
190
- if v["method"] == "missing":
191
- converted[k] = v["value"] # oops
192
- else:
193
- converted[k] = SERIALIZER.loads_typed(
194
- (v["method"], base64.b64decode(v["value"]))
195
- )
196
- else:
197
- converted[k] = v
198
- return json_dumpb(converted)
@@ -0,0 +1,95 @@
1
+ import asyncio
2
+ import time
3
+ from collections import OrderedDict
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Generic, TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class LRUCache(Generic[T]):
11
+ """LRU cache with TTL and proactive refresh support."""
12
+
13
+ def __init__(
14
+ self,
15
+ max_size: int = 1000,
16
+ ttl: float = 60,
17
+ refresh_window: float = 30,
18
+ refresh_callback: Callable[[str], Awaitable[T | None]] | None = None,
19
+ ):
20
+ self._cache: OrderedDict[str, tuple[T, float, bool]] = OrderedDict()
21
+ self._max_size = max_size if max_size > 0 else 1000
22
+ self._ttl = ttl
23
+ self._refresh_window = refresh_window if refresh_window > 0 else 30
24
+ self._refresh_callback = refresh_callback
25
+
26
+ def _get_time(self) -> float:
27
+ """Get current time, using loop.time() if available for better performance."""
28
+ try:
29
+ return asyncio.get_event_loop().time()
30
+ except RuntimeError:
31
+ return time.monotonic()
32
+
33
+ async def get(self, key: str) -> T | None:
34
+ """Get item from cache, attempting refresh if within refresh window."""
35
+ if key not in self._cache:
36
+ return None
37
+
38
+ value, timestamp, is_refreshing = self._cache[key]
39
+ current_time = self._get_time()
40
+ time_until_expiry = self._ttl - (current_time - timestamp)
41
+
42
+ # Check if expired
43
+ if time_until_expiry <= 0:
44
+ del self._cache[key]
45
+ return None
46
+
47
+ # Check if we should attempt refresh (within refresh window and not already refreshing)
48
+ if (
49
+ time_until_expiry <= self._refresh_window
50
+ and not is_refreshing
51
+ and self._refresh_callback
52
+ ):
53
+ # Mark as refreshing to prevent multiple simultaneous refresh attempts
54
+ self._cache[key] = (value, timestamp, True)
55
+
56
+ try:
57
+ # Attempt refresh
58
+ refreshed_value = await self._refresh_callback(key)
59
+ if refreshed_value is not None:
60
+ # Refresh successful, update cache with new value
61
+ self._cache[key] = (refreshed_value, current_time, False)
62
+ # Move to end (most recently used)
63
+ self._cache.move_to_end(key)
64
+ return refreshed_value
65
+ else:
66
+ # Refresh failed, fallback to cached value
67
+ self._cache[key] = (value, timestamp, False)
68
+ except Exception:
69
+ # Refresh failed with exception, fallback to cached value
70
+ self._cache[key] = (value, timestamp, False)
71
+
72
+ # Move to end (most recently used)
73
+ self._cache.move_to_end(key)
74
+ return value
75
+
76
+ def set(self, key: str, value: T) -> None:
77
+ """Set item in cache, evicting old entries if needed."""
78
+ # Remove if already exists (to update timestamp)
79
+ if key in self._cache:
80
+ del self._cache[key]
81
+
82
+ # Evict oldest entries if needed
83
+ while len(self._cache) >= self._max_size:
84
+ self._cache.popitem(last=False) # Remove oldest (FIFO)
85
+
86
+ # Add new entry (not refreshing initially)
87
+ self._cache[key] = (value, self._get_time(), False)
88
+
89
+ def size(self) -> int:
90
+ """Return current cache size."""
91
+ return len(self._cache)
92
+
93
+ def clear(self) -> None:
94
+ """Clear all entries from cache."""
95
+ self._cache.clear()
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+
3
+ import httpx
4
+ import structlog
5
+
6
+ logger = structlog.stdlib.get_logger(__name__)
7
+
8
+
9
+ async def _make_http_request_with_retries(
10
+ url: str,
11
+ headers: dict,
12
+ method: str = "GET",
13
+ json_data: dict | None = None,
14
+ max_retries: int = 3,
15
+ base_delay: float = 1.0,
16
+ ) -> httpx.Response | None:
17
+ """
18
+ Make an HTTP request with exponential backoff retries.
19
+
20
+ Args:
21
+ url: The URL to request
22
+ headers: Headers to include in the request
23
+ method: HTTP method ("GET" or "POST")
24
+ json_data: JSON data for POST requests
25
+ max_retries: Maximum number of retry attempts
26
+ base_delay: Base delay in seconds for exponential backoff
27
+
28
+ Returns:
29
+ httpx.Response: The successful response
30
+
31
+ Raises:
32
+ httpx.HTTPStatusError: If the request fails after all retries
33
+ httpx.RequestError: If the request fails after all retries
34
+ """
35
+ for attempt in range(max_retries + 1):
36
+ try:
37
+ async with httpx.AsyncClient(timeout=10.0) as client:
38
+ response = await client.request(
39
+ method, url, headers=headers, json=json_data
40
+ )
41
+ response.raise_for_status()
42
+ return response
43
+
44
+ except (
45
+ httpx.TimeoutException,
46
+ httpx.NetworkError,
47
+ httpx.RequestError,
48
+ httpx.HTTPStatusError,
49
+ ) as e:
50
+ if isinstance(e, httpx.HTTPStatusError) and e.response.status_code < 500:
51
+ # Don't retry on 4xx errors, but do on 5xxs
52
+ raise e
53
+
54
+ # Back off and retry if we haven't reached the max retries
55
+ if attempt < max_retries:
56
+ delay = base_delay * (2**attempt) # Exponential backoff
57
+ logger.warning(
58
+ "HTTP %s request attempt %d to %s failed: %s. Retrying in %.1f seconds...",
59
+ method,
60
+ attempt + 1,
61
+ url,
62
+ e,
63
+ delay,
64
+ )
65
+ await asyncio.sleep(delay)
66
+ else:
67
+ logger.exception(
68
+ "HTTP %s request to %s failed after %d attempts. Last error: %s",
69
+ method,
70
+ url,
71
+ max_retries + 1,
72
+ e,
73
+ )
74
+ raise e
@@ -153,7 +153,7 @@ async def worker(
153
153
  raise UserTimeout(e) from e
154
154
  raise
155
155
 
156
- async with Runs.enter(run_id, run["thread_id"], main_loop) as done:
156
+ async with Runs.enter(run_id, run["thread_id"], main_loop, resumable) as done:
157
157
  # attempt the run
158
158
  try:
159
159
  if attempt > BG_JOB_MAX_RETRIES:
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "cryptography>=42.0.0,<45.0",
32
32
  "langgraph-sdk>=0.2.0",
33
33
  "cloudpickle>=3.0.0",
34
- "langgraph-runtime-inmem>=0.11.0,<0.12.0",
34
+ "langgraph-runtime-inmem>=0.12.0,<0.13.0",
35
35
  "truststore>=0.1",
36
36
  ]
37
37
 
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.11"
4
4
 
5
5
  [[package]]
@@ -381,7 +381,7 @@ wheels = [
381
381
 
382
382
  [[package]]
383
383
  name = "langgraph"
384
- version = "0.6.6"
384
+ version = "0.6.7"
385
385
  source = { registry = "https://pypi.org/simple" }
386
386
  dependencies = [
387
387
  { name = "langchain-core" },
@@ -391,9 +391,9 @@ dependencies = [
391
391
  { name = "pydantic" },
392
392
  { name = "xxhash" },
393
393
  ]
394
- sdist = { url = "https://files.pythonhosted.org/packages/02/2b/59f0b2985467ec84b006dd41ec31c0aae43a7f16722d5514292500b871c9/langgraph-0.6.6.tar.gz", hash = "sha256:e7d3cefacf356f8c01721b166b67b3bf581659d5361a3530f59ecd9b8448eca7", size = 465452, upload-time = "2025-08-20T04:02:13.915Z" }
394
+ sdist = { url = "https://files.pythonhosted.org/packages/56/85/36feb25062da40ca395f6c44d0232a672842e5421885101f6faf4670b670/langgraph-0.6.7.tar.gz", hash = "sha256:ba7fd17b8220142d6a4269b6038f2b3dcbcef42cd5ecf4a4c8d9b60b010830a6", size = 465534, upload-time = "2025-09-07T16:49:42.895Z" }
395
395
  wheels = [
396
- { url = "https://files.pythonhosted.org/packages/e4/ef/81fce0a80925cd89987aa641ff01573e3556a24f2d205112862a69df7fd3/langgraph-0.6.6-py3-none-any.whl", hash = "sha256:a2283a5236abba6c8307c1a485c04e8a0f0ffa2be770878782a7bf2deb8d7954", size = 153274, upload-time = "2025-08-20T04:02:12.251Z" },
396
+ { url = "https://files.pythonhosted.org/packages/67/06/f440922a58204dbfd10f7fdda0de0325529a159e9dc3d1038afe4b431a49/langgraph-0.6.7-py3-none-any.whl", hash = "sha256:c724dd8c24806b70faf4903e8e20c0234f8c0a356e0e96a88035cbecca9df2cf", size = 153329, upload-time = "2025-09-07T16:49:40.45Z" },
397
397
  ]
398
398
 
399
399
  [[package]]
@@ -509,15 +509,15 @@ wheels = [
509
509
 
510
510
  [[package]]
511
511
  name = "langgraph-cli"
512
- version = "0.4.0"
512
+ version = "0.4.2"
513
513
  source = { registry = "https://pypi.org/simple" }
514
514
  dependencies = [
515
515
  { name = "click" },
516
516
  { name = "langgraph-sdk" },
517
517
  ]
518
- sdist = { url = "https://files.pythonhosted.org/packages/56/b9/b9386748fee6061340a84c8eac33e3fb39861f9cbf95dd135e8d96399bca/langgraph_cli-0.4.0.tar.gz", hash = "sha256:0478989dd1d90c42449f0cc5e7ae6fc57e6031104a600195810d848dbc7f6fde", size = 734098, upload-time = "2025-08-26T14:23:21.011Z" }
518
+ sdist = { url = "https://files.pythonhosted.org/packages/86/f1/598f9e1784432d790a937de4c466ba8bed3d18ef6f56fe7394af6bc1f175/langgraph_cli-0.4.2.tar.gz", hash = "sha256:074d93a2ebb9c60629a83bc4c149e837bd09e51222d48daacb498299d818ee9f", size = 778645, upload-time = "2025-09-05T22:55:03.37Z" }
519
519
  wheels = [
520
- { url = "https://files.pythonhosted.org/packages/f0/19/dfa21a9fb7064f86e16e132d1e94244f7073c8b0382443fec792f6f8a5fa/langgraph_cli-0.4.0-py3-none-any.whl", hash = "sha256:a7bc91d4e6112c4e186a74113764290a61f6a7c3385e50f9aeb58c18551e83d6", size = 37212, upload-time = "2025-08-26T14:23:19.987Z" },
520
+ { url = "https://files.pythonhosted.org/packages/d3/35/92c5a0de3f08bbc245ba7c0b1d5f9a7edd025a1483bf4adde97864419825/langgraph_cli-0.4.2-py3-none-any.whl", hash = "sha256:d83b00f11f9840f153aeba5ad417b09cd7a5aa98ab4ad7f94e45fb089ed73785", size = 38045, upload-time = "2025-09-05T22:55:02.044Z" },
521
521
  ]
522
522
 
523
523
  [[package]]
@@ -568,20 +568,20 @@ dev = [
568
568
 
569
569
  [[package]]
570
570
  name = "langgraph-sdk"
571
- version = "0.2.5"
571
+ version = "0.2.6"
572
572
  source = { registry = "https://pypi.org/simple" }
573
573
  dependencies = [
574
574
  { name = "httpx" },
575
575
  { name = "orjson" },
576
576
  ]
577
- sdist = { url = "https://files.pythonhosted.org/packages/42/72/37943b367f480b4761c052e86b0c380ecb928cdccf1d5c85d569e036c06f/langgraph_sdk-0.2.5.tar.gz", hash = "sha256:b28aa0f485811388465ac5d2a19d728ab84b59a8900cdfcf0f4e8177802cbf62", size = 79816, upload-time = "2025-09-03T00:51:14.285Z" }
577
+ sdist = { url = "https://files.pythonhosted.org/packages/55/35/a1caf4fdb725adec30f1e9562f218524a92d8b675deb97be653687f086ee/langgraph_sdk-0.2.6.tar.gz", hash = "sha256:7db27cd86d1231fa614823ff416fcd2541b5565ad78ae950f31ae96d7af7c519", size = 80346, upload-time = "2025-09-04T01:51:11.262Z" }
578
578
  wheels = [
579
- { url = "https://files.pythonhosted.org/packages/f1/2c/9e5a0259601253f89f8b50095a6a2541ff6beb9041c92e64bd8e05e77c29/langgraph_sdk-0.2.5-py3-none-any.whl", hash = "sha256:a837a7a3c5e16ba55a3952037f9d8e247d3e001e9a0e3b07aacef55861e5dc58", size = 54052, upload-time = "2025-09-03T00:51:13.057Z" },
579
+ { url = "https://files.pythonhosted.org/packages/c6/d2/c5fac919601b27a0af5df0bde46e7f1361d5e04505e404b75bed45d21fc8/langgraph_sdk-0.2.6-py3-none-any.whl", hash = "sha256:477216b573b8177bbd849f4c754782a81279fbbd88bfadfeda44422d14b18b08", size = 54565, upload-time = "2025-09-04T01:51:10.044Z" },
580
580
  ]
581
581
 
582
582
  [[package]]
583
583
  name = "langsmith"
584
- version = "0.4.23"
584
+ version = "0.4.26"
585
585
  source = { registry = "https://pypi.org/simple" }
586
586
  dependencies = [
587
587
  { name = "httpx" },
@@ -592,9 +592,9 @@ dependencies = [
592
592
  { name = "requests-toolbelt" },
593
593
  { name = "zstandard" },
594
594
  ]
595
- sdist = { url = "https://files.pythonhosted.org/packages/26/59/1f92c84585da3bad6d76050f32811198dd2bb4e6a0a63f7b0041b75344ee/langsmith-0.4.23.tar.gz", hash = "sha256:d8af9c6bf69c377a5a0a1c56bd742ab6acfce794e3c4a6993b08e76ee2397998", size = 939141, upload-time = "2025-09-02T22:06:38.606Z" }
595
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/54/6e919bbad4761bc45805f623c1f75582388468e081367d4cc43a3fbd20f9/langsmith-0.4.26.tar.gz", hash = "sha256:1fab1f52c40a374ce7f391085bffb10511a9fd5035c1d6feebb222e1faea5845", size = 955076, upload-time = "2025-09-08T02:24:56.014Z" }
596
596
  wheels = [
597
- { url = "https://files.pythonhosted.org/packages/3c/63/c132d3e356702668bf3103771e2398fee963c22715a88501d55361761f8f/langsmith-0.4.23-py3-none-any.whl", hash = "sha256:2b7cc692c537d5e48c2932277a69c35a89ff5b153828281194e2af34aeda985f", size = 378757, upload-time = "2025-09-02T22:06:36.621Z" },
597
+ { url = "https://files.pythonhosted.org/packages/b9/e8/5ec22fbe4323564c947eaebe4a3b010f1e0c80e8cbddb30fd5889007c4cb/langsmith-0.4.26-py3-none-any.whl", hash = "sha256:86eb83c21a1c3396eb2c7e02db4173fceda290ad8e1b7468ed0022c6aae2a220", size = 383852, upload-time = "2025-09-08T02:24:53.773Z" },
598
598
  ]
599
599
 
600
600
  [[package]]
@@ -907,7 +907,7 @@ wheels = [
907
907
 
908
908
  [[package]]
909
909
  name = "pytest"
910
- version = "8.4.1"
910
+ version = "8.4.2"
911
911
  source = { registry = "https://pypi.org/simple" }
912
912
  dependencies = [
913
913
  { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -916,9 +916,9 @@ dependencies = [
916
916
  { name = "pluggy" },
917
917
  { name = "pygments" },
918
918
  ]
919
- sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
919
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
920
920
  wheels = [
921
- { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
921
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
922
922
  ]
923
923
 
924
924
  [[package]]
@@ -1033,28 +1033,28 @@ wheels = [
1033
1033
 
1034
1034
  [[package]]
1035
1035
  name = "ruff"
1036
- version = "0.12.11"
1037
- source = { registry = "https://pypi.org/simple" }
1038
- sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" }
1039
- wheels = [
1040
- { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" },
1041
- { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" },
1042
- { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" },
1043
- { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" },
1044
- { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" },
1045
- { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" },
1046
- { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" },
1047
- { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" },
1048
- { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" },
1049
- { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" },
1050
- { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" },
1051
- { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" },
1052
- { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" },
1053
- { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" },
1054
- { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" },
1055
- { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" },
1056
- { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" },
1057
- { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" },
1036
+ version = "0.12.12"
1037
+ source = { registry = "https://pypi.org/simple" }
1038
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" }
1039
+ wheels = [
1040
+ { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" },
1041
+ { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" },
1042
+ { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" },
1043
+ { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" },
1044
+ { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" },
1045
+ { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" },
1046
+ { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" },
1047
+ { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" },
1048
+ { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" },
1049
+ { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" },
1050
+ { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" },
1051
+ { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" },
1052
+ { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" },
1053
+ { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" },
1054
+ { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" },
1055
+ { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" },
1056
+ { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" },
1057
+ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" },
1058
1058
  ]
1059
1059
 
1060
1060
  [[package]]
@@ -1 +0,0 @@
1
- __version__ = "0.4.11"
@@ -1,58 +0,0 @@
1
- import asyncio
2
- import time
3
- from collections import OrderedDict
4
- from typing import Generic, TypeVar
5
-
6
- T = TypeVar("T")
7
-
8
-
9
- class LRUCache(Generic[T]):
10
- """LRU cache with TTL support."""
11
-
12
- def __init__(self, max_size: int = 1000, ttl: float = 60):
13
- self._cache: OrderedDict[str, tuple[T, float]] = OrderedDict()
14
- self._max_size = max_size if max_size > 0 else 1000
15
- self._ttl = ttl
16
-
17
- def _get_time(self) -> float:
18
- """Get current time, using loop.time() if available for better performance."""
19
- try:
20
- return asyncio.get_event_loop().time()
21
- except RuntimeError:
22
- return time.monotonic()
23
-
24
- def get(self, key: str) -> T | None:
25
- """Get item from cache, returning None if expired or not found."""
26
- if key not in self._cache:
27
- return None
28
-
29
- value, timestamp = self._cache[key]
30
- if self._get_time() - timestamp >= self._ttl:
31
- # Expired, remove and return None
32
- del self._cache[key]
33
- return None
34
-
35
- # Move to end (most recently used)
36
- self._cache.move_to_end(key)
37
- return value
38
-
39
- def set(self, key: str, value: T) -> None:
40
- """Set item in cache, evicting old entries if needed."""
41
- # Remove if already exists (to update timestamp)
42
- if key in self._cache:
43
- del self._cache[key]
44
-
45
- # Evict oldest entries if needed
46
- while len(self._cache) >= self._max_size:
47
- self._cache.popitem(last=False) # Remove oldest (FIFO)
48
-
49
- # Add new entry
50
- self._cache[key] = (value, self._get_time())
51
-
52
- def size(self) -> int:
53
- """Return current cache size."""
54
- return len(self._cache)
55
-
56
- def clear(self) -> None:
57
- """Clear all entries from cache."""
58
- self._cache.clear()
File without changes
File without changes
File without changes