Skip to content

feat(sdk): first-class glossary term relations + system settings in Python & Java 2.0 SDKs#29788

Open
harshach wants to merge 2 commits into
mainfrom
harshach/glossary-relations-sdk
Open

feat(sdk): first-class glossary term relations + system settings in Python & Java 2.0 SDKs#29788
harshach wants to merge 2 commits into
mainfrom
harshach/glossary-relations-sdk

Conversation

@harshach

@harshach harshach commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Resolves #29789

What

Makes glossary term relations and the glossaryTermRelationSettings system setting first-class in both 2.0 SDKs, so callers no longer drop to the raw HTTP escape hatch (client().ometa.client in Python / getHttpClient().execute(...) in Java) for these operations. The underlying REST endpoints already exist on main — this is a thin, ergonomic wrapper layer.

API

Python (from metadata.sdk import GlossaryTerms, Settings)

Settings.define_glossary_relation_type(prescribes)          # idempotent, clobber-safe
GlossaryTerms.add_relation("HCP.DrJohnDoe", "Products.CardioDrugX", "prescribes")  # FQN or id
GlossaryTerms.remove_relation(hcp, drug, "prescribes")
GlossaryTerms.relations_graph(hcp, depth=2, relation_types=["prescribes"])
GlossaryTerms.relation_type_usage()

Java

client.settings().defineGlossaryRelationType(prescribes);   // idempotent
client.glossaryTerms().addRelation(hcpId, drugId, "prescribes");
client.glossaryTerms().removeRelation(hcpId, drugId, "prescribes");
client.glossaryTerms().relationGraph(hcpId, 2, List.of("prescribes"));
client.glossaryTerms().relationTypeUsage();
GlossaryTerms.find(hcpId).relateTo(drugFqn).as("prescribes").apply();   // fluent sugar

Design notes

  • Clobber-safe registration: define_glossary_relation_type / defineGlossaryRelationType register a type via JSON-Patch append (add /relationTypes/-) rather than GET→append→PUT. A full PUT replaces the whole relationTypes list, so concurrent callers on the shared glossaryTermRelationSettings global can overwrite each other's types; append only touches the caller's own entry. Both are idempotent (skip when a type of the same name already exists).
  • Escape hatch internalized: FQN→id resolution, the TermRelation payload shape, and json-patch construction all move inside the SDK. In Java, a JsonNode PATCH body auto-selects application/json-patch+json.
  • IT ported: GlossaryTermRelationsIT now drives the SDK methods instead of hand-rolled java.net.http calls — the end-to-end proof.

Tests

  • GlossaryTermRelationsMockTest (6) — mocks the HttpClient boundary and asserts the real URL, HTTP method, payload shape, DELETE query param, graph/usage parsing, and the append-vs-skip idempotency + JSON-patch structure.
  • test_glossary_relations.py (7) — mocks the ometa REST boundary and asserts URL/payload/idempotency for the Python facades.

Verification

  • openmetadata-sdk compiles (main + test); GlossaryTermRelationsMockTest 6/6 green; spotless:check clean on both Java modules.
  • ✅ Python ruff check + ruff format --check clean.
  • ⚠️ Python unit test not executed locally (worktree lacks venv + make generate) — runs in CI.
  • ⚠️ integration-tests module not compiled locally (needs an openmetadata-service build) — signature-matched to the verified SDK methods + import-clean; compiles in CI.

🤖 Generated with Claude Code

Greptile Summary

This PR adds first-class glossary relation support to the Python and Java SDKs. The main changes are:

  • New SDK methods for adding, removing, graphing, and counting glossary term relations.
  • New SDK settings helpers for glossary relation type registration.
  • Java fluent relation builder support for glossary terms.
  • Unit and integration test updates for the new SDK wrappers.

Confidence Score: 4/5

This is close, but the registration race should be fixed before merging.

  • Concurrent same-name registration can still persist duplicate glossary relation type entries.
  • The latest code documents the race, but the SDK methods still expose the operation as idempotent.
  • The relation operation wrappers otherwise follow the expected REST shapes in the changed code.

openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java; ingestion/src/metadata/sdk/entities/settings.py

Important Files Changed

Filename Overview
openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java Adds Java SDK settings support, but same-name concurrent registration can still create duplicate relation type entries.
ingestion/src/metadata/sdk/entities/settings.py Adds Python SDK settings support, but same-name concurrent registration can still create duplicate relation type entries.
openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/glossary/GlossaryTermService.java Adds Java SDK wrappers for glossary relation operations.
ingestion/src/metadata/sdk/entities/glossaryterms.py Adds Python SDK wrappers for glossary relation operations and term identifier resolution.

Reviews (2): Last reviewed commit: "fix(sdk): URL-encode glossary relation q..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used (3)

  • Context used - CLAUDE.md (source)
  • Context used - openmetadata-ui-core-components/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

…ython & Java 2.0 SDKs

Wrap the glossary /relations endpoints (add/remove/graph/usage) and the glossaryTermRelationSettings system setting in both 2.0 SDKs so callers no longer drop to the raw HTTP escape hatch.

Python (metadata.sdk): GlossaryTerms.add_relation/remove_relation/relations_graph/relation_type_usage plus a new Settings facade with an idempotent define_glossary_relation_type (JSON-Patch append, clobber-safe). Java (openmetadata-sdk): GlossaryTermService relation methods, a new SystemSettingsService exposed as client.settings(), and fluent GlossaryTerms.find(id).relateTo(to).as(type).apply(). GlossaryTermRelationsIT is ported off raw java.net.http onto the SDK.

Relation-type registration uses JSON-Patch append (/relationTypes/-) rather than GET->append->PUT so concurrent callers never clobber each other's types on the shared glossaryTermRelationSettings global.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 6, 2026 19:22
@harshach harshach requested a review from a team as a code owner July 6, 2026 19:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 6, 2026
Comment thread ingestion/src/metadata/sdk/entities/settings.py
Comment thread ingestion/src/metadata/sdk/entities/settings.py
Comment thread ingestion/src/metadata/sdk/entities/glossaryterms.py Outdated
"""Unit tests for the glossary term relations + settings SDK facades."""

import json
from unittest.mock import MagicMock

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: Python test uses unittest.mock against pytest guidance

test_glossary_relations.py uses from unittest.mock import MagicMock. The Python/ingestion testing guidance is to use pytest with plain assert (which the file does correctly) but to avoid unittest mock styles where possible. Consider using pytest-mock's mocker fixture (or monkeypatch) instead of constructing MagicMock objects directly, to stay consistent with the project's preferred style.

Sources: ingestion/tests/unit/sdk/test_glossary_relations.py:4,16-30

Was this helpful? React with 👍 / 👎

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (24 flaky)

