basic-memory 0.23.2.dev3__py3-none-any.whl → 0.23.3.dev2__py3-none-any.whl

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.
basic_memory/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
3
  # Package version - updated by release automation
4
- __version__ = "0.23.1"
4
+ __version__ = "0.23.2"
5
5
 
6
6
  # API version for FastAPI - independent of package version
7
7
  __api_version__ = "v2"
@@ -2,34 +2,24 @@
2
2
 
3
3
  import asyncio
4
4
  import os
5
- import sys
6
5
  from contextlib import suppress
7
6
  from logging.config import fileConfig
8
7
 
9
- from loguru import logger
10
8
  from sqlalchemy import engine_from_config, pool
11
9
  from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
12
10
 
13
11
  from alembic import context
14
12
 
15
13
  from basic_memory.config import ConfigManager
16
- from basic_memory.migration_loop import running_on_uvloop
17
14
 
18
- # Allow nested event loops (needed for pytest-asyncio and other async contexts).
19
- # nest_asyncio cannot patch a uvloop loop or Python 3.14+; in those cases we skip
20
- # it and rely on the thread-based fallback in run_migrations_online() instead
21
- # (see basic_memory.migration_loop for why uvloop must be detected up front).
22
- if sys.version_info < (3, 14) and not running_on_uvloop():
23
- try:
24
- import nest_asyncio
25
-
26
- nest_asyncio.apply()
27
- except (ImportError, ValueError) as exc:
28
- # Trigger: nest_asyncio is absent (ImportError) or refuses to patch the
29
- # running loop (ValueError).
30
- # Outcome: log at DEBUG (observable, not noisy) and fall through to the
31
- # thread-based migration fallback.
32
- logger.debug(f"nest_asyncio not applied ({exc!r}); using thread-based migration fallback")
15
+ # Constraint: never apply nest_asyncio here. It replaces asyncio.run and
16
+ # run_until_complete process-wide, and its run_until_complete returns the moment
17
+ # the root task finishes without running that task's done-callbacks. anyio stops
18
+ # its worker threads from exactly such a callback, so any request that touched
19
+ # the thread pool left a non-daemon worker parked forever and one-shot CLI
20
+ # commands hung at interpreter exit on Python < 3.14 (#1333, #1345). Callers
21
+ # already inside a running loop (pytest-asyncio, the API lifespan) take the
22
+ # thread fallback in run_migrations_online(), the only path 3.14 ever used.
33
23
 
34
24
  # Trigger: only set test env when actually running under pytest
35
25
  # Why: alembic/env.py is imported during normal operations (MCP server startup, migrations)
@@ -12,12 +12,16 @@ from fastapi import Depends, Request
12
12
  from basic_memory.config import BasicMemoryConfig
13
13
 
14
14
 
15
- def get_app_config(request: Request) -> BasicMemoryConfig:
15
+ async def get_app_config(request: Request) -> BasicMemoryConfig:
16
16
  """Resolve the application configuration from the composition root.
17
17
 
18
18
  API requests read the container the lifespan stored on ``app.state``.
19
19
  Requests served without a lifespan (the CLI/MCP local ASGI flow) fall back
20
20
  to the API composition root, which creates a container on demand.
21
+
22
+ Declared ``async`` although it never awaits: FastAPI runs *sync* dependencies
23
+ on anyio's worker-thread pool, and an attribute read is not worth a thread
24
+ hop per request — nor the non-daemon worker it leaves parked (#1345).
21
25
  """
22
26
  container = getattr(request.app.state, "container", None)
23
27
  if container is not None:
@@ -12,8 +12,12 @@ from basic_memory.read_cache.policy import (
12
12
  )
13
13
 
14
14
 
15
- def get_read_cache(request: Request) -> ReadCache | None:
16
- """Return the optional host-injected cache backend."""
15
+ async def get_read_cache(request: Request) -> ReadCache | None:
16
+ """Return the optional host-injected cache backend.
17
+
18
+ ``async`` for the same reason as ``get_app_config``: a sync dependency would
19
+ hop to anyio's worker pool just to read an attribute (#1345).
20
+ """
17
21
  try:
18
22
  container = request.app.state.container
19
23
  except AttributeError:
@@ -5,6 +5,7 @@ from typing import List, Any, Dict
5
5
 
