diff --git a/.bumpversion.toml b/.bumpversion.toml
index 888820ad..5f902d95 100644
--- a/.bumpversion.toml
+++ b/.bumpversion.toml
@@ -62,4 +62,16 @@ replace = 'version = "{new_version}"'
[[tool.bumpversion.files]]
filename = "rust/reqwest-client.toml"
search = 'version = "{current_version}"'
-replace = 'version = "{new_version}"'
\ No newline at end of file
+replace = 'version = "{new_version}"'
+
+# Python lance_namespace package
+[[tool.bumpversion.files]]
+filename = "python/pyproject.lance_namespace.toml"
+search = 'version = "{current_version}"'
+replace = 'version = "{new_version}"'
+
+# Java lance-namespace-core module
+[[tool.bumpversion.files]]
+filename = "java/lance-namespace-core/pom.xml"
+search = "{current_version}"
+replace = "{new_version}"
\ No newline at end of file
diff --git a/.github/workflows/java-publish.yml b/.github/workflows/java-publish.yml
index c2bb9c06..54813b1d 100644
--- a/.github/workflows/java-publish.yml
+++ b/.github/workflows/java-publish.yml
@@ -52,8 +52,8 @@ jobs:
- name: Set github
run: |
- git config --global user.name "LanceDB Github Runner"
- git config --global user.email "dev+gha@lancedb.com"
+ git config --global user.name "Lance Github Runner"
+ git config --global user.email "dev+gha@lance.org"
- name: Dry run
if: github.event_name == 'pull_request'
@@ -94,6 +94,7 @@ jobs:
# List of some artifacts to check
ARTIFACTS=(
"lance-namespace-apache-client"
+ "lance-namespace-core"
)
echo "Waiting for version $VERSION to be available in Maven Central..."
@@ -124,7 +125,14 @@ jobs:
echo ""
echo "Users can now add the following dependencies to their projects:"
echo ""
- echo "Maven:"
+ echo "Maven (interface):"
+ echo ""
+ echo " org.lance"
+ echo " lance-namespace-core"
+ echo " ${VERSION}"
+ echo ""
+ echo ""
+ echo "Maven (generated client):"
echo ""
echo " org.lance"
echo " lance-namespace-apache-client"
@@ -132,10 +140,8 @@ jobs:
echo ""
echo ""
echo "Gradle:"
+ echo "implementation 'org.lance:lance-namespace-core:${VERSION}'"
echo "implementation 'org.lance:lance-namespace-apache-client:${VERSION}'"
- echo ""
- echo "SBT:"
- echo "libraryDependencies += \"org.lance\" % \"lance-namespace-apache-client\" % \"${VERSION}\""
exit 0
fi
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index dfef7ccc..50991415 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -79,3 +79,8 @@ jobs:
run: |
uv sync
uv run pytest
+ - name: Test lance_namespace with Python ${{ matrix.python-version }}
+ working-directory: python/lance_namespace
+ run: |
+ uv sync
+ uv run pytest
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 3a766535..a4e1ee59 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -1,10 +1,10 @@
site_name: Lance Namespace
site_description: open specification on top of the storage-based Lance data format to standardize access to a collection of Lance tables
-site_url: https://lancedb.github.io/lance-namespace/
+site_url: https://lance.org/format/namespace/
docs_dir: src
-repo_name: lancedb/lance-namespace
-repo_url: https://github.com/lancedb/lance-namespace
+repo_name: lance-format/lance-namespace
+repo_url: https://github.com/lance-format/lance-namespace
theme:
name: material
@@ -60,9 +60,7 @@ plugins:
extra:
social:
- icon: fontawesome/brands/github
- link: https://github.com/lancedb/lance-namespace
+ link: https://github.com/lance-format/lance-namespace
- icon: fontawesome/brands/discord
- link: https://discord.gg/zMM32dvNtd
- - icon: fontawesome/brands/twitter
- link: https://twitter.com/lancedb
+ link: https://discord.gg/lance
diff --git a/java/Makefile b/java/Makefile
index 87d14500..e5d9e550 100644
--- a/java/Makefile
+++ b/java/Makefile
@@ -62,6 +62,19 @@ lint-springboot-server: gen-apache-client gen-springboot-server
build-springboot-server: gen-springboot-server lint-springboot-server
./mvnw install -pl lance-namespace-springboot-server -am
+# lance-namespace-core module (hand-written, no codegen)
+.PHONY: lint-core
+lint-core: gen-apache-client
+ ./mvnw spotless:apply -pl lance-namespace-core -am
+
+.PHONY: build-core
+build-core: build-apache-client lint-core
+ ./mvnw install -pl lance-namespace-core -am
+
+.PHONY: check-core
+check-core:
+ ./mvnw checkstyle:check spotless:check -pl lance-namespace-core -am
+
.PHONY: clean
clean: clean-apache-client clean-springboot-server
@@ -77,10 +90,10 @@ check-springboot-server:
./mvnw checkstyle:check spotless:check -pl lance-namespace-springboot-server -am
.PHONY: check
-check: check-apache-client check-springboot-server
+check: check-apache-client check-springboot-server check-core
.PHONY: lint
-lint: lint-apache-client lint-springboot-server
+lint: lint-apache-client lint-springboot-server lint-core
.PHONY: build
-build: build-apache-client build-springboot-server
\ No newline at end of file
+build: build-apache-client build-springboot-server build-core
\ No newline at end of file
diff --git a/java/lance-namespace-core/pom.xml b/java/lance-namespace-core/pom.xml
new file mode 100644
index 00000000..cfb22c35
--- /dev/null
+++ b/java/lance-namespace-core/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+
+ org.lance
+ lance-namespace-root
+ 0.1.0
+
+
+ lance-namespace-core
+ jar
+
+ lance-namespace-core
+ Lance Namespace interface and plugin registry
+
+
+
+
+ org.lance
+ lance-namespace-apache-client
+ ${project.version}
+
+
+
+
+ org.apache.arrow
+ arrow-memory-core
+ ${arrow.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.apache.arrow
+ arrow-memory-netty
+ ${arrow.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+ 1.8
+ 1.8
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
diff --git a/java/lance-namespace-core/src/main/java/org/lance/namespace/LanceNamespace.java b/java/lance-namespace-core/src/main/java/org/lance/namespace/LanceNamespace.java
new file mode 100644
index 00000000..b01601ba
--- /dev/null
+++ b/java/lance-namespace-core/src/main/java/org/lance/namespace/LanceNamespace.java
@@ -0,0 +1,423 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.lance.namespace;
+
+import org.lance.namespace.model.*;
+
+import org.apache.arrow.memory.BufferAllocator;
+
+import java.lang.reflect.Constructor;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Interface for LanceDB namespace operations.
+ *
+ *
A namespace provides hierarchical organization for tables and supports various storage
+ * backends (local filesystem, S3, Azure, GCS) with optional credential vending for cloud providers.
+ *
+ *
Implementations of this interface can provide different storage backends. Native
+ * implementations (DirectoryNamespace, RestNamespace) are provided by the lance package. External
+ * libraries can implement this interface to provide integration with catalog systems like AWS Glue,
+ * Hive Metastore, or Databricks Unity Catalog.
+ *
+ *
Most methods have default implementations that throw {@link UnsupportedOperationException}.
+ * Implementations should override the methods they support.
+ *
+ *
Use {@link #connect(String, Map, BufferAllocator)} to create namespace instances, and {@link
+ * #registerNamespaceImpl(String, String)} to register external implementations.
+ */
+public interface LanceNamespace {
+
+ // ========== Static Registry and Factory Methods ==========
+
+ /** Native implementations (provided by lance package). */
+ Map NATIVE_IMPLS =
+ Collections.unmodifiableMap(
+ new HashMap() {
+ {
+ put("dir", "org.lance.namespace.DirectoryNamespace");
+ put("rest", "org.lance.namespace.RestNamespace");
+ }
+ });
+
+ /** Plugin registry for external implementations. Thread-safe for concurrent access. */
+ Map REGISTERED_IMPLS = new ConcurrentHashMap<>();
+
+ /**
+ * Register a namespace implementation with a short name.
+ *
+ *
External libraries can use this to register their implementations, allowing users to use
+ * short names like "glue" instead of full class paths.
+ *
+ * @param name Short name for the implementation (e.g., "glue", "hive2", "unity")
+ * @param className Full class name (e.g., "org.lance.namespace.glue.GlueNamespace")
+ */
+ static void registerNamespaceImpl(String name, String className) {
+ REGISTERED_IMPLS.put(name, className);
+ }
+
+ /**
+ * Unregister a previously registered namespace implementation.
+ *
+ * @param name Short name of the implementation to unregister
+ * @return true if an implementation was removed, false if it wasn't registered
+ */
+ static boolean unregisterNamespaceImpl(String name) {
+ return REGISTERED_IMPLS.remove(name) != null;
+ }
+
+ /**
+ * Check if an implementation is registered with the given name.
+ *
+ * @param name Short name or class name to check
+ * @return true if the implementation is available
+ */
+ static boolean isRegistered(String name) {
+ return NATIVE_IMPLS.containsKey(name) || REGISTERED_IMPLS.containsKey(name);
+ }
+
+ /**
+ * Connect to a Lance namespace implementation.
+ *
+ *
This factory method creates namespace instances based on implementation aliases or full
+ * class names. It provides a unified way to instantiate different namespace backends.
+ *
+ * @param impl Implementation alias or full class name. Built-in aliases: "dir" for
+ * DirectoryNamespace, "rest" for RestNamespace (provided by lance package). External
+ * libraries can register additional aliases using {@link #registerNamespaceImpl(String,
+ * String)}.
+ * @param properties Configuration properties passed to the namespace
+ * @param allocator Arrow buffer allocator for memory management
+ * @return The connected namespace instance
+ * @throws IllegalArgumentException If the implementation class cannot be loaded or does not
+ * implement LanceNamespace interface
+ */
+ static LanceNamespace connect(
+ String impl, Map properties, BufferAllocator allocator) {
+ // Check native impls first, then registered plugins, then treat as full class name
+ String className = NATIVE_IMPLS.get(impl);
+ if (className == null) {
+ className = REGISTERED_IMPLS.get(impl);
+ }
+ if (className == null) {
+ className = impl;
+ }
+
+ try {
+ Class> clazz = Class.forName(className);
+
+ if (!LanceNamespace.class.isAssignableFrom(clazz)) {
+ throw new IllegalArgumentException(
+ "Class " + className + " does not implement LanceNamespace interface");
+ }
+
+ @SuppressWarnings("unchecked")
+ Class extends LanceNamespace> namespaceClass = (Class extends LanceNamespace>) clazz;
+
+ Constructor extends LanceNamespace> constructor = namespaceClass.getConstructor();
+ LanceNamespace namespace = constructor.newInstance();
+ namespace.initialize(properties, allocator);
+
+ return namespace;
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Namespace implementation class not found: " + className);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ "Namespace implementation class " + className + " must have a no-arg constructor");
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Failed to construct namespace impl " + className + ": " + e.getMessage(), e);
+ }
+ }
+
+ // ========== Instance Methods ==========
+
+ /**
+ * Initialize the namespace with configuration properties.
+ *
+ * @param configProperties Configuration properties (e.g., root path, storage options)
+ * @param allocator Arrow buffer allocator for memory management
+ */
+ void initialize(Map configProperties, BufferAllocator allocator);
+
+ /**
+ * Return a human-readable unique identifier for this namespace instance.
+ *
+ *
This is used for equality comparison and caching. Two namespace instances with the same ID
+ * are considered equal and will share cached resources.
+ *
+ * @return A human-readable unique identifier string
+ */
+ String namespaceId();
+
+ // Namespace operations
+
+ /**
+ * List namespaces.
+ *
+ * @param request The list namespaces request
+ * @return The list namespaces response
+ */
+ default ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
+ throw new UnsupportedOperationException("Not supported: listNamespaces");
+ }
+
+ /**
+ * Describe a namespace.
+ *
+ * @param request The describe namespace request
+ * @return The describe namespace response
+ */
+ default DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest request) {
+ throw new UnsupportedOperationException("Not supported: describeNamespace");
+ }
+
+ /**
+ * Create a new namespace.
+ *
+ * @param request The create namespace request
+ * @return The create namespace response
+ */
+ default CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
+ throw new UnsupportedOperationException("Not supported: createNamespace");
+ }
+
+ /**
+ * Drop a namespace.
+ *
+ * @param request The drop namespace request
+ * @return The drop namespace response
+ */
+ default DropNamespaceResponse dropNamespace(DropNamespaceRequest request) {
+ throw new UnsupportedOperationException("Not supported: dropNamespace");
+ }
+
+ /**
+ * Check if a namespace exists.
+ *
+ * @param request The namespace exists request
+ * @throws RuntimeException if the namespace does not exist
+ */
+ default void namespaceExists(NamespaceExistsRequest request) {
+ throw new UnsupportedOperationException("Not supported: namespaceExists");
+ }
+
+ // Table operations
+
+ /**
+ * List tables in a namespace.
+ *
+ * @param request The list tables request
+ * @return The list tables response
+ */
+ default ListTablesResponse listTables(ListTablesRequest request) {
+ throw new UnsupportedOperationException("Not supported: listTables");
+ }
+
+ /**
+ * Describe a table.
+ *
+ * @param request The describe table request
+ * @return The describe table response
+ */
+ default DescribeTableResponse describeTable(DescribeTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: describeTable");
+ }
+
+ /**
+ * Register a table.
+ *
+ * @param request The register table request
+ * @return The register table response
+ */
+ default RegisterTableResponse registerTable(RegisterTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: registerTable");
+ }
+
+ /**
+ * Check if a table exists.
+ *
+ * @param request The table exists request
+ * @throws RuntimeException if the table does not exist
+ */
+ default void tableExists(TableExistsRequest request) {
+ throw new UnsupportedOperationException("Not supported: tableExists");
+ }
+
+ /**
+ * Drop a table.
+ *
+ * @param request The drop table request
+ * @return The drop table response
+ */
+ default DropTableResponse dropTable(DropTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: dropTable");
+ }
+
+ /**
+ * Deregister a table.
+ *
+ * @param request The deregister table request
+ * @return The deregister table response
+ */
+ default DeregisterTableResponse deregisterTable(DeregisterTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: deregisterTable");
+ }
+
+ /**
+ * Count rows in a table.
+ *
+ * @param request The count table rows request
+ * @return The row count
+ */
+ default Long countTableRows(CountTableRowsRequest request) {
+ throw new UnsupportedOperationException("Not supported: countTableRows");
+ }
+
+ // Data operations
+
+ /**
+ * Create a new table with data from Arrow IPC stream.
+ *
+ * @param request The create table request
+ * @param requestData Arrow IPC stream data
+ * @return The create table response
+ */
+ default CreateTableResponse createTable(CreateTableRequest request, byte[] requestData) {
+ throw new UnsupportedOperationException("Not supported: createTable");
+ }
+
+ /**
+ * Create an empty table (metadata only operation).
+ *
+ * @param request The create empty table request
+ * @return The create empty table response
+ */
+ default CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: createEmptyTable");
+ }
+
+ /**
+ * Insert data into a table.
+ *
+ * @param request The insert into table request
+ * @param requestData Arrow IPC stream data
+ * @return The insert into table response
+ */
+ default InsertIntoTableResponse insertIntoTable(
+ InsertIntoTableRequest request, byte[] requestData) {
+ throw new UnsupportedOperationException("Not supported: insertIntoTable");
+ }
+
+ /**
+ * Merge insert data into a table.
+ *
+ * @param request The merge insert into table request
+ * @param requestData Arrow IPC stream data
+ * @return The merge insert into table response
+ */
+ default MergeInsertIntoTableResponse mergeInsertIntoTable(
+ MergeInsertIntoTableRequest request, byte[] requestData) {
+ throw new UnsupportedOperationException("Not supported: mergeInsertIntoTable");
+ }
+
+ /**
+ * Update a table.
+ *
+ * @param request The update table request
+ * @return The update table response
+ */
+ default UpdateTableResponse updateTable(UpdateTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: updateTable");
+ }
+
+ /**
+ * Delete from a table.
+ *
+ * @param request The delete from table request
+ * @return The delete from table response
+ */
+ default DeleteFromTableResponse deleteFromTable(DeleteFromTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: deleteFromTable");
+ }
+
+ /**
+ * Query a table.
+ *
+ * @param request The query table request
+ * @return Arrow IPC stream data containing query results
+ */
+ default byte[] queryTable(QueryTableRequest request) {
+ throw new UnsupportedOperationException("Not supported: queryTable");
+ }
+
+ // Index operations
+
+ /**
+ * Create a table index.
+ *
+ * @param request The create table index request
+ * @return The create table index response
+ */
+ default CreateTableIndexResponse createTableIndex(CreateTableIndexRequest request) {
+ throw new UnsupportedOperationException("Not supported: createTableIndex");
+ }
+
+ /**
+ * List table indices.
+ *
+ * @param request The list table indices request
+ * @return The list table indices response
+ */
+ default ListTableIndicesResponse listTableIndices(ListTableIndicesRequest request) {
+ throw new UnsupportedOperationException("Not supported: listTableIndices");
+ }
+
+ /**
+ * Describe table index statistics.
+ *
+ * @param request The describe table index stats request
+ * @param indexName The name of the index
+ * @return The describe table index stats response
+ */
+ default DescribeTableIndexStatsResponse describeTableIndexStats(
+ DescribeTableIndexStatsRequest request, String indexName) {
+ throw new UnsupportedOperationException("Not supported: describeTableIndexStats");
+ }
+
+ // Transaction operations
+
+ /**
+ * Describe a transaction.
+ *
+ * @param request The describe transaction request
+ * @return The describe transaction response
+ */
+ default DescribeTransactionResponse describeTransaction(DescribeTransactionRequest request) {
+ throw new UnsupportedOperationException("Not supported: describeTransaction");
+ }
+
+ /**
+ * Alter a transaction.
+ *
+ * @param request The alter transaction request
+ * @return The alter transaction response
+ */
+ default AlterTransactionResponse alterTransaction(AlterTransactionRequest request) {
+ throw new UnsupportedOperationException("Not supported: alterTransaction");
+ }
+}
diff --git a/java/lance-namespace-core/src/test/java/org/lance/namespace/LanceNamespaceTest.java b/java/lance-namespace-core/src/test/java/org/lance/namespace/LanceNamespaceTest.java
new file mode 100644
index 00000000..e70d2dd2
--- /dev/null
+++ b/java/lance-namespace-core/src/test/java/org/lance/namespace/LanceNamespaceTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.lance.namespace;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/** Tests for LanceNamespace interface and registry. */
+public class LanceNamespaceTest {
+
+ @BeforeEach
+ void setUp() {
+ // Clear registered implementations before each test
+ LanceNamespace.REGISTERED_IMPLS.clear();
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Clear registered implementations after each test
+ LanceNamespace.REGISTERED_IMPLS.clear();
+ }
+
+ @Test
+ void testNativeImplsDefined() {
+ // Test that native implementations are defined
+ assertTrue(LanceNamespace.NATIVE_IMPLS.containsKey("dir"));
+ assertTrue(LanceNamespace.NATIVE_IMPLS.containsKey("rest"));
+ assertEquals("org.lance.namespace.DirectoryNamespace", LanceNamespace.NATIVE_IMPLS.get("dir"));
+ assertEquals("org.lance.namespace.RestNamespace", LanceNamespace.NATIVE_IMPLS.get("rest"));
+ }
+
+ @Test
+ void testRegisterNamespaceImpl() {
+ // Test registering a custom implementation
+ LanceNamespace.registerNamespaceImpl("mock", "org.lance.namespace.MockNamespace");
+ assertTrue(LanceNamespace.REGISTERED_IMPLS.containsKey("mock"));
+ assertEquals("org.lance.namespace.MockNamespace", LanceNamespace.REGISTERED_IMPLS.get("mock"));
+ }
+
+ @Test
+ void testUnregisterNamespaceImpl() {
+ // Test unregistering an implementation
+ LanceNamespace.registerNamespaceImpl("mock", "org.lance.namespace.MockNamespace");
+ assertTrue(LanceNamespace.unregisterNamespaceImpl("mock"));
+ assertFalse(LanceNamespace.REGISTERED_IMPLS.containsKey("mock"));
+
+ // Test unregistering non-existent implementation
+ assertFalse(LanceNamespace.unregisterNamespaceImpl("nonexistent"));
+ }
+
+ @Test
+ void testIsRegistered() {
+ // Test checking if implementation is registered
+ assertTrue(LanceNamespace.isRegistered("dir")); // Native impl
+ assertTrue(LanceNamespace.isRegistered("rest")); // Native impl
+ assertFalse(LanceNamespace.isRegistered("mock"));
+
+ // Register and check again
+ LanceNamespace.registerNamespaceImpl("mock", "org.lance.namespace.MockNamespace");
+ assertTrue(LanceNamespace.isRegistered("mock"));
+ }
+
+ @Test
+ void testConnectWithFullClassPath() {
+ // Test connecting using full class path (inner class uses $ separator)
+ Map properties = new HashMap<>();
+ properties.put("id", "test");
+
+ LanceNamespace ns =
+ LanceNamespace.connect(
+ "org.lance.namespace.LanceNamespaceTest$MockNamespace", properties, null);
+ assertNotNull(ns);
+ assertTrue(ns instanceof MockNamespace);
+ assertTrue(ns.namespaceId().contains("test"));
+ }
+
+ @Test
+ void testConnectWithRegisteredImpl() {
+ // Test connecting using registered implementation alias
+ LanceNamespace.registerNamespaceImpl(
+ "mock", "org.lance.namespace.LanceNamespaceTest$MockNamespace");
+
+ Map properties = new HashMap<>();
+ properties.put("id", "test-registered");
+
+ LanceNamespace ns = LanceNamespace.connect("mock", properties, null);
+ assertNotNull(ns);
+ assertTrue(ns instanceof MockNamespace);
+ assertTrue(ns.namespaceId().contains("test-registered"));
+ }
+
+ @Test
+ void testConnectInvalidClassPath() {
+ // Test that invalid class path throws IllegalArgumentException
+ Map properties = new HashMap<>();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> LanceNamespace.connect("non.existent.Namespace", properties, null));
+ }
+
+ @Test
+ void testConnectNonNamespaceClass() {
+ // Test that non-LanceNamespace class throws IllegalArgumentException
+ Map properties = new HashMap<>();
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ LanceNamespace.connect(
+ "org.lance.namespace.LanceNamespaceTest$NotANamespace", properties, null));
+ assertTrue(ex.getMessage().contains("does not implement LanceNamespace"));
+ }
+
+ @Test
+ void testDefaultMethodsThrowUnsupportedOperation() {
+ // Test that default methods throw UnsupportedOperationException
+ MockNamespace ns = new MockNamespace();
+ ns.initialize(new HashMap<>(), null);
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> ns.listNamespaces(new org.lance.namespace.model.ListNamespacesRequest()));
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> ns.listTables(new org.lance.namespace.model.ListTablesRequest()));
+ }
+
+ /** Mock namespace implementation for testing. */
+ public static class MockNamespace implements LanceNamespace {
+ private String id = "default";
+
+ public MockNamespace() {
+ // No-arg constructor required for reflection-based instantiation
+ }
+
+ @Override
+ public void initialize(Map configProperties, BufferAllocator allocator) {
+ if (configProperties.containsKey("id")) {
+ this.id = configProperties.get("id");
+ }
+ }
+
+ @Override
+ public String namespaceId() {
+ return "MockNamespace { id: '" + id + "' }";
+ }
+ }
+
+ /** A class that doesn't implement LanceNamespace for testing rejection. */
+ public static class NotANamespace {
+ public NotANamespace() {}
+ }
+}
diff --git a/java/pom.xml b/java/pom.xml
index 1fec0171..b087b2ca 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -12,7 +12,7 @@
${project.artifactId}Lance Namespace
- https://lancedb.github.io/lance-namespace
+ https://lance.org/format/namespace/
@@ -23,23 +23,23 @@
- scm:git:git@github.com:lancedb/lance-namespace.git
- scm:git:git@github.com:lancedb/lance-namespace.git
- git@github.com:lancedb/lance-namespace.git
+ scm:git:git@github.com:lance-format/lance-namespace.git
+ scm:git:git@github.com:lance-format/lance-namespace.git
+ git@github.com:lance-format/lance-namespace.gitHEADGitHub
- https://github.com/lancedb/lance-namespace/issues
+ https://github.com/lance-format/lance-namespace/issues
- LanceDB Developers
- developers@lancedb.com
- LanceDB
- https://lancedb.com
+ Lance Developers
+ dev@lance.org
+ Lance Format
+ https://lance.org
@@ -89,6 +89,7 @@
lance-namespace-apache-clientlance-namespace-springboot-server
+ lance-namespace-core
diff --git a/pyproject.toml b/pyproject.toml
index 7f4e987f..8a045fab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,7 +18,9 @@ dev = [
[tool.uv.workspace]
members = [
"python/lance_namespace_urllib3_client",
+ "python/lance_namespace",
]
[tool.uv.sources]
-lance-namespace-urllib3-client = { workspace = true }
\ No newline at end of file
+lance-namespace-urllib3-client = { workspace = true }
+lance-namespace = { workspace = true }
\ No newline at end of file
diff --git a/python/Makefile b/python/Makefile
index 64635dd4..2cce4fe5 100644
--- a/python/Makefile
+++ b/python/Makefile
@@ -40,6 +40,18 @@ publish-urllib3-client:
cd lance_namespace_urllib3_client; \
uv build
+# lance_namespace package (hand-written, no codegen)
+.PHONY: build-ns
+build-ns: build-urllib3-client
+ cd lance_namespace; \
+ uv sync; \
+ uv run pytest
+
+.PHONY: publish-ns
+publish-ns:
+ cd lance_namespace; \
+ uv build
+
.PHONY: clean
clean: clean-urllib3-client
@@ -47,7 +59,7 @@ clean: clean-urllib3-client
gen: gen-urllib3-client
.PHONY: build
-build: build-urllib3-client
+build: build-urllib3-client build-ns
.PHONY: publish
-publish: publish-urllib3-client
\ No newline at end of file
+publish: publish-urllib3-client publish-ns
\ No newline at end of file
diff --git a/python/lance_namespace/README.md b/python/lance_namespace/README.md
new file mode 100644
index 00000000..742f088e
--- /dev/null
+++ b/python/lance_namespace/README.md
@@ -0,0 +1,47 @@
+# lance-namespace
+
+Lance Namespace interface and plugin registry.
+
+## Overview
+
+This package provides:
+- `LanceNamespace` ABC interface for namespace implementations
+- `connect()` factory function for creating namespace instances
+- `register_namespace_impl()` for external implementation registration
+- Re-exported model types from `lance_namespace_urllib3_client`
+
+## Installation
+
+```bash
+pip install lance-namespace
+```
+
+## Usage
+
+```python
+import lance_namespace
+
+# Connect using native implementations (requires lance package)
+ns = lance_namespace.connect("dir", {"root": "/path/to/data"})
+ns = lance_namespace.connect("rest", {"uri": "http://localhost:4099"})
+
+# Register a custom implementation
+lance_namespace.register_namespace_impl("glue", "lance_glue.GlueNamespace")
+ns = lance_namespace.connect("glue", {"catalog": "my_catalog"})
+```
+
+## Creating Custom Implementations
+
+```python
+from lance_namespace import LanceNamespace
+
+class MyNamespace(LanceNamespace):
+ def namespace_id(self) -> str:
+ return "MyNamespace { ... }"
+
+ # Override other methods as needed
+```
+
+## License
+
+Apache-2.0
diff --git a/python/lance_namespace/__init__.py b/python/lance_namespace/__init__.py
new file mode 100644
index 00000000..448c1e5a
--- /dev/null
+++ b/python/lance_namespace/__init__.py
@@ -0,0 +1,378 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Lance Namespace interface and plugin registry.
+
+This module provides:
+1. LanceNamespace ABC interface for namespace implementations
+2. connect() factory function for creating namespace instances
+3. register_namespace_impl() for external implementation registration
+4. Re-exported model types from lance_namespace_urllib3_client
+
+The actual implementations (DirectoryNamespace, RestNamespace) are provided
+by the lance package. This package only provides the abstract interface
+and plugin registration mechanism.
+"""
+
+import importlib
+from abc import ABC, abstractmethod
+from typing import Dict
+
+from lance_namespace_urllib3_client.models import (
+ AlterTransactionRequest,
+ AlterTransactionResponse,
+ CountTableRowsRequest,
+ CreateEmptyTableRequest,
+ CreateEmptyTableResponse,
+ CreateNamespaceRequest,
+ CreateNamespaceResponse,
+ CreateTableIndexRequest,
+ CreateTableIndexResponse,
+ CreateTableRequest,
+ CreateTableResponse,
+ DeleteFromTableRequest,
+ DeleteFromTableResponse,
+ DeregisterTableRequest,
+ DeregisterTableResponse,
+ DescribeNamespaceRequest,
+ DescribeNamespaceResponse,
+ DescribeTableIndexStatsRequest,
+ DescribeTableIndexStatsResponse,
+ DescribeTableRequest,
+ DescribeTableResponse,
+ DescribeTransactionRequest,
+ DescribeTransactionResponse,
+ DropNamespaceRequest,
+ DropNamespaceResponse,
+ DropTableRequest,
+ DropTableResponse,
+ InsertIntoTableRequest,
+ InsertIntoTableResponse,
+ ListNamespacesRequest,
+ ListNamespacesResponse,
+ ListTableIndicesRequest,
+ ListTableIndicesResponse,
+ ListTablesRequest,
+ ListTablesResponse,
+ MergeInsertIntoTableRequest,
+ MergeInsertIntoTableResponse,
+ NamespaceExistsRequest,
+ QueryTableRequest,
+ RegisterTableRequest,
+ RegisterTableResponse,
+ TableExistsRequest,
+ UpdateTableRequest,
+ UpdateTableResponse,
+)
+
+__all__ = [
+ # Interface and factory
+ "LanceNamespace",
+ "connect",
+ "register_namespace_impl",
+ # Registry access
+ "NATIVE_IMPLS",
+ # Request/Response types (re-exported from lance_namespace_urllib3_client)
+ "AlterTransactionRequest",
+ "AlterTransactionResponse",
+ "CountTableRowsRequest",
+ "CreateEmptyTableRequest",
+ "CreateEmptyTableResponse",
+ "CreateNamespaceRequest",
+ "CreateNamespaceResponse",
+ "CreateTableIndexRequest",
+ "CreateTableIndexResponse",
+ "CreateTableRequest",
+ "CreateTableResponse",
+ "DeleteFromTableRequest",
+ "DeleteFromTableResponse",
+ "DeregisterTableRequest",
+ "DeregisterTableResponse",
+ "DescribeNamespaceRequest",
+ "DescribeNamespaceResponse",
+ "DescribeTableIndexStatsRequest",
+ "DescribeTableIndexStatsResponse",
+ "DescribeTableRequest",
+ "DescribeTableResponse",
+ "DescribeTransactionRequest",
+ "DescribeTransactionResponse",
+ "DropNamespaceRequest",
+ "DropNamespaceResponse",
+ "DropTableRequest",
+ "DropTableResponse",
+ "InsertIntoTableRequest",
+ "InsertIntoTableResponse",
+ "ListNamespacesRequest",
+ "ListNamespacesResponse",
+ "ListTableIndicesRequest",
+ "ListTableIndicesResponse",
+ "ListTablesRequest",
+ "ListTablesResponse",
+ "MergeInsertIntoTableRequest",
+ "MergeInsertIntoTableResponse",
+ "NamespaceExistsRequest",
+ "QueryTableRequest",
+ "RegisterTableRequest",
+ "RegisterTableResponse",
+ "TableExistsRequest",
+ "UpdateTableRequest",
+ "UpdateTableResponse",
+]
+
+
+class LanceNamespace(ABC):
+ """Base interface for Lance Namespace implementations.
+
+ This abstract base class defines the contract for namespace implementations
+ that manage Lance tables. Implementations can provide different storage backends
+ (directory-based, REST API, cloud catalogs, etc.).
+
+ To create a custom namespace implementation, subclass this ABC and implement
+ at least the `namespace_id()` method. Other methods have default implementations
+ that raise `NotImplementedError`.
+
+ Native implementations (DirectoryNamespace, RestNamespace) are provided by the
+ lance package. External integrations (Glue, Hive, Unity) can be registered
+ using `register_namespace_impl()`.
+ """
+
+ @abstractmethod
+ def namespace_id(self) -> str:
+ """Return a human-readable unique identifier for this namespace instance.
+
+ This is used for equality comparison and hashing when the namespace is
+ used as part of a storage options provider. Two namespace instances with
+ the same ID are considered equal and will share cached resources.
+
+ The ID should be human-readable for debugging and logging purposes.
+ For example:
+ - REST namespace: "RestNamespace { uri: 'https://api.example.com' }"
+ - Directory namespace: "DirectoryNamespace { root: '/path/to/data' }"
+
+ Returns
+ -------
+ str
+ A human-readable unique identifier string
+ """
+ pass
+
+ def list_namespaces(self, request: ListNamespacesRequest) -> ListNamespacesResponse:
+ """List namespaces."""
+ raise NotImplementedError("Not supported: list_namespaces")
+
+ def describe_namespace(
+ self, request: DescribeNamespaceRequest
+ ) -> DescribeNamespaceResponse:
+ """Describe a namespace."""
+ raise NotImplementedError("Not supported: describe_namespace")
+
+ def create_namespace(
+ self, request: CreateNamespaceRequest
+ ) -> CreateNamespaceResponse:
+ """Create a new namespace."""
+ raise NotImplementedError("Not supported: create_namespace")
+
+ def drop_namespace(self, request: DropNamespaceRequest) -> DropNamespaceResponse:
+ """Drop a namespace."""
+ raise NotImplementedError("Not supported: drop_namespace")
+
+ def namespace_exists(self, request: NamespaceExistsRequest) -> None:
+ """Check if a namespace exists."""
+ raise NotImplementedError("Not supported: namespace_exists")
+
+ def list_tables(self, request: ListTablesRequest) -> ListTablesResponse:
+ """List tables in a namespace."""
+ raise NotImplementedError("Not supported: list_tables")
+
+ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse:
+ """Describe a table."""
+ raise NotImplementedError("Not supported: describe_table")
+
+ def register_table(self, request: RegisterTableRequest) -> RegisterTableResponse:
+ """Register a table."""
+ raise NotImplementedError("Not supported: register_table")
+
+ def table_exists(self, request: TableExistsRequest) -> None:
+ """Check if a table exists."""
+ raise NotImplementedError("Not supported: table_exists")
+
+ def drop_table(self, request: DropTableRequest) -> DropTableResponse:
+ """Drop a table."""
+ raise NotImplementedError("Not supported: drop_table")
+
+ def deregister_table(
+ self, request: DeregisterTableRequest
+ ) -> DeregisterTableResponse:
+ """Deregister a table."""
+ raise NotImplementedError("Not supported: deregister_table")
+
+ def count_table_rows(self, request: CountTableRowsRequest) -> int:
+ """Count rows in a table."""
+ raise NotImplementedError("Not supported: count_table_rows")
+
+ def create_table(
+ self, request: CreateTableRequest, request_data: bytes
+ ) -> CreateTableResponse:
+ """Create a new table with data from Arrow IPC stream."""
+ raise NotImplementedError("Not supported: create_table")
+
+ def create_empty_table(
+ self, request: CreateEmptyTableRequest
+ ) -> CreateEmptyTableResponse:
+ """Create an empty table (metadata only operation)."""
+ raise NotImplementedError("Not supported: create_empty_table")
+
+ def insert_into_table(
+ self, request: InsertIntoTableRequest, request_data: bytes
+ ) -> InsertIntoTableResponse:
+ """Insert data into a table."""
+ raise NotImplementedError("Not supported: insert_into_table")
+
+ def merge_insert_into_table(
+ self, request: MergeInsertIntoTableRequest, request_data: bytes
+ ) -> MergeInsertIntoTableResponse:
+ """Merge insert data into a table."""
+ raise NotImplementedError("Not supported: merge_insert_into_table")
+
+ def update_table(self, request: UpdateTableRequest) -> UpdateTableResponse:
+ """Update a table."""
+ raise NotImplementedError("Not supported: update_table")
+
+ def delete_from_table(
+ self, request: DeleteFromTableRequest
+ ) -> DeleteFromTableResponse:
+ """Delete from a table."""
+ raise NotImplementedError("Not supported: delete_from_table")
+
+ def query_table(self, request: QueryTableRequest) -> bytes:
+ """Query a table."""
+ raise NotImplementedError("Not supported: query_table")
+
+ def create_table_index(
+ self, request: CreateTableIndexRequest
+ ) -> CreateTableIndexResponse:
+ """Create a table index."""
+ raise NotImplementedError("Not supported: create_table_index")
+
+ def list_table_indices(
+ self, request: ListTableIndicesRequest
+ ) -> ListTableIndicesResponse:
+ """List table indices."""
+ raise NotImplementedError("Not supported: list_table_indices")
+
+ def describe_table_index_stats(
+ self, request: DescribeTableIndexStatsRequest
+ ) -> DescribeTableIndexStatsResponse:
+ """Describe table index statistics."""
+ raise NotImplementedError("Not supported: describe_table_index_stats")
+
+ def describe_transaction(
+ self, request: DescribeTransactionRequest
+ ) -> DescribeTransactionResponse:
+ """Describe a transaction."""
+ raise NotImplementedError("Not supported: describe_transaction")
+
+ def alter_transaction(
+ self, request: AlterTransactionRequest
+ ) -> AlterTransactionResponse:
+ """Alter a transaction."""
+ raise NotImplementedError("Not supported: alter_transaction")
+
+
+# Native implementations (provided by lance package)
+NATIVE_IMPLS: Dict[str, str] = {
+ "rest": "lance.namespace.RestNamespace",
+ "dir": "lance.namespace.DirectoryNamespace",
+}
+
+# Plugin registry for external implementations
+_REGISTERED_IMPLS: Dict[str, str] = {}
+
+
+def register_namespace_impl(name: str, class_path: str) -> None:
+ """Register a namespace implementation with a short name.
+
+ External libraries can use this to register their implementations,
+ allowing users to use short names like "glue" instead of full class paths.
+
+ Parameters
+ ----------
+ name : str
+ Short name for the implementation (e.g., "glue", "hive2", "unity")
+ class_path : str
+ Full class path (e.g., "lance_glue.GlueNamespace")
+
+ Examples
+ --------
+ >>> # Register a custom implementation
+ >>> register_namespace_impl("glue", "lance_glue.GlueNamespace")
+ >>> # Now users can use: connect("glue", {"catalog": "my_catalog"})
+ """
+ _REGISTERED_IMPLS[name] = class_path
+
+
+def connect(impl: str, properties: Dict[str, str]) -> LanceNamespace:
+ """Connect to a Lance namespace implementation.
+
+ This factory function creates namespace instances based on implementation
+ aliases or full class paths. It provides a unified way to instantiate
+ different namespace backends.
+
+ Parameters
+ ----------
+ impl : str
+ Implementation alias or full class path. Built-in aliases:
+ - "rest": RestNamespace (REST API client, provided by lance)
+ - "dir": DirectoryNamespace (local/cloud filesystem, provided by lance)
+ You can also use full class paths like "my.custom.Namespace"
+ External libraries can register additional aliases using
+ `register_namespace_impl()`.
+ properties : Dict[str, str]
+ Configuration properties passed to the namespace constructor
+
+ Returns
+ -------
+ LanceNamespace
+ The connected namespace instance
+
+ Raises
+ ------
+ ValueError
+ If the implementation class cannot be loaded or does not
+ implement LanceNamespace interface
+
+ Examples
+ --------
+ >>> # Connect to a directory namespace (requires lance package)
+ >>> ns = connect("dir", {"root": "/path/to/data"})
+ >>>
+ >>> # Connect to a REST namespace (requires lance package)
+ >>> ns = connect("rest", {"uri": "http://localhost:4099"})
+ >>>
+ >>> # Use a full class path
+ >>> ns = connect("my_package.MyNamespace", {"key": "value"})
+ """
+ # Check native impls first, then registered plugins, then treat as full class path
+ impl_class = NATIVE_IMPLS.get(impl) or _REGISTERED_IMPLS.get(impl) or impl
+ try:
+ module_name, class_name = impl_class.rsplit(".", 1)
+ module = importlib.import_module(module_name)
+ namespace_class = getattr(module, class_name)
+
+ if not issubclass(namespace_class, LanceNamespace):
+ raise ValueError(
+ f"Class {impl_class} does not implement LanceNamespace interface"
+ )
+
+ return namespace_class(**properties)
+ except Exception as e:
+ raise ValueError(f"Failed to construct namespace impl {impl_class}: {e}")
diff --git a/python/lance_namespace/py.typed b/python/lance_namespace/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/python/lance_namespace/pyproject.toml b/python/lance_namespace/pyproject.toml
new file mode 100644
index 00000000..8d20932d
--- /dev/null
+++ b/python/lance_namespace/pyproject.toml
@@ -0,0 +1,32 @@
+[project]
+name = "lance-namespace"
+version = "0.1.0"
+description = "Lance Namespace interface and plugin registry"
+readme = "README.md"
+authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]
+license = { text = "Apache-2.0" }
+keywords = ["lance", "namespace", "lancedb", "vector-database"]
+requires-python = ">=3.8"
+dependencies = [
+ "lance-namespace-urllib3-client",
+]
+
+[dependency-groups]
+dev = [
+ "pytest>=7.2.1",
+ "pytest-cov>=2.8.1",
+]
+
+[project.urls]
+Repository = "https://github.com/lance-format/lance-namespace"
+Documentation = "https://lance.org/format/namespace/"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["lance_namespace"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
diff --git a/python/lance_namespace/tests/__init__.py b/python/lance_namespace/tests/__init__.py
new file mode 100644
index 00000000..4d9a9249
--- /dev/null
+++ b/python/lance_namespace/tests/__init__.py
@@ -0,0 +1,11 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/lance_namespace/tests/test_namespace.py b/python/lance_namespace/tests/test_namespace.py
new file mode 100644
index 00000000..5d44d28d
--- /dev/null
+++ b/python/lance_namespace/tests/test_namespace.py
@@ -0,0 +1,175 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for lance_namespace interface and registry."""
+
+import pytest
+
+from lance_namespace import (
+ LanceNamespace,
+ connect,
+ register_namespace_impl,
+ NATIVE_IMPLS,
+ _REGISTERED_IMPLS,
+ # Test model re-exports
+ ListNamespacesRequest,
+ ListNamespacesResponse,
+)
+
+
+class MockNamespace(LanceNamespace):
+ """Mock namespace implementation for testing."""
+
+ def __init__(self, **properties):
+ self._properties = properties
+ self._id = properties.get("id", "mock")
+
+ def namespace_id(self) -> str:
+ return f"MockNamespace {{ id: '{self._id}' }}"
+
+
+class TestLanceNamespaceInterface:
+ """Tests for the LanceNamespace ABC interface."""
+
+ def test_abstract_method_namespace_id(self):
+ """Test that namespace_id is abstract and must be implemented."""
+ # MockNamespace implements namespace_id, so it should work
+ ns = MockNamespace(id="test")
+ assert ns.namespace_id() == "MockNamespace { id: 'test' }"
+
+ def test_default_methods_raise_not_implemented(self):
+ """Test that default methods raise NotImplementedError."""
+ ns = MockNamespace()
+
+ with pytest.raises(NotImplementedError, match="list_namespaces"):
+ ns.list_namespaces(ListNamespacesRequest(parent=[]))
+
+ with pytest.raises(NotImplementedError, match="list_tables"):
+ from lance_namespace import ListTablesRequest
+
+ ns.list_tables(ListTablesRequest(namespace=[]))
+
+
+class TestRegisterNamespaceImpl:
+ """Tests for register_namespace_impl function."""
+
+ def setup_method(self):
+ """Clear registered implementations before each test."""
+ _REGISTERED_IMPLS.clear()
+
+ def teardown_method(self):
+ """Clear registered implementations after each test."""
+ _REGISTERED_IMPLS.clear()
+
+ def test_register_implementation(self):
+ """Test registering a custom implementation."""
+ register_namespace_impl("mock", "lance_namespace.tests.test_namespace.MockNamespace")
+ assert "mock" in _REGISTERED_IMPLS
+ assert _REGISTERED_IMPLS["mock"] == "lance_namespace.tests.test_namespace.MockNamespace"
+
+ def test_register_overwrites_existing(self):
+ """Test that registering with same name overwrites."""
+ register_namespace_impl("mock", "path.to.OldNamespace")
+ register_namespace_impl("mock", "path.to.NewNamespace")
+ assert _REGISTERED_IMPLS["mock"] == "path.to.NewNamespace"
+
+
+class TestConnect:
+ """Tests for connect factory function."""
+
+ def setup_method(self):
+ """Clear registered implementations before each test."""
+ _REGISTERED_IMPLS.clear()
+
+ def teardown_method(self):
+ """Clear registered implementations after each test."""
+ _REGISTERED_IMPLS.clear()
+
+ def test_connect_with_full_class_path(self):
+ """Test connecting using full class path."""
+ ns = connect(
+ "lance_namespace.tests.test_namespace.MockNamespace",
+ {"id": "test-full-path"},
+ )
+ assert isinstance(ns, LanceNamespace)
+ assert isinstance(ns, MockNamespace)
+ assert "test-full-path" in ns.namespace_id()
+
+ def test_connect_with_registered_impl(self):
+ """Test connecting using registered implementation alias."""
+ register_namespace_impl("mock", "lance_namespace.tests.test_namespace.MockNamespace")
+ ns = connect("mock", {"id": "test-registered"})
+ assert isinstance(ns, MockNamespace)
+ assert "test-registered" in ns.namespace_id()
+
+ def test_connect_passes_properties(self):
+ """Test that properties are passed to the constructor."""
+ ns = connect(
+ "lance_namespace.tests.test_namespace.MockNamespace",
+ {"id": "prop-test", "extra": "value"},
+ )
+ assert ns._properties["id"] == "prop-test"
+ assert ns._properties["extra"] == "value"
+
+ def test_connect_invalid_class_path(self):
+ """Test that invalid class path raises ValueError."""
+ with pytest.raises(ValueError, match="Failed to construct"):
+ connect("non.existent.Namespace", {})
+
+ def test_connect_non_namespace_class(self):
+ """Test that non-LanceNamespace class raises ValueError."""
+ with pytest.raises(ValueError, match="does not implement LanceNamespace"):
+ connect("lance_namespace.tests.test_namespace.NotANamespace", {})
+
+ def test_native_impls_defined(self):
+ """Test that native implementations are defined."""
+ assert "dir" in NATIVE_IMPLS
+ assert "rest" in NATIVE_IMPLS
+ assert NATIVE_IMPLS["dir"] == "lance.namespace.DirectoryNamespace"
+ assert NATIVE_IMPLS["rest"] == "lance.namespace.RestNamespace"
+
+
+class TestModelReexports:
+ """Tests for model re-exports from lance_namespace_urllib3_client."""
+
+ def test_request_types_exported(self):
+ """Test that request types are re-exported."""
+ from lance_namespace import (
+ CreateNamespaceRequest,
+ ListTablesRequest,
+ DescribeTableRequest,
+ )
+
+ # Just verify they can be imported and are the right types
+ assert CreateNamespaceRequest is not None
+ assert ListTablesRequest is not None
+ assert DescribeTableRequest is not None
+
+ def test_response_types_exported(self):
+ """Test that response types are re-exported."""
+ from lance_namespace import (
+ CreateNamespaceResponse,
+ ListTablesResponse,
+ DescribeTableResponse,
+ )
+
+ assert CreateNamespaceResponse is not None
+ assert ListTablesResponse is not None
+ assert DescribeTableResponse is not None
+
+
+# Helper class for testing non-namespace class rejection
+class NotANamespace:
+ """A class that doesn't implement LanceNamespace."""
+
+ def __init__(self, **properties):
+ pass
diff --git a/python/lance_namespace_urllib3_client/pyproject.toml b/python/lance_namespace_urllib3_client/pyproject.toml
index 561d2059..4f13b7cf 100644
--- a/python/lance_namespace_urllib3_client/pyproject.toml
+++ b/python/lance_namespace_urllib3_client/pyproject.toml
@@ -3,7 +3,7 @@ name = "lance-namespace-urllib3-client"
version = "0.1.0"
description = "Lance Namespace Specification"
readme = "README.md"
-authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]
+authors = [{ name = "Lance Devs", email = "dev@lance.org" }]
license = { text = "Apache-2.0" }
keywords = ["OpenAPI", "OpenAPI-Generator", "Lance Namespace Specification"]
requires-python = ">=3.8"
diff --git a/python/pyproject.urllib3_client.toml b/python/pyproject.urllib3_client.toml
index 561d2059..4f13b7cf 100644
--- a/python/pyproject.urllib3_client.toml
+++ b/python/pyproject.urllib3_client.toml
@@ -3,7 +3,7 @@ name = "lance-namespace-urllib3-client"
version = "0.1.0"
description = "Lance Namespace Specification"
readme = "README.md"
-authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]
+authors = [{ name = "Lance Devs", email = "dev@lance.org" }]
license = { text = "Apache-2.0" }
keywords = ["OpenAPI", "OpenAPI-Generator", "Lance Namespace Specification"]
requires-python = ">=3.8"
diff --git a/uv.lock b/uv.lock
index cd2d184e..04c311a2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -8,6 +8,7 @@ resolution-markers = [
[manifest]
members = [
+ "lance-namespace",
"lance-namespace-urllib3-client",
"lance-namespace-workspace",
]
@@ -612,6 +613,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
]
+[[package]]
+name = "lance-namespace"
+version = "0.1.0"
+source = { editable = "python/lance_namespace" }
+dependencies = [
+ { name = "lance-namespace-urllib3-client" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "lance-namespace-urllib3-client", editable = "python/lance_namespace_urllib3_client" }]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest", specifier = ">=7.2.1" },
+ { name = "pytest-cov", specifier = ">=2.8.1" },
+]
+
[[package]]
name = "lance-namespace-urllib3-client"
version = "0.1.0"