✅ 4086 passed · ❌ 0 failed · 🟡 24 flaky · ⏭️ 22 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 2 811 0 12 4
🟡 Shard 3 816 0 1 12
🟡 Shard 4 818 0 1 5
🟡 Shard 5 828 0 2 0
🟡 Shard 6 813 0 8 1
🟡 24 flaky test(s) (passed on retry)
  • Features/AdvancedSearch.spec.ts › Verify Rule functionality for field Field with AND operator (shard 2, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary Term (Nested) (shard 2, 2 retries)
  • Features/BulkImport.spec.ts › Database service (shard 2, 2 retries)
  • Features/BulkImport.spec.ts › Database (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database Schema (shard 2, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 2, 2 retries)
  • Features/ContextCenterArticles.spec.ts › Article list cards, recently viewed widget, and pagination work (shard 2, 1 retry)
  • Features/ContextCenterMemories.spec.ts › Shared memory shows the shared-with-specific-people description (shard 2, 1 retry)
  • Features/ContextCenterPermission.spec.ts › user with view-only permission sees no Add Memory button, no row actions, and no modal action buttons (shard 2, 1 retry)
  • Features/ContextCenterPermission.spec.ts › user with createAll permission sees Add Memory button but no row edit/delete actions or modal action buttons, and can create a memory (shard 2, 1 retry)
  • Features/DataQuality/TableLevelTests.spec.ts › Table Row Count To Equal (shard 2, 1 retry)
  • Features/GlobalPageSize.spec.ts › Page size should persist across different pages (shard 2, 2 retries)
  • Features/SearchExport.spec.ts › Export queues a background job and downloads from the jobs tray (shard 3, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Database Schema (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Announcement create, edit & delete (shard 5, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 5, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Import partial success - some terms pass, some fail (shard 6, 1 retry)
  • Pages/GlossaryTermRightPanel.spec.ts › Should edit description from glossary term assets context (shard 6, 1 retry)
  • Pages/GlossaryTermRightPanel.spec.ts › Should assign tier from glossary term assets context (shard 6, 1 retry)
  • Pages/GlossaryTermRightPanel.spec.ts › Should edit domain from glossary term assets context (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify Impact Analysis service filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for pipelineService in platform lineage (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for apiService in platform lineage (shard 6, 1 retry)
  • Pages/TagPageRightPanel.spec.ts › Should display correct tabs for table entity in tag assets page context (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

…ency docs

Addresses code review on #29788.

Python remove_relation/relations_graph now build query strings via urllib.parse.urlencode instead of raw f-strings, so relation-type names with reserved characters (space, &, #) cannot malform the URL. Java already encodes via RequestOptions + OkHttp addQueryParameter. Adds an encoding regression test.

Clarifies define_glossary_relation_type / defineGlossaryRelationType docs: the name check makes registration idempotent for sequential use but is not atomic — concurrent registration of the same new name can duplicate, and the server does not dedupe relationTypes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@harshach

harshach commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — addressed in b48d003.

2. Query params not URL-encoded (Python) — fixed. remove_relation and relations_graph now build query strings via urllib.parse.urlencode (with safe="," to keep the relationTypes delimiter literal) instead of raw f-strings, so a value containing a space / & / # can't malform the URL. Added a regression test (test_remove_relation_encodes_special_chars) asserting ?relationType=a%26b. The Java side was already safe — RequestOptions.queryParam(...) flows through OkHttp HttpUrl.Builder.addQueryParameter, which percent-encodes.

1. Idempotency TOCTOU — documented. Confirmed the server does not dedupe: normalizeGlossaryTermRelationSettings only normalizes cardinality and validateGlossaryTermRelationSettingsUpdate only guards removals, so concurrent registration of the same new name can indeed produce duplicates. This can't be made atomic client-side without giving up the clobber-safety that motivated append-over-PUT, so I've made the docstrings (Python + Java) precise: the name check is idempotent for sequential use but not atomic; registration is a one-time setup step and racing callers on the same new name should reconcile duplicates. Server-side dedup is a reasonable follow-up but out of scope for this SDK PR.

3. unittest.mock vs pytest-mock — keeping as-is. This one runs counter to the project's documented standard — CLAUDE.md:527 explicitly says "Use unittest.mock for mocking (MagicMock, patch) - this is compatible with pytest." The test already follows the rest of that guidance (pytest style, plain assert, fixtures, no TestCase). Switching to pytest-mock's mocker would deviate from the stated convention, so I've left it unchanged.

@gitar-bot

gitar-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 3 findings

Implements first-class SDK support for glossary term relations and system settings with safe, idempotent registration. Refactor the Python unit tests to use pytest fixtures instead of unittest.mock for improved test alignment.

💡 Quality: Python test uses unittest.mock against pytest guidance

📄 ingestion/tests/unit/sdk/test_glossary_relations.py:4 📄 ingestion/tests/unit/sdk/test_glossary_relations.py:16-30

test_glossary_relations.py uses from unittest.mock import MagicMock. The Python/ingestion testing guidance is to use pytest with plain assert (which the file does correctly) but to avoid unittest mock styles where possible. Consider using pytest-mock's mocker fixture (or monkeypatch) instead of constructing MagicMock objects directly, to stay consistent with the project's preferred style.

Sources: ingestion/tests/unit/sdk/test_glossary_relations.py:4,16-30

✅ 2 resolved
Edge Case: Idempotency check on relation-type register still races

📄 ingestion/src/metadata/sdk/entities/settings.py:63-77 📄 openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java:62-76
define_glossary_relation_type (Python) and defineGlossaryRelationType (Java) implement idempotency by GET-ing current settings, checking whether a type with the same name exists, and only then PATCH-appending. This is a check-then-act (TOCTOU) sequence against the shared global glossaryTermRelationSettings. The JSON-Patch add /relationTypes/- append does avoid clobbering other callers' types (as the docstrings claim), but two callers registering the same new name concurrently will both pass the exists-check and both append, producing duplicate entries in relationTypes. The docstrings state callers 'never overwrite each other's relation types', which is true, but they don't guard against this duplicate-append case. Consider deduplicating server-side, or documenting that duplicate registration under concurrency is possible so downstream consumers of relationTypes tolerate duplicates.

Sources: ingestion/src/metadata/sdk/entities/settings.py:63-85, openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java:62-93

Edge Case: Query params not URL-encoded in Python relation facades

📄 ingestion/src/metadata/sdk/entities/glossaryterms.py:74-76 📄 ingestion/src/metadata/sdk/entities/glossaryterms.py:90-92
remove_relation and relations_graph build query strings via raw f-string concatenation: ?relationType={relation_type} and ?depth={depth}&relationTypes={','.join(relation_types)}. relation_type / relation_types are free-form strings coming from callers (custom relation-type names). If a value contains a space, &, #, or other reserved characters, the resulting URL will be malformed or silently truncate parameters. The Java side avoids this by using RequestOptions.queryParam(...), which typically handles encoding. Consider passing these through the REST client's params mechanism or urllib.parse.quote rather than embedding raw values.

Sources: ingestion/src/metadata/sdk/entities/glossaryterms.py:74-76,90-92

🤖 Prompt for agents
Code Review: Implements first-class SDK support for glossary term relations and system settings with safe, idempotent registration. Refactor the Python unit tests to use `pytest` fixtures instead of `unittest.mock` for improved test alignment.

1. 💡 Quality: Python test uses unittest.mock against pytest guidance
   Files: ingestion/tests/unit/sdk/test_glossary_relations.py:4, ingestion/tests/unit/sdk/test_glossary_relations.py:16-30

   `test_glossary_relations.py` uses `from unittest.mock import MagicMock`. The Python/ingestion testing guidance is to use pytest with plain `assert` (which the file does correctly) but to avoid unittest mock styles where possible. Consider using `pytest-mock`'s `mocker` fixture (or `monkeypatch`) instead of constructing `MagicMock` objects directly, to stay consistent with the project's preferred style.
   
   Sources: ingestion/tests/unit/sdk/test_glossary_relations.py:4,16-30

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Comment on lines +67 to +75
public Settings defineGlossaryRelationType(GlossaryTermRelationType relationType)
throws OpenMetadataException {
Settings current = getGlossaryRelationSettings();
Settings result = current;
if (!relationTypeExists(current, relationType.getName())) {
result = appendRelationType(relationType);
}
return result;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Registration remains racy

defineGlossaryRelationType still reads the current settings, checks the name locally, and then sends an unconditional add /relationTypes/- patch. When two Java SDK callers register the same absent relation type at the same time, both can pass the local check and both can append the same value. The settings PATCH path does not deduplicate the array, so the global relation type list can still persist duplicate names even though this API is presented as idempotent.

Comment on lines +84 to +91
existing = {entry.get("name") for entry in cls.glossary_relation_types()}
result: Optional[dict[str, Any]] = None # noqa: UP045
if name not in existing:
patch = [{"op": "add", "path": "/relationTypes/-", "value": relation_type}]
result = cls._get_rest_client().patch(
f"{_SETTINGS_ENDPOINT}/{GLOSSARY_TERM_RELATION_SETTINGS}",
data=json.dumps(patch),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Registration remains racy

define_glossary_relation_type still builds existing from a separate settings read before sending an unconditional append patch. If two Python SDK clients register the same absent name concurrently, both can observe it as missing and both can append to /relationTypes/-. Because the settings PATCH path does not deduplicate appended relation types, the shared settings list can still end up with duplicate names.

@gitar-bot

gitar-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 3 findings

Implements first-class SDK support for glossary term relations and system settings with safe, idempotent registration. Refactor the Python unit tests to use pytest fixtures instead of unittest.mock for improved test alignment.

💡 Quality: Python test uses unittest.mock against pytest guidance

📄 ingestion/tests/unit/sdk/test_glossary_relations.py:4 📄 ingestion/tests/unit/sdk/test_glossary_relations.py:16-30

test_glossary_relations.py uses from unittest.mock import MagicMock. The Python/ingestion testing guidance is to use pytest with plain assert (which the file does correctly) but to avoid unittest mock styles where possible. Consider using pytest-mock's mocker fixture (or monkeypatch) instead of constructing MagicMock objects directly, to stay consistent with the project's preferred style.

Sources: ingestion/tests/unit/sdk/test_glossary_relations.py:4,16-30

✅ 2 resolved
Edge Case: Idempotency check on relation-type register still races

📄 ingestion/src/metadata/sdk/entities/settings.py:63-77 📄 openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java:62-76
define_glossary_relation_type (Python) and defineGlossaryRelationType (Java) implement idempotency by GET-ing current settings, checking whether a type with the same name exists, and only then PATCH-appending. This is a check-then-act (TOCTOU) sequence against the shared global glossaryTermRelationSettings. The JSON-Patch add /relationTypes/- append does avoid clobbering other callers' types (as the docstrings claim), but two callers registering the same new name concurrently will both pass the exists-check and both append, producing duplicate entries in relationTypes. The docstrings state callers 'never overwrite each other's relation types', which is true, but they don't guard against this duplicate-append case. Consider deduplicating server-side, or documenting that duplicate registration under concurrency is possible so downstream consumers of relationTypes tolerate duplicates.

Sources: ingestion/src/metadata/sdk/entities/settings.py:63-85, openmetadata-sdk/src/main/java/org/openmetadata/sdk/services/system/SystemSettingsService.java:62-93

Edge Case: Query params not URL-encoded in Python relation facades

📄 ingestion/src/metadata/sdk/entities/glossaryterms.py:74-76 📄 ingestion/src/metadata/sdk/entities/glossaryterms.py:90-92
remove_relation and relations_graph build query strings via raw f-string concatenation: ?relationType={relation_type} and ?depth={depth}&relationTypes={','.join(relation_types)}. relation_type / relation_types are free-form strings coming from callers (custom relation-type names). If a value contains a space, &, #, or other reserved characters, the resulting URL will be malformed or silently truncate parameters. The Java side avoids this by using RequestOptions.queryParam(...), which typically handles encoding. Consider passing these through the REST client's params mechanism or urllib.parse.quote rather than embedding raw values.

Sources: ingestion/src/metadata/sdk/entities/glossaryterms.py:74-76,90-92

🤖 Prompt for agents
Code Review: Implements first-class SDK support for glossary term relations and system settings with safe, idempotent registration. Refactor the Python unit tests to use `pytest` fixtures instead of `unittest.mock` for improved test alignment.

1. 💡 Quality: Python test uses unittest.mock against pytest guidance
   Files: ingestion/tests/unit/sdk/test_glossary_relations.py:4, ingestion/tests/unit/sdk/test_glossary_relations.py:16-30

   `test_glossary_relations.py` uses `from unittest.mock import MagicMock`. The Python/ingestion testing guidance is to use pytest with plain `assert` (which the file does correctly) but to avoid unittest mock styles where possible. Consider using `pytest-mock`'s `mocker` fixture (or `monkeypatch`) instead of constructing `MagicMock` objects directly, to stay consistent with the project's preferred style.
   
   Sources: ingestion/tests/unit/sdk/test_glossary_relations.py:4,16-30

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add first-class glossary term relations + system settings support to the Python & Java 2.0 SDKs

2 participants