6
6
  from basic_memory.utils import normalize_project_reference
7
7
  from markdown_it import MarkdownIt
8
+ from markdown_it.rules_inline.backticks import backtick
8
9
  from markdown_it.token import Token
9
10
 
10
11
  # Transcript timecodes like [00:00:11] or [1:02:03.500] share the bracket shape of
@@ -127,6 +128,84 @@ def parse_observation(token: Token) -> Dict[str, Any]:
127
128
 
128
129
 
129
130
  # Relation handling functions
131
+ def _relation_content(token: Token) -> str:
132
+ """Return the source content of an inline token."""
133
+ return token.tag or token.content
134
+
135
+
136
+ _CODE_SPANS_KEY = "basic_memory_code_spans"
137
+ _CODE_SPAN_PARSER = MarkdownIt()
138
+
139
+
140
+ def _record_inline_code_span(state: Any, silent: bool) -> bool:
141
+ """Delegate to MarkdownIt and retain exact source spans for code tokens."""
142
+ start = state.pos
143
+ token_count = len(state.tokens)
144
+ matched = backtick(state, silent)
145
+ if matched and not silent and len(state.tokens) > token_count:
146
+ if state.tokens[-1].type == "code_inline":
147
+ state.env[_CODE_SPANS_KEY].append((start, state.pos))
148
+ return matched
149
+
150
+
151
+ _CODE_SPAN_PARSER.inline.ruler.at("backticks", _record_inline_code_span)
152
+
153
+
154
+ def _inline_code_spans(content: str) -> list[tuple[int, int]]:
155
+ """Return source ranges that MarkdownIt classifies as inline code.
156
+
157
+ This delegates delimiter and escape handling to the same MarkdownIt inline
158
+ rule used by the document parser instead of duplicating CommonMark's
159
+ backtick scanner. It also preserves MarkdownIt's linear-time cache for
160
+ unmatched delimiter runs.
161
+ """
162
+ if "`" not in content:
163
+ return []
164
+
165
+ spans: list[tuple[int, int]] = []
166
+ _CODE_SPAN_PARSER.inline.parse(content, _CODE_SPAN_PARSER, {_CODE_SPANS_KEY: spans}, [])
167
+ return spans
168
+
169
+
170
+ def _mask_wikilinks_in_inline_code(content: str, code_spans: list[tuple[int, int]]) -> str:
171
+ """Mask only brackets in code spans, retaining all non-link source text."""
172
+ if not code_spans:
173
+ return content
174
+
175
+ masked = list(content)
176
+ for start, end in code_spans:
177
+ for position in range(start, end):
178
+ if masked[position] in "[]":
179
+ masked[position] = " "
180
+
181
+ return "".join(masked)
182
+
183
+
184
+ def _remove_links_to_directive_outside_inline_code(
185
+ content: str, code_spans: list[tuple[int, int]]
186
+ ) -> tuple[str, bool]:
187
+ """Remove a terminal directive only when MarkdownIt parsed it as non-code."""
188
+ directive_content = list(content)
189
+ for start, end in code_spans:
190
+ directive_content[start:end] = " " * (end - start)
191
+
192
+ match = _LINKS_TO_DIRECTIVE.search("".join(directive_content))
193
+ if not match:
194
+ return content, False
195
+ return content[: match.start()].rstrip(), True
196
+
197
+
198
+ def _relation_parsing_content(token: Token) -> tuple[str, str, list[tuple[int, int]]]:
199
+ """Build source-preserving relation input from MarkdownIt's code spans."""
200
+ source_content = _relation_content(token)
201
+ code_spans = _inline_code_spans(source_content)
202
+ return (
203
+ source_content,
204
+ _mask_wikilinks_in_inline_code(source_content, code_spans),
205
+ code_spans,
206
+ )
207
+
208
+
130
209
  def parse_relation_type(content: str) -> str | None:
131
210
  """Return the explicit relation label before the first wikilink, if any."""
132
211
  before_link = content.partition("[[")[0].strip()
@@ -154,15 +233,17 @@ def is_explicit_relation(token: Token) -> bool:
154
233
  if token.type != "inline": # pragma: no cover
155
234
  return False
156
235
 
157
- # Use token.tag which contains the actual content for test tokens, fallback to content
158
- content = (token.tag or token.content).strip()
236
+ _, content, _ = _relation_parsing_content(token)
159
237
  if "[[" not in content or "]]" not in content:
160
238
  return False
161
239
  return _parse_explicit_relation(content) is not None
162
240
 
163
241
 
164
- def _parse_explicit_relation(content: str) -> Dict[str, Any] | None:
242
+ def _parse_explicit_relation(
243
+ content: str, source_content: str | None = None
244
+ ) -> Dict[str, Any] | None:
165
245
  """Parse ``type [[target]] (context)``, rejecting lines with a prose tail."""
246
+ source_content = source_content or content
166
247
  rel_type = parse_relation_type(content)
167
248
  if rel_type is None:
168
249
  return None
@@ -172,7 +253,7 @@ def _parse_explicit_relation(content: str) -> Dict[str, Any] | None:
172
253
  if start == -1 or end == -1:
173
254
  return None
174
255
 
175
- target = normalize_project_reference(content[start + 2 : end].strip())
256
+ target = normalize_project_reference(source_content[start + 2 : end].strip())
176
257
  if not target:
177
258
  return None
178
259
 
@@ -189,7 +270,8 @@ def _parse_explicit_relation(content: str) -> Dict[str, Any] | None:
189
270
  if after:
190
271
  if not _is_single_parenthesized(after):
191
272
  return None
192
- context = after[1:-1].strip() or None
273
+ source_after = source_content[end + 2 :].strip()
274
+ context = source_after[1:-1].strip() or None
193
275
 
194
276
  return {"type": rel_type, "target": target, "context": context}
195
277
 
@@ -217,13 +299,23 @@ def _is_single_parenthesized(text: str) -> bool:
217
299
 
218
300
  def parse_relation(token: Token) -> Dict[str, Any] | None:
219
301
  """Extract relation parts from token."""
220
- # Use token.tag which contains the actual content for test tokens, fallback to content
221
- content = (token.tag or token.content).strip()
222
- return _parse_explicit_relation(content)
302
+ source_content, content, _ = _relation_parsing_content(token)
303
+ return _parse_explicit_relation(content, source_content)
304
+
223
305
 
306
+ def _is_escaped(content: str, position: int) -> bool:
307
+ """Whether the character at ``position`` is escaped by an odd slash run."""
308
+ slash_count = 0
309
+ position -= 1
310
+ while position >= 0 and content[position] == "\\":
311
+ slash_count += 1
312
+ position -= 1
313
+ return slash_count % 2 == 1
224
314
 
225
- def parse_inline_relations(content: str) -> List[Dict[str, Any]]:
226
- """Find wiki-style links in regular content."""
315
+
316
+ def parse_inline_relations(content: str, source_content: str | None = None) -> List[Dict[str, Any]]:
317
+ """Find wiki-style links, extracting targets from the original source."""
318
+ source_content = content if source_content is None else source_content
227
319
  relations = []
228
320
  start = 0
229
321
 
@@ -232,6 +324,9 @@ def parse_inline_relations(content: str) -> List[Dict[str, Any]]:
232
324
  start = content.find("[[", start)
233
325
  if start == -1: # pragma: no cover
234
326
  break
327
+ if _is_escaped(content, start):
328
+ start += 2
329
+ continue
235
330
 
236
331
  # Find matching ]]
237
332
  depth = 1
@@ -239,10 +334,10 @@ def parse_inline_relations(content: str) -> List[Dict[str, Any]]:
239
334
  end = -1
240
335
 
241
336
  while pos < len(content):
242
- if content[pos : pos + 2] == "[[":
337
+ if content[pos : pos + 2] == "[[" and not _is_escaped(content, pos):
243
338
  depth += 1
244
339
  pos += 2
245
- elif content[pos : pos + 2] == "]]":
340
+ elif content[pos : pos + 2] == "]]" and not _is_escaped(content, pos):
246
341
  depth -= 1
247
342
  if depth == 0:
248
343
  end = pos
@@ -255,7 +350,7 @@ def parse_inline_relations(content: str) -> List[Dict[str, Any]]:
255
350
  # No matching ]] found
256
351
  break
257
352
 
258
- target = normalize_project_reference(content[start + 2 : end].strip())
353
+ target = normalize_project_reference(source_content[start + 2 : end].strip())
259
354
  if target:
260
355
  relations.append({"type": "links_to", "target": target, "context": None})
