@neat.is/core 0.6.2-dev.20260723 → 0.6.2

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.
package/dist/index.cjs CHANGED
@@ -5671,7 +5671,7 @@ function keywordArrayStrings(argsNode, key) {
5671
5671
  }
5672
5672
  return [];
5673
5673
  }
5674
- function collectApiRouterPrefixes(root) {
5674
+ function collectPythonRouterPrefixes(root) {
5675
5675
  const prefixes = /* @__PURE__ */ new Map();
5676
5676
  walk(root, (node) => {
5677
5677
  if (node.type !== "assignment") return;
@@ -5680,7 +5680,8 @@ function collectApiRouterPrefixes(root) {
5680
5680
  const fn = right.childForFieldName("function");
5681
5681
  if (!fn) return;
5682
5682
  const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
5683
- if (ctor !== "APIRouter") return;
5683
+ const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
5684
+ if (!prefixKey) return;
5684
5685
  const left = node.childForFieldName("left");
5685
5686
  if (!left || left.type !== "identifier") return;
5686
5687
  const args = right.childForFieldName("arguments");
@@ -5688,7 +5689,7 @@ function collectApiRouterPrefixes(root) {
5688
5689
  for (let i = 0; i < args.namedChildCount; i++) {
5689
5690
  const arg = args.namedChild(i);
5690
5691
  if (arg?.type !== "keyword_argument") continue;
5691
- if (arg.childForFieldName("name")?.text !== "prefix") continue;
5692
+ if (arg.childForFieldName("name")?.text !== prefixKey) continue;
5692
5693
  const val = arg.childForFieldName("value");
5693
5694
  const p = val ? pyStaticStringText(val) : null;
5694
5695
  if (p !== null) prefixes.set(left.text, p);
@@ -5696,9 +5697,9 @@ function collectApiRouterPrefixes(root) {
5696
5697
  });
5697
5698
  return prefixes;
5698
5699
  }
5699
- function fastapiRoutesFromSource(source, parser) {
5700
+ function pythonRoutesFromSource(source, parser, framework) {
5700
5701
  const tree = parseSource2(parser, source);
5701
- const prefixes = collectApiRouterPrefixes(tree.rootNode);
5702
+ const prefixes = collectPythonRouterPrefixes(tree.rootNode);
5702
5703
  const out = [];
5703
5704
  walk(tree.rootNode, (node) => {
5704
5705
  if (node.type !== "decorator") return;
@@ -5709,7 +5710,9 @@ function fastapiRoutesFromSource(source, parser) {
5709
5710
  const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5710
5711
  if (!method) return;
5711
5712
  const isVerb = FASTAPI_METHODS.has(method);
5712
- if (!isVerb && method !== "api_route") return;
5713
+ const isFlaskRoute = method === "route";
5714
+ const isApiRoute = method === "api_route";
5715
+ if (!isVerb && !isFlaskRoute && !isApiRoute) return;
5713
5716
  const args = call.childForFieldName("arguments");
5714
5717
  const first = args?.namedChild(0);
5715
5718
  if (!first || first.type !== "string") return;
@@ -5720,17 +5723,41 @@ function fastapiRoutesFromSource(source, parser) {
5720
5723
  const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5721
5724
  const line = node.startPosition.row + 1;
5722
5725
  if (isVerb) {
5723
- out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5726
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
5724
5727
  return;
5725
5728
  }
5726
5729
  const methods = keywordArrayStrings(args, "methods");
5727
- const list = methods.length > 0 ? methods : ["ALL"];
5730
+ const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
5728
5731
  for (const m of list) {
5732
+ out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
5733
+ }
5734
+ });
5735
+ return out;
5736
+ }
5737
+ function djangoRoutesFromSource(source, parser) {
5738
+ const tree = parseSource2(parser, source);
5739
+ const out = [];
5740
+ walk(tree.rootNode, (node) => {
5741
+ if (node.type !== "assignment") return;
5742
+ if (node.childForFieldName("left")?.text !== "urlpatterns") return;
5743
+ const list = node.childForFieldName("right");
5744
+ if (!list || list.type !== "list") return;
5745
+ for (let i = 0; i < list.namedChildCount; i++) {
5746
+ const el = list.namedChild(i);
5747
+ if (el?.type !== "call") continue;
5748
+ if (el.childForFieldName("function")?.text !== "path") continue;
5749
+ const args = el.childForFieldName("arguments");
5750
+ const first = args?.namedChild(0);
5751
+ if (first?.type !== "string") continue;
5752
+ const raw = pyStaticStringText(first);
5753
+ if (raw === null) continue;
5754
+ const second = args?.namedChild(1);
5755
+ if (second?.type === "call" && second.childForFieldName("function")?.text === "include") continue;
5729
5756
  out.push({
5730
- method: m === "ALL" ? "ALL" : m.toUpperCase(),
5731
- pathTemplate,
5732
- line,
5733
- framework: "fastapi"
5757
+ method: "ALL",
5758
+ pathTemplate: canonicalizeTemplate(raw),
5759
+ line: el.startPosition.row + 1,
5760
+ framework: "django"
5734
5761
  });
5735
5762
  }
5736
5763
  });
@@ -5751,7 +5778,10 @@ async function addRoutes(graph, services) {
5751
5778
  const hasHono = deps["hono"] !== void 0;
5752
5779
  const hasNext = deps["next"] !== void 0;
5753
5780
  const hasFastapi = deps["fastapi"] !== void 0;
5754
- if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
5781
+ const hasFlask = deps["flask"] !== void 0;
5782
+ const hasDjango = deps["django"] !== void 0;
5783
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi && !hasFlask && !hasDjango)
5784
+ continue;
5755
5785
  const files = await loadSourceFiles(service.dir);
5756
5786
  for (const file of files) {
5757
5787
  if (isTestPath(file.path)) continue;
@@ -5762,7 +5792,8 @@ async function addRoutes(graph, services) {
5762
5792
  let routes;
5763
5793
  try {
5764
5794
  if (isPy) {
5765
- routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5795
+ routes = hasFastapi || hasFlask ? pythonRoutesFromSource(file.content, pyParser, hasFastapi ? "fastapi" : "flask") : [];
5796
+ if (hasDjango) routes = routes.concat(djangoRoutesFromSource(file.content, pyParser));
5766
5797
  } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5767
5798
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5768
5799
  } else if (hasExpress || hasFastify || hasHono) {
@@ -10373,6 +10404,9 @@ var import_node_path52 = __toESM(require("path"), 1);
10373
10404
  var import_node_module = require("module");
10374
10405
  init_otel();
10375
10406
 
10407
+ // src/connectors/registry.ts
10408
+ init_cjs_shims();
10409
+
10376
10410
  // src/connectors/index.ts
10377
10411
  init_cjs_shims();
10378
10412
  var import_types36 = require("@neat.is/types");
@@ -10484,6 +10518,7 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
10484
10518
  }
10485
10519
  })();
10486
10520
  };
10521
+ tick();
10487
10522
  const interval = setInterval(tick, intervalMs);
10488
10523
  if (typeof interval.unref === "function") interval.unref();
10489
10524
  return () => {
@@ -10492,9 +10527,6 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
10492
10527
  };
10493
10528
  }
10494
10529
 
10495
- // src/connectors/registry.ts
10496
- init_cjs_shims();
10497
-
10498
10530
  // src/connectors/junction.ts
10499
10531
  init_cjs_shims();
10500
10532
  var buckets = /* @__PURE__ */ new Map();
@@ -11102,17 +11134,22 @@ function readRailwayToken(credentials) {
11102
11134
  }
11103
11135
  return token;
11104
11136
  }
11105
- function projectAccessTokenHeader(token) {
11106
- return { "Project-Access-Token": token };
11137
+ var RAILWAY_AUTH_STYLES = ["bearer", "project-access-token"];
11138
+ function railwayAuthHeader(style, token) {
11139
+ return style === "bearer" ? { Authorization: `Bearer ${token}` } : { "Project-Access-Token": token };
11107
11140
  }
11108
- async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11141
+ var resolvedRailwayAuthStyle = /* @__PURE__ */ new Map();
11142
+ function isRailwayNotAuthorized(err) {
11143
+ return err instanceof Error && /not authorized/i.test(err.message);
11144
+ }
11145
+ async function railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl) {
11109
11146
  const res = await junctionFetch(
11110
11147
  apiUrl,
11111
11148
  {
11112
11149
  method: "POST",
11113
11150
  headers: {
11114
11151
  "Content-Type": "application/json",
11115
- ...projectAccessTokenHeader(token)
11152
+ ...railwayAuthHeader(style, token)
11116
11153
  },
11117
11154
  body: JSON.stringify({ query, variables })
11118
11155
  },
@@ -11128,6 +11165,26 @@ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetch
11128
11165
  if (!body.data) throw new Error("Railway GraphQL response carried no data");
11129
11166
  return body.data;
11130
11167
  }
11168
+ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11169
+ const known = resolvedRailwayAuthStyle.get(token);
11170
+ const styles = known ? [known] : RAILWAY_AUTH_STYLES;
11171
+ let lastNotAuthorized;
11172
+ for (let i = 0; i < styles.length; i++) {
11173
+ const style = styles[i];
11174
+ try {
11175
+ const data = await railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl);
11176
+ resolvedRailwayAuthStyle.set(token, style);
11177
+ return data;
11178
+ } catch (err) {
11179
+ if (isRailwayNotAuthorized(err) && i < styles.length - 1) {
11180
+ lastNotAuthorized = err;
11181
+ continue;
11182
+ }
11183
+ throw err;
11184
+ }
11185
+ }
11186
+ throw lastNotAuthorized ?? new Error("Railway GraphQL: no auth style resolved");
11187
+ }
11131
11188
  var HTTP_LOGS_QUERY = `
11132
11189
  query HttpLogs($deploymentId: String!, $startDate: String, $endDate: String, $limit: Int) {
11133
11190
  httpLogs(deploymentId: $deploymentId, startDate: $startDate, endDate: $endDate, limit: $limit) {
@@ -12234,6 +12291,27 @@ async function loadConnectorRegistrations(input) {
12234
12291
  }
12235
12292
  return registrations;
12236
12293
  }
12294
+ async function startConnectorPolling(input) {
12295
+ const fileConnectors = input.home ? await loadConnectorRegistrations({
12296
+ project: input.project,
12297
+ graph: input.graph,
12298
+ home: input.home,
12299
+ ...input.onSkip ? { onSkip: input.onSkip } : {}
12300
+ }) : [];
12301
+ const all = [...input.extra ?? [], ...fileConnectors];
12302
+ const stopFns = all.map(
12303
+ (registration) => startConnectorPollLoop(
12304
+ registration.connector,
12305
+ { projectDir: input.projectDir, credentials: registration.credentials },
12306
+ input.graph,
12307
+ registration.resolveTarget,
12308
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
12309
+ )
12310
+ );
12311
+ return () => {
12312
+ for (const stop of stopFns) stop();
12313
+ };
12314
+ }
12237
12315
 
12238
12316
  // src/daemon.ts
12239
12317
  init_auth();
@@ -12425,31 +12503,16 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
12425
12503
  staleEventsPath: paths.staleEventsPath,
12426
12504
  project: entry.name
12427
12505
  });
12428
- const fileConnectors = neatHome3 ? await loadConnectorRegistrations({
12506
+ const stopConnectors = await startConnectorPolling({
12429
12507
  project: entry.name,
12430
12508
  graph,
12431
- home: neatHome3,
12509
+ projectDir: entry.path,
12510
+ ...neatHome3 ? { home: neatHome3 } : {},
12511
+ extra: connectors,
12432
12512
  onSkip: (skipped, reason) => console.warn(
12433
12513
  `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
12434
12514
  )
12435
- }) : [];
12436
- const allConnectors = [...connectors, ...fileConnectors];
12437
- const stopFns = allConnectors.map(
12438
- (registration) => startConnectorPollLoop(
12439
- registration.connector,
12440
- { projectDir: entry.path, credentials: registration.credentials },
12441
- graph,
12442
- registration.resolveTarget,
12443
- // `connectorId` is threaded through so every tick lands in the
12444
- // in-process status tracker the connector-status endpoint reads
12445
- // (ADR-136). Undefined for a programmatic registration, which records
12446
- // nothing.
12447
- { intervalMs: registration.intervalMs, connectorId: registration.id }
12448
- )
12449
- );
12450
- const stopConnectors = () => {
12451
- for (const stop of stopFns) stop();
12452
- };
12515
+ });
12453
12516
  await touchLastSeen(entry.name).catch(() => {
12454
12517
  });
12455
12518
  return {