From 9eb589b2dfb6dde06e44d20c80a80d4e097cdb0d Mon Sep 17 00:00:00 2001 From: Jaren Goldberg Date: Tue, 4 Aug 2026 18:20:06 -0400 Subject: [PATCH 1/4] feat: add support for non-root sites and subsites --- src/oikb/connectors/sharepoint.py | 64 ++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/oikb/connectors/sharepoint.py b/src/oikb/connectors/sharepoint.py index 83da39b..2bdef9a 100644 --- a/src/oikb/connectors/sharepoint.py +++ b/src/oikb/connectors/sharepoint.py @@ -240,13 +240,59 @@ def _get_token_via_certificate( # ── Source parser ─────────────────────────────────────────────── - -def parse_sharepoint_source(source: str) -> dict[str, str | None]: - """Parse sharepoint:site/library or sharepoint:site.""" +_SITE_PATH_PREFIXES = ("sites", "teams") +_SEPARATOR = "::" + +def parse_sharepoint_source(source: str) -> dict[str, str]: + """Parse a SharePoint source string. Supports: + sharepoint:/ + sharepoint:/sites// + sharepoint:/sites//:: (subsites) + + The "::" separator is required whenever the site path has more than one + segment after sites/ — otherwise there's no way to tell a subsite + apart from a library name. + + Examples: + sharepoint:mycompany.sharepoint.us/Documents + sharepoint:mycompany.sharepoint.us/sites/TeamSite/Documents + sharepoint:mycompany.sharepoint.us/sites/TeamSite/SubSite::Documents + """ source = source.removeprefix("sharepoint:") - parts = source.split("/", 1) - site = parts[0] - library = parts[1] if len(parts) > 1 else "Documents" - if not site: - raise ValueError("Invalid SharePoint source. Expected: sharepoint:[/library]") - return {"site": site, "library": library} + host, _, rest = source.partition("/") + if not host: + raise ValueError( + "Invalid SharePoint source. Expected one of:\n" + " sharepoint:/\n" + " sharepoint:/sites//\n" + " sharepoint:/sites//::" + ) + + if _SEPARATOR in rest: + site_path_str, _, library = rest.partition(_SEPARATOR) + site_path = site_path_str.strip("/") + if not library: + raise ValueError(f"Invalid SharePoint source: '{_SEPARATOR}' must be followed by a library name.") + return {"site": host, "site_path": site_path, "library": library} + + segments = [s for s in rest.split("/") if s] + + if segments and segments[0] in _SITE_PATH_PREFIXES: + if len(segments) < 3: + raise ValueError( + f"Invalid SharePoint source. '{segments[0]}/...' requires a site name and " + f"library, e.g. sharepoint:{host}/{segments[0]}/TeamSite/Documents" + ) + if len(segments) > 3: + raise ValueError( + "Ambiguous SharePoint source with a subsite path — separate the site path " + f"from the library explicitly with '{_SEPARATOR}', e.g.\n" + f" sharepoint:{host}/{'/'.join(segments[:-1])}{_SEPARATOR}{segments[-1]}" + ) + site_path = "/".join(segments[:2]) # e.g. "sites/TeamSite" + library = segments[2] + else: + site_path = "" + library = "/".join(segments) if segments else "Documents" + + return {"site": host, "site_path": site_path, "library": library} From ff13e9736ad783bb6a6a4f013188dda7b6a9dc6b Mon Sep 17 00:00:00 2001 From: Jaren Goldberg Date: Tue, 4 Aug 2026 18:21:06 -0400 Subject: [PATCH 2/4] Refactor SharePointConnector initialization --- src/oikb/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/oikb/cli.py b/src/oikb/cli.py index 4e668c3..d1ef75f 100644 --- a/src/oikb/cli.py +++ b/src/oikb/cli.py @@ -116,7 +116,11 @@ def _resolve_connector(source: str, branch: str | None = None, path: str | None if source.startswith("sharepoint:"): from oikb.connectors.sharepoint import SharePointConnector, parse_sharepoint_source parsed = parse_sharepoint_source(source) - return SharePointConnector(site=parsed["site"], library=parsed.get("library", "Documents")) + return SharePointConnector( + site=parsed["site"], + site_path=parsed["site_path"], + library=parsed["library"], + ) if source.startswith("nextcloud:"): from oikb.connectors.nextcloud import NextcloudConnector, parse_nextcloud_source From c429236c01f9c3d59ea9e520ec69392612214666 Mon Sep 17 00:00:00 2001 From: Jaren Goldberg Date: Tue, 4 Aug 2026 18:21:52 -0400 Subject: [PATCH 3/4] Add site_path parameter to SharePoint connector --- src/oikb/connectors/sharepoint.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/oikb/connectors/sharepoint.py b/src/oikb/connectors/sharepoint.py index 2bdef9a..9215aff 100644 --- a/src/oikb/connectors/sharepoint.py +++ b/src/oikb/connectors/sharepoint.py @@ -30,6 +30,7 @@ class SharePointConnector(BaseConnector): def __init__( self, site: str, + site_path: str = "", library: str = "Documents", tenant_id: str | None = None, client_id: str | None = None, @@ -38,6 +39,7 @@ def __init__( certificate_password: str | None = None, ): self.site = site + self.site_path = site_path.strip("/") self.library = library tid = tenant_id or os.environ.get("SHAREPOINT_TENANT_ID", "") @@ -90,7 +92,8 @@ def __init__( ) # Resolve site ID. - site_resp = self._http.get(f"/sites/{self.site}") + site_identifier = f"{self.site}:/{self.site_path}" if self.site_path else self.site + site_resp = self._http.get(f"/sites/{site_identifier}") site_resp.raise_for_status() self._site_id = site_resp.json()["id"] From f2701099c3f9759437d38513b3acbd9e40dda57c Mon Sep 17 00:00:00 2001 From: Jaren Goldberg Date: Wed, 5 Aug 2026 14:05:59 -0400 Subject: [PATCH 4/4] Clean up comments in parse_sharepoint_source function Removed redundant comments and examples from the parse_sharepoint_source function. --- src/oikb/connectors/sharepoint.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/oikb/connectors/sharepoint.py b/src/oikb/connectors/sharepoint.py index 9215aff..8256eed 100644 --- a/src/oikb/connectors/sharepoint.py +++ b/src/oikb/connectors/sharepoint.py @@ -250,16 +250,7 @@ def parse_sharepoint_source(source: str) -> dict[str, str]: """Parse a SharePoint source string. Supports: sharepoint:/ sharepoint:/sites// - sharepoint:/sites//:: (subsites) - - The "::" separator is required whenever the site path has more than one - segment after sites/ — otherwise there's no way to tell a subsite - apart from a library name. - - Examples: - sharepoint:mycompany.sharepoint.us/Documents - sharepoint:mycompany.sharepoint.us/sites/TeamSite/Documents - sharepoint:mycompany.sharepoint.us/sites/TeamSite/SubSite::Documents + sharepoint:/sites//:: """ source = source.removeprefix("sharepoint:") host, _, rest = source.partition("/")