261
356
 
@@ -336,21 +431,27 @@ def relation_plugin(md: MarkdownIt) -> None:
336
431
 
337
432
  # Only process inline tokens
338
433
  if token.type == "inline":
339
- content = token.tag or token.content
340
- content_without_directive, has_directive = remove_links_to_directive(content)
434
+ source_content = _relation_content(token)
435
+ if "[[" not in source_content:
436
+ continue
437
+
438
+ code_spans = _inline_code_spans(source_content)
439
+ relation_content = _mask_wikilinks_in_inline_code(source_content, code_spans)
440
+ content_without_directive, has_directive = (
441
+ _remove_links_to_directive_outside_inline_code(relation_content, code_spans)
442
+ )
341
443
 
342
444
  # Check for explicit relations in list items
343
- if in_list_item and not has_directive and is_explicit_relation(token):
344
- rel = parse_relation(token)
445
+ if in_list_item and not has_directive:
446
+ rel = _parse_explicit_relation(relation_content, source_content)
345
447
  if rel:
346
448
  token.meta["relations"] = [rel]
449
+ continue
347
450
 
348
451
  # Always check for inline links in any text
349
- else:
350
- if "[[" in content:
351
- rels = parse_inline_relations(content_without_directive)
352
- if rels:
353
- token.meta["relations"] = token.meta.get("relations", []) + rels
452
+ rels = parse_inline_relations(content_without_directive, source_content)
453
+ if rels:
454
+ token.meta["relations"] = token.meta.get("relations", []) + rels
354
455
 
355
456
  # Add the rule after inline processing
356
457
  md.core.ruler.after("inline", "relations", relation_rule)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: basic-memory
3
- Version: 0.23.2.dev3
3
+ Version: 0.23.3.dev2
4
4
  Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
5
5
  Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
6
6
  Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
@@ -29,7 +29,6 @@ Requires-Dist: mcp<3,>=2
29
29
  Requires-Dist: mdformat-frontmatter>=2.0.8
30
30
  Requires-Dist: mdformat-gfm>=0.3.7
31
31
  Requires-Dist: mdformat>=0.7.22
32
- Requires-Dist: nest-asyncio>=1.6.0
33
32
  Requires-Dist: openai>=1.100.2
34
33
  Requires-Dist: pillow>=11.1.0
35
34
  Requires-Dist: psutil>=5.9.0
@@ -1,4 +1,4 @@
1
- basic_memory/__init__.py,sha256=rHK-KinfjZzEkeUVmFnT-996yse86yhhjCpaHBNCd9k,256
1
+ basic_memory/__init__.py,sha256=dtmjVy3Q4_DYddSDwS9XOq3okErGMfYtVZ-_1s5p2zQ,256
2
2
  basic_memory/config.py,sha256=gPw1piQZJANYCF-TkpEpc9-vD7-lHnSez8HQidGh5qk,12080
3
3
  basic_memory/config_logging.py,sha256=keqWv5xPOP6fjh1LDQmKMDyduHTYAhr6Y1EKiiYVDnc,1857
4
4
  basic_memory/config_migrations.py,sha256=OO5hwzs3x5EEix-jNMeETrJQwMx0l2iHUywXRLMjzFA,3582
@@ -6,14 +6,13 @@ basic_memory/config_models.py,sha256=TehggqA0vm9eVo9rQugAXyTmQ-R_BJVriNl8niMiVw0
6
6
  basic_memory/db.py,sha256=STYA6RmCWXFaF3ZhC0_ay0Q8B2V_htrOFo6YQcW528Q,25977
7
7
  basic_memory/file_utils.py,sha256=_cIrb3xebx2wDdoHvvIWWgA9yfePc4TDuAAnks3Dges,18540
8
8
  basic_memory/ignore_utils.py,sha256=1OoKxiS7_ofYYHWTtH5zoUVeaMJ8-V7j0dsqxYA3dDo,8486
9
- basic_memory/migration_loop.py,sha256=KGhuCLgrgwW21GAGT9cOczbtRlM4mGi7aUBjG1shQ8s,1609
10
9
  basic_memory/project_resolver.py,sha256=vHWHw1X9_1IqtX9Dy5yNXqNRas3wcrpQJVKxJuj4qWg,3801
11
10
  basic_memory/redaction.py,sha256=tYolNeRQdOfcELD9sGtzQbB5dS3odP9PAnzipqmMl6M,4804
12
11
  basic_memory/telemetry.py,sha256=pAkPgbAOmivZDkCAW0S2CKSNw4AqBfMckdsbb2EipcQ,1721
13
12
  basic_memory/utils.py,sha256=bXlhv3qHOO_1Lb_2gUspjOZ3H1-qgreWl_n5eR6_Jxg,35426
14
13
  basic_memory/workspace_context.py,sha256=v-u4jO8VFB1WeFZ1UpzZnbf9ph71vvz1OMibpAtrtQI,3648
15
14
  basic_memory/alembic/alembic.ini,sha256=IEZsnF8CbbZnkwBr67LzKKNobHuzTaQNUvM8Psop5xc,3733
16
- basic_memory/alembic/env.py,sha256=ZfUWoJeIJKTCsOSvz6mty0c570k2k4MF9cvi7T4sj1g,9581
15
+ basic_memory/alembic/env.py,sha256=26K_F2KEvz3SneUQ5LuHCpvZ1FD1w9s1wP63lQFDNZI,9275
17
16
  basic_memory/alembic/migrations.py,sha256=lriHPXDdBLSNXEW3QTpU0SJKuVd1V-8NrVkpN3qfsUQ,718
18
17
  basic_memory/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
19
18
  basic_memory/alembic/versions/2d26b287813b_dedupe_observations_and_purge_stale_.py,sha256=h-Q0g5g2r3GmXWK49RyxvmvHGd5idVGA4VYkK4LYTek,2139
@@ -115,11 +114,11 @@ basic_memory/cli/commands/cloud/webdav.py,sha256=5yanr7lQy6dv8RVvixI0k_3HpCOnyzV
115
114
  basic_memory/cli/commands/cloud/webdav_transfer.py,sha256=ncj_eLthaN7a8-eKyieBxHoBqg3SaXVXIs6K8_UGw2Q,23340
116
115
  basic_memory/cli/commands/cloud/workspace.py,sha256=08TdcYohmDunprmUgpGO4SvCjD7A_vynBYIkD2LNooc,3940
117
116
  basic_memory/deps/__init__.py,sha256=thQt1C04eNMA49Y3OubiIiv0WIWie75aCd4TBTMkddE,6139
118
- basic_memory/deps/config.py,sha256=FjhA3OVWf0U0n9BzYnxSn5MXdgq-mMGxxhgzHjIgBgo,1200
117
+ basic_memory/deps/config.py,sha256=zQrYU7K6XIFflvi8VYFmbEhIJ5GDQXoqHZ9SFWoLKQg,1444
119
118
  basic_memory/deps/db.py,sha256=xpD5Mcq16f8J0lbu8PCoRcr2s4e1xKOj1nze_NW3CGg,2395
120
119
  basic_memory/deps/importers.py,sha256=G2ulW2xdexR4indqlNYqt9kzoQCtSqnPcRgb-ae4Cuo,3786
121
120
  basic_memory/deps/projects.py,sha256=z2Qziv58GJw-I4CofIiIXeyXBVtYHAcd-dep5Fg2slw,3352
122
- basic_memory/deps/read_cache.py,sha256=fcwfzvqS5b4aNBxcLjCPNlKvMYj3ndxyHcOd4vFJkOM,1400
121
+ basic_memory/deps/read_cache.py,sha256=l6yJCUWkW2kHhK80GqzPMSJDwEe6wqWFC7gOgJg_YY8,1559
123
122
  basic_memory/deps/repositories.py,sha256=2ZQeScK-C3VOnYs_re0FCAty5cEz3PpTPnSxtW_gFqQ,2750
124
123
  basic_memory/deps/services.py,sha256=cjxsFhVTwqmf-nUUv3SdEKM-UXBf8OH0oQ9Dj5HO7RY,21096
125
124
  basic_memory/hooks/__init__.py,sha256=kEvNW8tpXcnTdzwUpjoir2RZxVXRULJhB38WGwDKCHc,861
@@ -207,7 +206,7 @@ basic_memory/man/bm.1,sha256=iQhnHzJHSN3WRdMIphv22AnWTYR2SErzeKwCqQJ3LzQ,3406
207
206
  basic_memory/markdown/__init__.py,sha256=DdzioCWtDnKaq05BHYLgL_78FawEHLpLXnp-kPSVfIc,501
