Skip to content

Library: Make layers consistently string-typed.#726

Open
mithunbharadwaj wants to merge 15 commits intomasterfrom
Enhancement/Library-Find-works-in-target-scope
Open

Library: Make layers consistently string-typed.#726
mithunbharadwaj wants to merge 15 commits intomasterfrom
Enhancement/Library-Find-works-in-target-scope

Conversation

@mithunbharadwaj
Copy link
Copy Markdown
Collaborator

@mithunbharadwaj mithunbharadwaj commented Jan 28, 2026

Summary by CodeRabbit

  • New Features

    • Search now runs per-root (personal, tenant, global), respects per-root context and propagates target through traversal.
    • Library items use multi-type layer identifiers (strings), and include new override and optional size fields; the previous single-target field was removed.
  • Bug Fixes

    • Enforces scope boundaries during traversal, restricts recursion to valid directories, and safely accumulates results across roots.

@mithunbharadwaj mithunbharadwaj self-assigned this Jan 28, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Replaces single-root recursion in the ZooKeeper provider with per-root (personal, tenant, global) searches; _recursive_find now requires keyword-only target and root, enforces scope boundaries during traversal, and produces per-root layer labels. LibraryItem.layers changed to list of strings; override and size fields added.

Changes

Cohort / File(s) Summary
ZooKeeper provider (multi-root find)
asab/library/providers/zookeeper.py
Replaces single-root recursion with per-root searches (personal, tenant, global). _recursive_find signature extended with keyword-only target and root. Traversal enforces hard scope boundaries (skips .tenants and .personal), recurses only into directory-like entries, propagates target/root, and aggregates results with per-root-relative names and updated layer labels ("0:<target>" or string-cast layer).
LibraryItem API changes
asab/library/item.py
LibraryItem.layers type changed from List[int] to List[str] (opaque provider-defined identifiers). Added public fields override: int = 0 and size: Optional[int] = None. Removed target field and updated docstring/examples.
Filesystem provider layer formatting
asab/library/providers/filesystem.py
Library items now set layers as [str(self.Layer)] instead of numeric values to match LibraryItem.layers string type.
Azure Storage provider layer formatting
asab/library/providers/azurestorage.py
LibraryItem.layers now uses [str(self.Layer)] (stringified layer) when constructing items.
Library service layer merging
asab/library/service.py
Normalizes fallback/outer layer to string via str(...) when merging provider items so layer identifiers remain strings across merge logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • New 'personal' targets #712 — Modifies the ZooKeeper library provider to add per-target awareness and per-root traversal/labeling; strongly related to the per-root find changes here.

Suggested reviewers

  • ateska

Poem

