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 namespaceClass = (Class) clazz; + + Constructor 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.git HEAD GitHub - 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-client lance-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"