208
207
  basic_memory/markdown/entity_parser.py,sha256=7RUL7QXOQc00ez7OFOJB6kvsD8yY6K_yVD4VGSHOPxc,12629
209
208
  basic_memory/markdown/markdown_processor.py,sha256=YK8KF8NY0N9nQM3I_8Pcxybf5SPOPnIL5kzmFBFBlx0,7429
210
- basic_memory/markdown/plugins.py,sha256=y_gfzywOC4r-WNTYPPnn-Px0eiCTwdm2MJnFXJEfIwg,12831
209
+ basic_memory/markdown/plugins.py,sha256=US8WhuZVfElSPGlSjP8_8C1J_dvasc6EjZJAF7-QvZ8,16464
211
210
  basic_memory/markdown/schemas.py,sha256=IHi4sm40ew4zNjb359imwiPJXGO6cIni_1f3YNVyZPs,2798
212
211
  basic_memory/markdown/utils.py,sha256=EP8P8uDNrhcnvpgc_iaczIQK3r9Z6k1MHMKCcZvTzm0,4122
213
212
  basic_memory/mcp/__init__.py,sha256=dsDOhKqjYeIbCULbHIxfcItTbqudEuEg1Np86eq0GEQ,35
@@ -391,8 +390,8 @@ basic_memory/services/search_service.py,sha256=8A1vXo5IIpKXL2gxEsCEP_tcKyRcnz4R5
391
390
  basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
392
391
  basic_memory/templates/prompts/continue_conversation.hbs,sha256=trrDHSXA5S0JCbInMoUJL04xvCGRB_ku1RHNQHtl6ZI,3076
393
392
  basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
394
- basic_memory-0.23.2.dev3.dist-info/METADATA,sha256=g1nFCTGZ2YGR6nRflPOqiOMpWI_8Ew2oJHYpIj7ji3Q,26847
395
- basic_memory-0.23.2.dev3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
396
- basic_memory-0.23.2.dev3.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
397
- basic_memory-0.23.2.dev3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
398
- basic_memory-0.23.2.dev3.dist-info/RECORD,,
393
+ basic_memory-0.23.3.dev2.dist-info/METADATA,sha256=_c99-bcBWDk2nBo5pqbUCMbUVlYnl3TV2vtXjWFolGk,26812
394
+ basic_memory-0.23.3.dev2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
395
+ basic_memory-0.23.3.dev2.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
396
+ basic_memory-0.23.3.dev2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
397
+ basic_memory-0.23.3.dev2.dist-info/RECORD,,
@@ -1,34 +0,0 @@
1
- """Pure helpers for how Alembic bridges async migrations across event loops.
2
-
3
- Extracted from ``alembic/env.py`` so these two decisions are unit-testable:
4
- env.py runs migrations at import time and cannot be imported in a test, but the
5
- loop-detection and error-classification logic is exactly what regresses, so it
6
- lives here where it can be covered directly.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import asyncio
12
-
13
-
14
- def running_on_uvloop() -> bool:
15
- """Return True when the active asyncio policy is uvloop.
16
-
17
- The Postgres backend installs the uvloop policy before the event loop starts
18
- (#831/#877). nest_asyncio cannot patch a uvloop loop, but depending on the
19
- nest_asyncio version ``apply()`` may NOT raise the ValueError we expect — it
20
- can silently mis-patch the stdlib loop instead. The later ``asyncio.run()``
21
- then fails with "this event loop is already running" rather than the standard
22
- "cannot be called from a running event loop", which the thread-based fallback
23
- would not recognize, crashing startup. Detecting uvloop up front lets env.py
24
- skip nest_asyncio entirely and stay on the standard running-loop error path.
25
- """
26
- try:
27
- import uvloop
28
- except ImportError:
29
- return False
30
- # Constraint: on supported Python 3.12-3.13 this helper runs before a loop
31
- # exists, so the active policy is the only observable uvloop signal. Python
32
- # 3.14+ skips this helper in alembic/env.py; remove it with the policy seam.
33
- policy = asyncio.get_event_loop_policy() # ty: ignore[deprecated]
34
- return isinstance(policy, uvloop.EventLoopPolicy)