🐇 I hop through roots both near and far,
Personal, tenant, global — each a star.
Layers now speak in strings so bright,
Override and size tucked in my sight.
A crunchy change, then off I dart — hooray, library art! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Library: Make layers consistently string-typed.' directly describes the main changes across the codebase where layers are converted from List[int] to List[str] and layer values are wrapped as strings throughout multiple providers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Enhancement/Library-Find-works-in-target-scope

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/providers/zookeeper.py`:
- Around line 646-654: The recursive directory check in _recursive_find uses the
simplistic condition '"." not in child' which is inconsistent with process_nodes
(around line 386) that treats names with .io and .d suffixes as directories;
update _recursive_find to use the same directory-detection logic as
process_nodes (either call the same helper function or replicate the suffix
check for '.io' and '.d') so entries like 'templates.d' are recursed into;
reference the _recursive_find method, the process_nodes logic, and the child
variable when making the change.
🧹 Nitpick comments (1)
asab/library/providers/zookeeper.py (1)

630-633: Consider filtering all dotfiles, not just scope directories.

Unlike process_nodes (lines 382-384), _recursive_find only skips .tenants and .personal. Other hidden entries (e.g., .version.yaml) could be matched if the search filename happens to be a suffix. For consistency, consider filtering all dot-prefixed children.

Proposed fix
 		for child in children:
 			# ---- hard stop: never cross scopes ----
-			if child in {".tenants", ".personal"}:
+			if child.startswith("."):
 				continue

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/providers/zookeeper.py`:
- Around line 625-630: The loop assumes children is iterable but
self.Zookeeper.get_children may return None (it swallows NoNodeError); update
the code around the call to self.Zookeeper.get_children to handle a None return
(e.g., assign children = await self.Zookeeper.get_children(path) and if children
is None: return or set children = [] ) before executing the for child in
children loop so you don't attempt to iterate over None.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/providers/zookeeper.py`:
- Around line 594-600: The find method lacks the readiness guard present in
read/list, so add the same check at the start of async def find(self, filename:
str) to raise a RuntimeError if the ZooKeeper client isn't connected;
specifically replicate the readiness guard used in the list/read methods
(call/check the same readiness attribute or helper and raise RuntimeError with
the same message) before using attributes that assume a connected client (so
tenant/cred logic and subsequent operations won't hit AttributeError).

Comment on lines +594 to +600
async def find(self, filename: str) -> list:
"""
Recursively search for files ending with a specific name in ZooKeeper nodes, starting from the base path.

:param filename: The filename to search for (e.g., '.setup.yaml')
:return: A list of LibraryItem objects for files ending with the specified name,
or an empty list if no matching files were found.
"""
results = []
await self._recursive_find(self.BasePath, filename, results)

tenant_id = self._current_tenant_id()
cred_id = self._current_credentials_id()

search_roots = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a readiness guard in find (match read/list).

Line 594: find can be called before ZooKeeper is connected; it will fail with an AttributeError instead of the clearer RuntimeError used by read/list. Consider adding the same guard for consistency.

💡 Suggested fix
 async def find(self, filename: str) -> list:
+	if self.Zookeeper is None:
+		L.warning("Zookeeper Client has not been established (yet). Cannot find {}".format(filename))
+		raise RuntimeError("Zookeeper Client has not been established (yet). Not ready.")
 	results = []
🤖 Prompt for AI Agents
In `@asab/library/providers/zookeeper.py` around lines 594 - 600, The find method
lacks the readiness guard present in read/list, so add the same check at the
start of async def find(self, filename: str) to raise a RuntimeError if the
ZooKeeper client isn't connected; specifically replicate the readiness guard
used in the list/read methods (call/check the same readiness attribute or helper
and raise RuntimeError with the same message) before using attributes that
assume a connected client (so tenant/cred logic and subsequent operations won't
hit AttributeError).

Copy link
Copy Markdown
Contributor

@mejroslav mejroslav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @mithunbharadwaj , I have questions before you merge this one.

Comment on lines -596 to -602
"""
Recursively search for files ending with a specific name in ZooKeeper nodes, starting from the base path.

:param filename: The filename to search for (e.g., '.setup.yaml')
:return: A list of LibraryItem objects for files ending with the specified name,
or an empty list if no matching files were found.
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mithunbharadwaj Did you remove the docstring? Please provide a new one then.

)
else:
# recurse only into directories
if "." not in child:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will also skip directories like /Site/LogMan.io/ - don't we have exceptions somewhere in asab.library?

type="item", # or "dir" if applicable
layers=[self.Layer],
type="item",
layers=[self.Layer if self.Layer != 0 else "0:{}".format(target)],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you mixing ints and strings here?

layers=[str(self.Layer) if self.Layer != 0 else "0:{}".format(target)]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mejroslav

This is intentional for the new target-aware layer model.

Layer 0 now carries target information ("0:tenant", "0:personal"), and layers are treated as opaque identifiers throughout the library stack.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but the library layers should be consistent in types.

I propose:

  1. Double check that item.Layer attribute is not used as an integer somewhere in asab.library and micro-services which use asab.
  2. Change it's type to a string for all layers, not only the zeroth.
  3. Clearly state that this as a breaking change in changelog.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mejroslav So this change does not break internal behavior.

However, I agree this is a public type contract change for consumers, and I’ll mark it as a breaking change and normalize all layers to strings.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/providers/zookeeper.py`:
- Around line 639-657: The returned LibraryItem.name currently slices using
name=full_path[len(self.BasePath):], which leaves internal scope prefixes (e.g.
/.tenants/... or /.personal/...) in tenant/personal results; change the logic
that computes the public name for LibraryItem (where name=...) to strip the
per-root scope prefix (remove the specific per-root segment like
"/.tenants/<id>" or "/.personal/<id>") rather than only trimming self.BasePath,
so the resulting name is a library-relative path (e.g. "/foo/bar.yaml")
compatible with read/list; update the code that builds name in the LibraryItem
construction (same spot using full_path and self.BasePath) to detect and remove
the per-root prefix when present.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
asab/library/item.py (1)

13-21: Align docstring with actual fields (override semantics, target, size).

The Attributes section documents target (which doesn't exist on LibraryItem), describes override with boolean-like language ("If True") despite it being an int field, and omits the size field entirely. This mismatch confuses users of this public dataclass API.

✍️ Proposed docstring fix
-        override (int): If `True`, this item is marked as an override for the providers with the same Item name.
-        target (str): Specifies the target context, e.g., "tenant" or "global". Defaults to "global".
+        override (int): Non-zero indicates this item overrides providers with the same Item name.
+        size (Optional[int]): Size in bytes. `None` for directories or unknown size.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@asab/library/providers/zookeeper.py`:
- Around line 642-651: The continuation line supplying the name argument to
LibraryItem in the results.append block is over-indented causing flake8 E126;
locate the results.append(...) that constructs LibraryItem (look for
LibraryItem, name= and self.Layer/target) and re-indent the continuation so the
string concatenation ("/" + full_path[len(root):].lstrip("/")) aligns with the
opening parenthesis or the hanging indent style used in the file (i.e., reduce
its indent to match the other continued arguments), ensuring the block passes
flake8 E126.
- Around line 653-661: The recursive call to _recursive_find is missing the
required keyword-only root argument; update the call in the else branch to pass
root (the same root used in the current invocation) as a keyword, i.e. call
self._recursive_find(full_path, filename, results, target=target, root=root) so
_recursive_find receives its required root parameter and recursion into
subdirectories works correctly.

Comment on lines +13 to +15
layer (int): The number of highest layer in which this Item is found. The higher the number, the lower the layer is.
layers (list[int | str]): Identifiers of layers in which this item was found.
Values are provider-defined and treated as opaque identifiers.
Examples: 0, 1, "0:global", "0:tenant", "0:personal".
Copy link
Copy Markdown
Contributor

@mejroslav mejroslav Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to provide a list of strings only to avoid additional complexity.

Besides, the example should be like:

["0:personal", "0:tenant", "0:global", 2, 3]

@mithunbharadwaj mithunbharadwaj added the breaking change This will introduce a breaking change label Jan 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
asab/library/item.py (1)

13-24: ⚠️ Potential issue | 🟠 Major

Align layers type hint with documented/actual values.

The docstring and existing providers treat layer identifiers as int or str, but the type annotation now restricts to List[str]. This is a public API mismatch and will break typed consumers (and conflicts with current provider behavior). Prefer making the type List[Union[int, str]] or, if you want strings only, convert all providers to str and update the docstring/examples accordingly.

🐛 Proposed fix (align type hint with docstring/examples)
-    layers: typing.List[str]
+    layers: typing.List[typing.Union[int, str]]

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/providers/filesystem.py`:
- Around line 154-159: The _list method emits layers as List[int] while
_recursive_find returns List[str], causing mixed types against the
LibraryItem.layers: List[str] contract; update the _list implementation to cast
or format its layers to strings (e.g., using str(self.Layer) or mapping ints to
str) so that LibraryItem(..., layers=[...]) always receives List[str]; touch the
_list function and ensure it uses the same conversion logic as _recursive_find
and references LibraryItem, layers, and self.Layer.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@asab/library/item.py`:
- Line 18: The docstring for the Item field "override" describes boolean
semantics but the code declares override: int = 0; update the declaration to a
boolean and default to False (e.g., override: bool = False) in the Item class,
and adjust any code that relies on numeric values of override to use True/False;
alternatively, if numeric semantics are intended, update the docstring to
explain what the integer represents (e.g., count or priority) and keep override:
int = 0—refer to the Item class and the override field to implement the chosen
change consistently.
🧹 Nitpick comments (1)
asab/library/providers/zookeeper.py (1)

641-651: endswith(filename) can produce false-positive matches.

full_path.endswith(filename) will match any path whose suffix equals filename. For example, searching for setup.yaml would match both /foo/setup.yaml and /foo/my-setup.yaml. Consider checking that the match is preceded by / to ensure an exact filename match:

-			if full_path.endswith(filename):
+			if full_path.endswith("/" + filename) or full_path == filename:

This may be pre-existing behavior, but worth tightening now that find is being reworked.

providers (list): List of `LibraryProvider` objects containing this Item.
disabled (bool): `True` if the Item is disabled, `False` otherwise. If the Item is disabled, `LibraryService.read(...)` will return `None`.
favorite (bool): True if the Item is marked as a favorite.
override (int): If `True`, this item is marked as an override for the providers with the same Item name.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring says "If True" but override is typed as int.

The docstring describes boolean semantics ("If True") while the field is override: int = 0. Either change the type to bool or update the doc to describe what the integer value represents (e.g., override count or priority).

🤖 Prompt for AI Agents
In `@asab/library/item.py` at line 18, The docstring for the Item field "override"
describes boolean semantics but the code declares override: int = 0; update the
declaration to a boolean and default to False (e.g., override: bool = False) in
the Item class, and adjust any code that relies on numeric values of override to
use True/False; alternatively, if numeric semantics are intended, update the
docstring to explain what the integer represents (e.g., count or priority) and
keep override: int = 0—refer to the Item class and the override field to
implement the chosen change consistently.

@mithunbharadwaj mithunbharadwaj changed the title Library Find is scoped now. Library: Make layers consistently string-typed. Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change This will introduce a breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants