langgraph-api 0.2.129__tar.gz → 0.2.132__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.
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/Makefile +2 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/PKG-INFO +2 -2
- langgraph_api-0.2.132/benchmark/.gitignore +10 -0
- langgraph_api-0.2.132/benchmark/Makefile +16 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/README.md +4 -2
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/burst.js +2 -1
- langgraph_api-0.2.132/benchmark/graphs.js +236 -0
- langgraph_api-0.2.132/benchmark/package.json +17 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/ramp.js +28 -13
- langgraph_api-0.2.132/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/assistants.py +6 -5
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/meta.py +3 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/openapi.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/runs.py +13 -10
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/ui.py +2 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/asgi_transport.py +2 -2
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/asyncio.py +10 -8
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/custom.py +9 -4
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/client.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/cli.py +5 -4
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/config.py +2 -0
- langgraph_api-0.2.132/langgraph_api/executor_entrypoint.py +23 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/graph.py +25 -9
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/http.py +10 -7
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/http_metrics.py +4 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/base.py +0 -3
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/build.mts +11 -2
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/client.http.mts +2 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/client.mts +15 -11
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/remote.py +22 -12
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/preload.mjs +9 -1
- langgraph_api-0.2.132/langgraph_api/js/src/utils/files.mts +7 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/sse.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/logging.py +3 -3
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/http_logger.py +4 -3
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/models/run.py +20 -15
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/patch.py +2 -2
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/queue_entrypoint.py +33 -18
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/route.py +7 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/schema.py +20 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/serde.py +32 -5
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/server.py +5 -3
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/state.py +8 -8
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/store.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/stream.py +35 -20
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/traceblock.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/__init__.py +21 -5
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/config.py +13 -4
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/future.py +1 -1
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/headers.py +22 -5
- langgraph_api-0.2.132/langgraph_api/utils/uuids.py +87 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/webhook.py +20 -20
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/worker.py +36 -9
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/openapi.json +2 -2
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/pyproject.toml +9 -11
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/scripts/create_license.py +4 -3
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/uv.lock +146 -140
- langgraph_api-0.2.129/benchmark/.gitignore +0 -3
- langgraph_api-0.2.129/benchmark/Makefile +0 -9
- langgraph_api-0.2.129/langgraph_api/__init__.py +0 -1
- langgraph_api-0.2.129/langgraph_api/js/src/utils/files.mts +0 -4
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/.gitignore +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/LICENSE +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/README.md +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/weather.js +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/constraints.txt +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/forbidden.txt +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/healthcheck.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/command.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/cache.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.2.129 → langgraph_api-0.2.132}/logging.json +0 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
lint:
|
|
6
6
|
uv run ruff check .
|
|
7
7
|
uv run ruff format . --diff
|
|
8
|
+
uvx ty check --exclude "**/pb/**" --exclude "**/*_test.py" --exclude "**/test_*.py" --exclude "**/tests/**" --exclude "venv/**" --exclude ".venv/**" --exclude "build/**" --exclude "dist/**" .
|
|
8
9
|
|
|
9
10
|
format:
|
|
10
11
|
uv run ruff check --fix .
|
|
@@ -113,4 +114,4 @@ start-auth-fastapi-jwt:
|
|
|
113
114
|
VERSION_KIND ?= patch
|
|
114
115
|
|
|
115
116
|
bump-version:
|
|
116
|
-
uv run --with hatch hatch version $(VERSION_KIND)
|
|
117
|
+
uv run --with hatch hatch version $(VERSION_KIND)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.132
|
|
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.7,>=0.6.
|
|
14
|
+
Requires-Dist: langgraph-runtime-inmem<0.7,>=0.6.13
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Benchmark commands
|
|
2
|
+
benchmark-burst:
|
|
3
|
+
k6 run burst.js
|
|
4
|
+
|
|
5
|
+
benchmark-ramp:
|
|
6
|
+
k6 run --out json=raw_data_$(shell date +%Y-%m-%dT%H-%M-%S).json ramp.js
|
|
7
|
+
|
|
8
|
+
benchmark-charts:
|
|
9
|
+
npm install
|
|
10
|
+
node graphs.js $(shell ls -t raw_data_*.json | head -1) true
|
|
11
|
+
|
|
12
|
+
benchmark-clean:
|
|
13
|
+
rm -f results_*.json summary_*.json raw_data_*.json *_chart_*.png
|
|
14
|
+
|
|
15
|
+
benchmark-clean-charts:
|
|
16
|
+
rm -f *_chart_*.png
|
|
@@ -12,8 +12,8 @@ There are two modes of testing available:
|
|
|
12
12
|
BURST_SIZE - How many requests to run. Default: 100
|
|
13
13
|
2. `Ramp` - Scale up the number of /run/wait requests and then plateau.
|
|
14
14
|
Available Params:
|
|
15
|
-
LOAD_SIZE - How much traffic to ramp up over a 60s period. Default:
|
|
16
|
-
LEVELS - The number of times to ramp up. Default:
|
|
15
|
+
LOAD_SIZE - How much traffic to ramp up over a 60s period. Default: 500
|
|
16
|
+
LEVELS - The number of times to ramp up. Default: 2
|
|
17
17
|
PLATEAU_DURATION - How long to sustain the max level of traffic in seconds. Default: 300
|
|
18
18
|
|
|
19
19
|
### Agent
|
|
@@ -65,6 +65,8 @@ make benchmark-clean
|
|
|
65
65
|
|
|
66
66
|
Summary results are written to stdout and persisted in a summary_burst file. More detailed results for the same burst are persisted in a results_burst file.
|
|
67
67
|
|
|
68
|
+
Charts can be created from the run locally using the `make benchmark-charts` command.
|
|
69
|
+
|
|
68
70
|
## Resources
|
|
69
71
|
|
|
70
72
|
- [K6 Documentation](https://k6.io/docs/)
|
|
@@ -139,7 +139,8 @@ export default function() {
|
|
|
139
139
|
export function setup() {
|
|
140
140
|
console.log(`Starting burst benchmark`);
|
|
141
141
|
console.log(`Running on pod: ${__ENV.POD_NAME || 'local'}`);
|
|
142
|
-
console.log(`
|
|
142
|
+
console.log(`Running with the following burst config: burst size ${BURST_SIZE}`);
|
|
143
|
+
console.log(`Running with the following agent config: data size ${DATA_SIZE}, delay ${DELAY}, expand ${EXPAND}, mode ${MODE}`);
|
|
143
144
|
|
|
144
145
|
return { startTime: new Date().toISOString() };
|
|
145
146
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const QuickChart = require('quickchart-js');
|
|
5
|
+
const { plot } = require('nodeplotlib');
|
|
6
|
+
|
|
7
|
+
// Function to save chart using Quickchart
|
|
8
|
+
async function saveChartWithQuickchart(chartData, chartLayout, filename) {
|
|
9
|
+
const chart = new QuickChart();
|
|
10
|
+
|
|
11
|
+
chart.setWidth(800);
|
|
12
|
+
chart.setHeight(500);
|
|
13
|
+
chart.setFormat('png');
|
|
14
|
+
|
|
15
|
+
const config = {
|
|
16
|
+
type: 'line',
|
|
17
|
+
data: {
|
|
18
|
+
labels: chartData[0].x,
|
|
19
|
+
datasets: chartData.map(metric => {
|
|
20
|
+
return {
|
|
21
|
+
label: metric.name,
|
|
22
|
+
data: metric.y
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
chart.setConfig(config);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const imageBuffer = await chart.toBinary();
|
|
32
|
+
fs.writeFileSync(filename, imageBuffer);
|
|
33
|
+
console.log(`Chart saved to ${filename}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Error saving chart to ${filename}:`, error.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function to generate charts from benchmark results
|
|
41
|
+
*
|
|
42
|
+
* Graphs that we generate:
|
|
43
|
+
* - VU scaling over time
|
|
44
|
+
* - Requests over time, broken into success and failure by reason
|
|
45
|
+
* - Average run duration over time
|
|
46
|
+
*
|
|
47
|
+
* Useful but currently in deployment dashboard:
|
|
48
|
+
* - Connection use over time (postgres, redis)
|
|
49
|
+
* - IOPS over time (postgres, redis)
|
|
50
|
+
* - Workers in use over time
|
|
51
|
+
*
|
|
52
|
+
* Dashboard: https://smith.langchain.com/o/ebbaf2eb-769b-4505-aca2-d11de10372a4/host/deployments/a23f03ff-6d4d-4efd-8149-bb5a7f3b95cf?tab=2&paginationModel=%7B%22pageIndex%22%3A0%2C%22pageSize%22%3A10%7D#
|
|
53
|
+
*/
|
|
54
|
+
async function generateCharts(rawDataFile, displayInBrowser = false) {
|
|
55
|
+
console.log("Generating charts for", rawDataFile);
|
|
56
|
+
const aggregatedData = {};
|
|
57
|
+
|
|
58
|
+
// Read the results and summary files
|
|
59
|
+
const fileStream = fs.createReadStream(rawDataFile, 'utf8');
|
|
60
|
+
const rl = readline.createInterface({
|
|
61
|
+
input: fileStream,
|
|
62
|
+
crlfDelay: Infinity
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for await (const line of rl) {
|
|
66
|
+
const entry = JSON.parse(line);
|
|
67
|
+
|
|
68
|
+
// Always written before the first point for that metric
|
|
69
|
+
if (entry.type === 'Metric') {
|
|
70
|
+
const metricName = entry.metric;
|
|
71
|
+
aggregatedData[metricName] = {};
|
|
72
|
+
} else if (entry.type === 'Point') {
|
|
73
|
+
const metricName = entry.metric;
|
|
74
|
+
const metricValue = entry.data.value;
|
|
75
|
+
const timestamp = Date.parse(entry.data.time);
|
|
76
|
+
|
|
77
|
+
// Round timestamp to 10-second intervals
|
|
78
|
+
const roundedTimestamp = Math.floor(timestamp / 10000) * 10000;
|
|
79
|
+
|
|
80
|
+
if (!aggregatedData[metricName][roundedTimestamp]) {
|
|
81
|
+
aggregatedData[metricName][roundedTimestamp] = [];
|
|
82
|
+
}
|
|
83
|
+
aggregatedData[metricName][roundedTimestamp].push(metricValue);
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`Unexpected row type: ${entry.type}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Convert aggregated data to final format with averages
|
|
90
|
+
const finalData = {};
|
|
91
|
+
for (const [metricName, timestampBuckets] of Object.entries(aggregatedData)) {
|
|
92
|
+
finalData[metricName] = [];
|
|
93
|
+
for (const [timestamp, values] of Object.entries(timestampBuckets)) {
|
|
94
|
+
const average = values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
95
|
+
const max = Math.max(...values);
|
|
96
|
+
finalData[metricName].push({
|
|
97
|
+
timestamp: timestamp,
|
|
98
|
+
average: average,
|
|
99
|
+
max: max,
|
|
100
|
+
count: values.length
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Sort by timestamp
|
|
104
|
+
finalData[metricName].sort((a, b) => a.timestamp - b.timestamp);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create the charts
|
|
108
|
+
// We always get a http_reqs metric as one of the first
|
|
109
|
+
const firstTimestamp = finalData['http_reqs'][0].timestamp;
|
|
110
|
+
|
|
111
|
+
// Failed requests won't always be present
|
|
112
|
+
const data = [
|
|
113
|
+
{
|
|
114
|
+
x: finalData['http_reqs'].map(d => (d.timestamp - firstTimestamp) / 1000),
|
|
115
|
+
y: finalData['http_reqs'].map(d => d.count),
|
|
116
|
+
type: 'scatter',
|
|
117
|
+
mode: 'lines',
|
|
118
|
+
name: 'Total Requests',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
x: finalData['successful_runs'].map(d => (d.timestamp - firstTimestamp) / 1000),
|
|
122
|
+
y: finalData['successful_runs'].map(d => d.count),
|
|
123
|
+
type: 'scatter',
|
|
124
|
+
mode: 'lines',
|
|
125
|
+
name: 'Success Rate',
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
if (finalData['failed_runs']) {
|
|
129
|
+
data.push({
|
|
130
|
+
x: finalData['failed_runs'].map(d => (d.timestamp - firstTimestamp) / 1000),
|
|
131
|
+
y: finalData['failed_runs'].map(d => d.count),
|
|
132
|
+
type: 'scatter',
|
|
133
|
+
mode: 'lines',
|
|
134
|
+
name: 'Failed Requests',
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const requestsChart = {
|
|
139
|
+
data,
|
|
140
|
+
layout: {
|
|
141
|
+
title: {
|
|
142
|
+
text: 'Success Rate Over Time',
|
|
143
|
+
},
|
|
144
|
+
xaxis: {
|
|
145
|
+
title: 'Time (10 second intervals)',
|
|
146
|
+
},
|
|
147
|
+
yaxis: {
|
|
148
|
+
title: 'Call Counts',
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const runDurationChart = {
|
|
154
|
+
data: [
|
|
155
|
+
{
|
|
156
|
+
x: finalData['run_duration'].map(d => (d.timestamp - firstTimestamp) / 1000),
|
|
157
|
+
y: finalData['run_duration'].map(d => d.average),
|
|
158
|
+
type: 'scatter',
|
|
159
|
+
mode: 'lines',
|
|
160
|
+
name: 'Run Duration',
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
layout: {
|
|
164
|
+
title: {
|
|
165
|
+
text: 'Run Duration Over Time',
|
|
166
|
+
},
|
|
167
|
+
xaxis: {
|
|
168
|
+
title: 'Time (10 second intervals)',
|
|
169
|
+
},
|
|
170
|
+
yaxis: {
|
|
171
|
+
title: 'Run Duration (ms)',
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const vusChart = {
|
|
177
|
+
data: [
|
|
178
|
+
{
|
|
179
|
+
x: finalData['vus'].map(d => (d.timestamp - firstTimestamp) / 1000),
|
|
180
|
+
y: finalData['vus'].map(d => d.max),
|
|
181
|
+
type: 'scatter',
|
|
182
|
+
mode: 'lines',
|
|
183
|
+
name: 'Concurrent Virtual Users',
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
layout: {
|
|
187
|
+
title: {
|
|
188
|
+
text: 'Concurrent Virtual Users over Time',
|
|
189
|
+
},
|
|
190
|
+
xaxis: {
|
|
191
|
+
title: 'Time (10 second intervals)',
|
|
192
|
+
},
|
|
193
|
+
yaxis: {
|
|
194
|
+
title: 'VUs (max)',
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Always save charts as images using Quickchart
|
|
200
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
201
|
+
const requestsImagePath = path.join(__dirname, `requests_chart_${timestamp}.png`);
|
|
202
|
+
const runDurationImagePath = path.join(__dirname, `run_duration_chart_${timestamp}.png`);
|
|
203
|
+
const vusImagePath = path.join(__dirname, `vus_chart_${timestamp}.png`);
|
|
204
|
+
|
|
205
|
+
// Save charts using Quickchart
|
|
206
|
+
await saveChartWithQuickchart(requestsChart.data, requestsChart.layout, requestsImagePath);
|
|
207
|
+
await saveChartWithQuickchart(runDurationChart.data, runDurationChart.layout, runDurationImagePath);
|
|
208
|
+
await saveChartWithQuickchart(vusChart.data, vusChart.layout, vusImagePath);
|
|
209
|
+
|
|
210
|
+
// Display in CLI if requested
|
|
211
|
+
if (displayInBrowser) {
|
|
212
|
+
plot(requestsChart.data, requestsChart.layout);
|
|
213
|
+
plot(runDurationChart.data, runDurationChart.layout);
|
|
214
|
+
plot(vusChart.data, vusChart.layout);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`Charts generated: ${vusImagePath}, ${requestsImagePath}, ${runDurationImagePath}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// CLI usage
|
|
221
|
+
if (require.main === module) {
|
|
222
|
+
const [,, rawDataFile, displayInBrowser] = process.argv;
|
|
223
|
+
|
|
224
|
+
if (!rawDataFile) {
|
|
225
|
+
console.error('Usage: node graphs.js <raw-data-file>');
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
generateCharts(rawDataFile, displayInBrowser)
|
|
230
|
+
.catch(error => {
|
|
231
|
+
console.error('Failed to generate charts:', error);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { generateCharts };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "benchmark-charts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Chart generation for K6 benchmark results",
|
|
5
|
+
"main": "graphs.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"generate-charts": "node graphs.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"nodeplotlib": "^1.0.4",
|
|
11
|
+
"quickchart-js": "^3.1.3"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=14.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -18,9 +18,10 @@ const BASE_URL = __ENV.BASE_URL || 'http://localhost:9123';
|
|
|
18
18
|
const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
|
|
19
19
|
|
|
20
20
|
// Params for the runner
|
|
21
|
-
const LOAD_SIZE = parseInt(__ENV.LOAD_SIZE || '
|
|
22
|
-
const LEVELS = parseInt(__ENV.LEVELS || '
|
|
21
|
+
const LOAD_SIZE = parseInt(__ENV.LOAD_SIZE || '500');
|
|
22
|
+
const LEVELS = parseInt(__ENV.LEVELS || '2');
|
|
23
23
|
const PLATEAU_DURATION = parseInt(__ENV.PLATEAU_DURATION || '300');
|
|
24
|
+
const STATEFUL = __ENV.STATEFUL === 'true';
|
|
24
25
|
|
|
25
26
|
// Params for the agent
|
|
26
27
|
const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
|
|
@@ -33,7 +34,7 @@ for (let i = 1; i <= LEVELS; i++) {
|
|
|
33
34
|
stages.push({ duration: '60s', target: LOAD_SIZE * i });
|
|
34
35
|
}
|
|
35
36
|
stages.push({ duration: `${PLATEAU_DURATION}s`, target: LOAD_SIZE * LEVELS});
|
|
36
|
-
stages.push({ duration: '
|
|
37
|
+
stages.push({ duration: '60s', target: 0 }); // Ramp down
|
|
37
38
|
|
|
38
39
|
// Test configuration
|
|
39
40
|
export let options = {
|
|
@@ -46,9 +47,10 @@ export let options = {
|
|
|
46
47
|
},
|
|
47
48
|
},
|
|
48
49
|
thresholds: {
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
'
|
|
50
|
+
// These are the first set of goals I'd like to hit. To get the job just working and ramp on main, leaving them off for now.
|
|
51
|
+
// 'run_duration': ['p(95)<10000'], // 95% of runs should complete within 10s
|
|
52
|
+
// 'successful_runs': ['count>100000'], // At least 100,000 successful runs
|
|
53
|
+
// 'http_req_failed': ['rate<0.05'], // Error rate should be less than 5%
|
|
52
54
|
},
|
|
53
55
|
};
|
|
54
56
|
|
|
@@ -75,10 +77,21 @@ export default function() {
|
|
|
75
77
|
config: {
|
|
76
78
|
recursion_limit: EXPAND + 2,
|
|
77
79
|
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// If the request is stateful, create a thread first and use it in the url
|
|
83
|
+
let url = `${BASE_URL}/runs/wait`;
|
|
84
|
+
if (STATEFUL) {
|
|
85
|
+
const thread = http.post(`${BASE_URL}/threads`, payload, {
|
|
86
|
+
headers,
|
|
87
|
+
timeout: '120s' // k6 request timeout slightly longer than the server timeout
|
|
78
88
|
});
|
|
89
|
+
const threadId = thread.json().thread_id;
|
|
90
|
+
url = `${BASE_URL}/threads/${threadId}/runs/wait`;
|
|
91
|
+
}
|
|
79
92
|
|
|
80
93
|
// Make a single request to the wait endpoint
|
|
81
|
-
const response = http.post(
|
|
94
|
+
const response = http.post(url, payload, {
|
|
82
95
|
headers,
|
|
83
96
|
timeout: '120s' // k6 request timeout slightly longer than the server timeout
|
|
84
97
|
});
|
|
@@ -141,9 +154,10 @@ export default function() {
|
|
|
141
154
|
export function setup() {
|
|
142
155
|
console.log(`Starting ramp benchmark`);
|
|
143
156
|
console.log(`Running on pod: ${__ENV.POD_NAME || 'local'}`);
|
|
144
|
-
console.log(`Running ${
|
|
157
|
+
console.log(`Running with the following ramp config: load size ${LOAD_SIZE}, levels ${LEVELS}, plateau duration ${PLATEAU_DURATION}, stateful ${STATEFUL}`);
|
|
158
|
+
console.log(`Running with the following agent config: data size ${DATA_SIZE}, delay ${DELAY}, expand ${EXPAND}, mode ${MODE}`);
|
|
145
159
|
|
|
146
|
-
return { startTime: new Date().toISOString() };
|
|
160
|
+
return { startTime: new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '') };
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
// Handle summary
|
|
@@ -152,13 +166,14 @@ export function handleSummary(data) {
|
|
|
152
166
|
|
|
153
167
|
// Create summary information with aggregated metrics
|
|
154
168
|
const summary = {
|
|
155
|
-
|
|
169
|
+
startTimestamp: data.setup_data.startTime,
|
|
170
|
+
endTimestamp: timestamp,
|
|
156
171
|
metrics: {
|
|
157
|
-
totalRuns: data.metrics.successful_runs.values.count + data.metrics.failed_runs
|
|
172
|
+
totalRuns: data.metrics.successful_runs.values.count + (data.metrics.failed_runs?.values?.count || 0),
|
|
158
173
|
successfulRuns: data.metrics.successful_runs.values.count,
|
|
159
|
-
failedRuns: data.metrics.failed_runs
|
|
174
|
+
failedRuns: data.metrics.failed_runs?.values?.count || 0,
|
|
160
175
|
successRate: data.metrics.successful_runs.values.count /
|
|
161
|
-
(data.metrics.successful_runs.values.count + data.metrics.failed_runs
|
|
176
|
+
(data.metrics.successful_runs.values.count + (data.metrics.failed_runs?.values?.count || 0)) * 100,
|
|
162
177
|
averageDuration: data.metrics.run_duration.values.avg / 1000, // in seconds
|
|
163
178
|
p95Duration: data.metrics.run_duration.values["p(95)"] / 1000, // in seconds
|
|
164
179
|
errors: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.132"
|
|
@@ -61,7 +61,8 @@ def _get_configurable_jsonschema(graph: Pregel) -> dict:
|
|
|
61
61
|
in favor of graph.get_context_jsonschema().
|
|
62
62
|
"""
|
|
63
63
|
# Otherwise, use the config_schema method.
|
|
64
|
-
|
|
64
|
+
# TODO: Remove this when we no longer support langgraph < 0.6
|
|
65
|
+
config_schema = graph.config_schema() # type: ignore[deprecated]
|
|
65
66
|
model_fields = getattr(config_schema, "model_fields", None) or getattr(
|
|
66
67
|
config_schema, "__fields__", None
|
|
67
68
|
)
|
|
@@ -87,11 +88,11 @@ def _state_jsonschema(graph: Pregel) -> dict | None:
|
|
|
87
88
|
for k in graph.stream_channels_list:
|
|
88
89
|
v = graph.channels[k]
|
|
89
90
|
try:
|
|
90
|
-
create_model(k, __root__=(v.UpdateType, None)).
|
|
91
|
+
create_model(k, __root__=(v.UpdateType, None)).model_json_schema()
|
|
91
92
|
fields[k] = (v.UpdateType, None)
|
|
92
93
|
except Exception:
|
|
93
94
|
fields[k] = (Any, None)
|
|
94
|
-
return create_model(graph.get_name("State"), **fields).
|
|
95
|
+
return create_model(graph.get_name("State"), **fields).model_json_schema()
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def _graph_schemas(graph: Pregel) -> dict:
|
|
@@ -132,7 +133,7 @@ def _graph_schemas(graph: Pregel) -> dict:
|
|
|
132
133
|
logger.warning(
|
|
133
134
|
f"Failed to get context schema for graph {graph.name} with error: `{str(e)}`"
|
|
134
135
|
)
|
|
135
|
-
context_schema = graph.config_schema()
|
|
136
|
+
context_schema = graph.config_schema() # type: ignore[deprecated]
|
|
136
137
|
else:
|
|
137
138
|
context_schema = None
|
|
138
139
|
|
|
@@ -366,7 +367,7 @@ async def patch_assistant(
|
|
|
366
367
|
|
|
367
368
|
|
|
368
369
|
@retry_db
|
|
369
|
-
async def delete_assistant(request: ApiRequest) ->
|
|
370
|
+
async def delete_assistant(request: ApiRequest) -> Response:
|
|
370
371
|
"""Delete an assistant by ID."""
|
|
371
372
|
assistant_id = request.path_params["assistant_id"]
|
|
372
373
|
validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
1
3
|
import langgraph.version
|
|
2
4
|
from starlette.responses import JSONResponse, PlainTextResponse
|
|
3
5
|
|
|
@@ -43,7 +45,7 @@ async def meta_metrics(request: ApiRequest):
|
|
|
43
45
|
|
|
44
46
|
# collect stats
|
|
45
47
|
metrics = get_metrics()
|
|
46
|
-
worker_metrics = metrics["workers"]
|
|
48
|
+
worker_metrics = cast(dict[str, int], metrics["workers"])
|
|
47
49
|
workers_max = worker_metrics["max"]
|
|
48
50
|
workers_active = worker_metrics["active"]
|
|
49
51
|
workers_available = worker_metrics["available"]
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import AsyncIterator
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Literal, cast
|
|
4
4
|
|
|
5
5
|
import orjson
|
|
6
|
-
from langgraph.checkpoint.base.id import uuid6
|
|
7
6
|
from starlette.exceptions import HTTPException
|
|
8
7
|
from starlette.responses import Response, StreamingResponse
|
|
9
8
|
|
|
@@ -12,7 +11,7 @@ from langgraph_api.asyncio import ValueEvent, aclosing
|
|
|
12
11
|
from langgraph_api.models.run import create_valid_run
|
|
13
12
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
14
13
|
from langgraph_api.sse import EventSourceResponse
|
|
15
|
-
from langgraph_api.utils import fetchone, get_pagination_headers, validate_uuid
|
|
14
|
+
from langgraph_api.utils import fetchone, get_pagination_headers, uuid7, validate_uuid
|
|
16
15
|
from langgraph_api.validation import (
|
|
17
16
|
CronCreate,
|
|
18
17
|
CronSearch,
|
|
@@ -92,7 +91,7 @@ async def stream_run(
|
|
|
92
91
|
thread_id = request.path_params["thread_id"]
|
|
93
92
|
payload = await request.json(RunCreateStateful)
|
|
94
93
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
95
|
-
run_id =
|
|
94
|
+
run_id = uuid7()
|
|
96
95
|
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
97
96
|
|
|
98
97
|
try:
|
|
@@ -132,7 +131,7 @@ async def stream_run_stateless(
|
|
|
132
131
|
"""Create a stateless run."""
|
|
133
132
|
payload = await request.json(RunCreateStateless)
|
|
134
133
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
135
|
-
run_id =
|
|
134
|
+
run_id = uuid7()
|
|
136
135
|
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
137
136
|
|
|
138
137
|
try:
|
|
@@ -173,7 +172,7 @@ async def wait_run(request: ApiRequest):
|
|
|
173
172
|
thread_id = request.path_params["thread_id"]
|
|
174
173
|
payload = await request.json(RunCreateStateful)
|
|
175
174
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
176
|
-
run_id =
|
|
175
|
+
run_id = uuid7()
|
|
177
176
|
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
178
177
|
|
|
179
178
|
try:
|
|
@@ -255,7 +254,7 @@ async def wait_run_stateless(request: ApiRequest):
|
|
|
255
254
|
"""Create a stateless run, wait for the output."""
|
|
256
255
|
payload = await request.json(RunCreateStateless)
|
|
257
256
|
on_disconnect = payload.get("on_disconnect", "continue")
|
|
258
|
-
run_id =
|
|
257
|
+
run_id = uuid7()
|
|
259
258
|
sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
|
|
260
259
|
|
|
261
260
|
try:
|
|
@@ -425,7 +424,10 @@ async def cancel_run(
|
|
|
425
424
|
wait_str = request.query_params.get("wait", "false")
|
|
426
425
|
wait = wait_str.lower() in {"true", "yes", "1"}
|
|
427
426
|
action_str = request.query_params.get("action", "interrupt")
|
|
428
|
-
action =
|
|
427
|
+
action = cast(
|
|
428
|
+
Literal["interrupt", "rollback"],
|
|
429
|
+
action_str if action_str in {"interrupt", "rollback"} else "interrupt",
|
|
430
|
+
)
|
|
429
431
|
|
|
430
432
|
async with connect() as conn:
|
|
431
433
|
await Runs.cancel(
|
|
@@ -471,8 +473,9 @@ async def cancel_runs(
|
|
|
471
473
|
for rid in run_ids:
|
|
472
474
|
validate_uuid(rid, "Invalid run ID: must be a UUID")
|
|
473
475
|
action_str = request.query_params.get("action", "interrupt")
|
|
474
|
-
action
|
|
475
|
-
|
|
476
|
+
action = cast(
|
|
477
|
+
Literal["interrupt", "rollback"],
|
|
478
|
+
action_str if action_str in ("interrupt", "rollback") else "interrupt",
|
|
476
479
|
)
|
|
477
480
|
|
|
478
481
|
async with connect() as conn:
|
|
@@ -56,6 +56,8 @@ async def handle_ui(request: ApiRequest) -> Response:
|
|
|
56
56
|
|
|
57
57
|
# Use http:// protocol if accessing a localhost service
|
|
58
58
|
def is_host(needle: str) -> bool:
|
|
59
|
+
if not isinstance(host, str):
|
|
60
|
+
return False
|
|
59
61
|
return host.startswith(needle + ":") or host == needle
|
|
60
62
|
|
|
61
63
|
protocol = "http:" if is_host("localhost") or is_host("127.0.0.1") else ""
|
|
@@ -13,7 +13,7 @@ from httpx import AsyncByteStream, Request, Response
|
|
|
13
13
|
if typing.TYPE_CHECKING: # pragma: no cover
|
|
14
14
|
import asyncio
|
|
15
15
|
|
|
16
|
-
import trio
|
|
16
|
+
import trio # type: ignore[unresolved-import]
|
|
17
17
|
|
|
18
18
|
Event = asyncio.Event | trio.Event
|
|
19
19
|
|
|
@@ -37,7 +37,7 @@ def is_running_trio() -> bool:
|
|
|
37
37
|
|
|
38
38
|
def create_event() -> Event:
|
|
39
39
|
if is_running_trio():
|
|
40
|
-
import trio
|
|
40
|
+
import trio # type: ignore[unresolved-import]
|
|
41
41
|
|
|
42
42
|
return trio.Event()
|
|
43
43
|
|
|
@@ -119,7 +119,7 @@ def create_task(
|
|
|
119
119
|
|
|
120
120
|
def run_coroutine_threadsafe(
|
|
121
121
|
coro: Coroutine[Any, Any, T], ignore_exceptions: tuple[type[Exception], ...] = ()
|
|
122
|
-
) -> concurrent.futures.Future[T | None]:
|
|
122
|
+
) -> concurrent.futures.Future[T] | concurrent.futures.Future[None]:
|
|
123
123
|
if _MAIN_LOOP is None:
|
|
124
124
|
raise RuntimeError("No event loop set")
|
|
125
125
|
future = asyncio.run_coroutine_threadsafe(coro, _MAIN_LOOP)
|
|
@@ -226,7 +226,7 @@ def to_aiter(*args: T) -> AsyncIterator[T]:
|
|
|
226
226
|
V = TypeVar("V")
|
|
227
227
|
|
|
228
228
|
|
|
229
|
-
class aclosing(Generic[V], AbstractAsyncContextManager):
|
|
229
|
+
class aclosing(Generic[V], AbstractAsyncContextManager[V]):
|
|
230
230
|
"""Async context manager for safely finalizing an asynchronously cleaned-up
|
|
231
231
|
resource such as an async generator, calling its ``aclose()`` method.
|
|
232
232
|
|
|
@@ -255,14 +255,16 @@ class aclosing(Generic[V], AbstractAsyncContextManager):
|
|
|
255
255
|
await self.thing.aclose()
|
|
256
256
|
|
|
257
257
|
|
|
258
|
-
async def aclosing_aiter(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
258
|
+
async def aclosing_aiter(
|
|
259
|
+
aiterator: AsyncIterator[T],
|
|
260
|
+
) -> AsyncIterator[T]:
|
|
261
|
+
if hasattr(aiterator, "__aenter__"):
|
|
262
|
+
async with aiterator: # type: ignore[invalid-context-manager]
|
|
263
|
+
async for item in aiterator:
|
|
262
264
|
yield item
|
|
263
265
|
else:
|
|
264
|
-
async with aclosing(
|
|
265
|
-
async for item in
|
|
266
|
+
async with aclosing(aiterator):
|
|
267
|
+
async for item in aiterator:
|
|
266
268
|
yield item
|
|
267
269
|
|
|
268
270
|
|