metaspn-entities 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/PKG-INFO +14 -1
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/README.md +13 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/__init__.py +2 -0
- metaspn_entities-0.1.8/metaspn_entities/demo.py +81 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/normalize.py +11 -1
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/PKG-INFO +14 -1
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/SOURCES.txt +2 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/pyproject.toml +1 -1
- metaspn_entities-0.1.8/tests/test_demo_support.py +84 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/LICENSE +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/adapter.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/attribution.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/context.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/events.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/models.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/resolver.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities/sqlite_backend.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/dependency_links.txt +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/requires.txt +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/top_level.txt +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/setup.cfg +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_adapter.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_attribution.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_context.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_event_contract.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_recommendation_context.py +0 -0
- {metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/tests/test_resolver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metaspn-entities
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Canonical entity resolution, aliasing, and merges for MetaSPN systems
|
|
5
5
|
Author: MetaSPN Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,3 +135,16 @@ Attribution guarantees:
|
|
|
135
135
|
- canonical merge redirects are resolved before returning `entity_id`
|
|
136
136
|
- output includes explicit confidence for downstream learning logic
|
|
137
137
|
- deterministic tie-breaks are applied by score, then hit count, then entity ID
|
|
138
|
+
|
|
139
|
+
## Demo Pipeline Invocation
|
|
140
|
+
|
|
141
|
+
For demo digest identity resolution (without direct DB queries in renderer), use:
|
|
142
|
+
|
|
143
|
+
- `resolve_demo_social_identity(resolver, social_payload)`
|
|
144
|
+
|
|
145
|
+
Returned payload includes:
|
|
146
|
+
- `entity_id`
|
|
147
|
+
- `confidence`
|
|
148
|
+
- `matched_identifiers`
|
|
149
|
+
- `why` metadata (confidence summary, counts, relationship hint)
|
|
150
|
+
- emitted event payloads for auditability
|
|
@@ -110,3 +110,16 @@ Attribution guarantees:
|
|
|
110
110
|
- canonical merge redirects are resolved before returning `entity_id`
|
|
111
111
|
- output includes explicit confidence for downstream learning logic
|
|
112
112
|
- deterministic tie-breaks are applied by score, then hit count, then entity ID
|
|
113
|
+
|
|
114
|
+
## Demo Pipeline Invocation
|
|
115
|
+
|
|
116
|
+
For demo digest identity resolution (without direct DB queries in renderer), use:
|
|
117
|
+
|
|
118
|
+
- `resolve_demo_social_identity(resolver, social_payload)`
|
|
119
|
+
|
|
120
|
+
Returned payload includes:
|
|
121
|
+
- `entity_id`
|
|
122
|
+
- `confidence`
|
|
123
|
+
- `matched_identifiers`
|
|
124
|
+
- `why` metadata (confidence summary, counts, relationship hint)
|
|
125
|
+
- emitted event payloads for auditability
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .adapter import SignalResolutionResult, resolve_normalized_social_signal
|
|
2
2
|
from .attribution import OutcomeAttribution
|
|
3
3
|
from .context import RecommendationContext, EntityContext, build_confidence_summary, build_recommendation_context
|
|
4
|
+
from .demo import resolve_demo_social_identity
|
|
4
5
|
from .events import EmittedEvent
|
|
5
6
|
from .models import EntityResolution
|
|
6
7
|
from .resolver import EntityResolver
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
|
10
11
|
"resolve_normalized_social_signal",
|
|
11
12
|
"SignalResolutionResult",
|
|
12
13
|
"OutcomeAttribution",
|
|
14
|
+
"resolve_demo_social_identity",
|
|
13
15
|
"EntityContext",
|
|
14
16
|
"RecommendationContext",
|
|
15
17
|
"build_confidence_summary",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping
|
|
4
|
+
|
|
5
|
+
from .models import EntityType
|
|
6
|
+
from .resolver import EntityResolver
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_demo_social_identity(
|
|
10
|
+
resolver: EntityResolver,
|
|
11
|
+
social_payload: Mapping[str, Any],
|
|
12
|
+
*,
|
|
13
|
+
caused_by: str = "demo-pipeline",
|
|
14
|
+
) -> Dict[str, Any]:
|
|
15
|
+
platform = str(social_payload.get("platform") or "").strip().lower()
|
|
16
|
+
source = str(social_payload.get("source") or social_payload.get("provenance") or "demo")
|
|
17
|
+
|
|
18
|
+
handle = social_payload.get("author_handle") or social_payload.get("handle")
|
|
19
|
+
if not isinstance(handle, str) or not handle.strip():
|
|
20
|
+
raise ValueError("demo payload requires author_handle or handle")
|
|
21
|
+
handle = handle.strip()
|
|
22
|
+
|
|
23
|
+
handle_type = f"{platform}_handle" if platform else "handle"
|
|
24
|
+
resolution = resolver.resolve(
|
|
25
|
+
handle_type,
|
|
26
|
+
handle,
|
|
27
|
+
context={
|
|
28
|
+
"entity_type": EntityType.PERSON,
|
|
29
|
+
"caused_by": caused_by,
|
|
30
|
+
"provenance": source,
|
|
31
|
+
"confidence": 0.93,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for key in ("profile_url", "author_url", "canonical_url"):
|
|
36
|
+
url = social_payload.get(key)
|
|
37
|
+
if isinstance(url, str) and url.strip():
|
|
38
|
+
resolver.add_alias(
|
|
39
|
+
resolution.entity_id,
|
|
40
|
+
"canonical_url",
|
|
41
|
+
url.strip(),
|
|
42
|
+
confidence=0.96,
|
|
43
|
+
caused_by=caused_by,
|
|
44
|
+
provenance=source,
|
|
45
|
+
)
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
email = social_payload.get("email")
|
|
49
|
+
if isinstance(email, str) and email.strip():
|
|
50
|
+
resolver.add_alias(
|
|
51
|
+
resolution.entity_id,
|
|
52
|
+
"email",
|
|
53
|
+
email.strip(),
|
|
54
|
+
confidence=0.98,
|
|
55
|
+
caused_by=caused_by,
|
|
56
|
+
provenance=source,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
canonical_id = resolver.store.canonical_entity_id(resolution.entity_id)
|
|
60
|
+
context = resolver.entity_context(canonical_id)
|
|
61
|
+
digest_payload = {
|
|
62
|
+
"entity_id": canonical_id,
|
|
63
|
+
"confidence": context.confidence_summary["overall_confidence"],
|
|
64
|
+
"matched_identifiers": [
|
|
65
|
+
{
|
|
66
|
+
"identifier_type": item["identifier_type"],
|
|
67
|
+
"value": item["value"],
|
|
68
|
+
"confidence": item["confidence"],
|
|
69
|
+
"last_seen_at": item["last_seen_at"],
|
|
70
|
+
}
|
|
71
|
+
for item in context.identifiers
|
|
72
|
+
],
|
|
73
|
+
"why": {
|
|
74
|
+
"matched_identifier_count": len(context.identifiers),
|
|
75
|
+
"alias_count": len(context.aliases),
|
|
76
|
+
"confidence_summary": context.confidence_summary,
|
|
77
|
+
"relationship_stage_hint": resolver.recommendation_context(canonical_id).relationship_stage_hint,
|
|
78
|
+
},
|
|
79
|
+
"events": [event.payload for event in resolver.drain_events()],
|
|
80
|
+
}
|
|
81
|
+
return digest_payload
|
|
@@ -7,7 +7,17 @@ def normalize_identifier(identifier_type: str, value: str) -> str:
|
|
|
7
7
|
identifier_type = identifier_type.strip().lower()
|
|
8
8
|
value = value.strip()
|
|
9
9
|
|
|
10
|
-
if identifier_type in {
|
|
10
|
+
if identifier_type in {
|
|
11
|
+
"twitter_handle",
|
|
12
|
+
"x_handle",
|
|
13
|
+
"linkedin_handle",
|
|
14
|
+
"github_handle",
|
|
15
|
+
"instagram_handle",
|
|
16
|
+
"tiktok_handle",
|
|
17
|
+
"bluesky_handle",
|
|
18
|
+
"youtube_handle",
|
|
19
|
+
"handle",
|
|
20
|
+
}:
|
|
11
21
|
return value.lstrip("@").lower()
|
|
12
22
|
|
|
13
23
|
if identifier_type == "email":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metaspn-entities
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Canonical entity resolution, aliasing, and merges for MetaSPN systems
|
|
5
5
|
Author: MetaSPN Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,3 +135,16 @@ Attribution guarantees:
|
|
|
135
135
|
- canonical merge redirects are resolved before returning `entity_id`
|
|
136
136
|
- output includes explicit confidence for downstream learning logic
|
|
137
137
|
- deterministic tie-breaks are applied by score, then hit count, then entity ID
|
|
138
|
+
|
|
139
|
+
## Demo Pipeline Invocation
|
|
140
|
+
|
|
141
|
+
For demo digest identity resolution (without direct DB queries in renderer), use:
|
|
142
|
+
|
|
143
|
+
- `resolve_demo_social_identity(resolver, social_payload)`
|
|
144
|
+
|
|
145
|
+
Returned payload includes:
|
|
146
|
+
- `entity_id`
|
|
147
|
+
- `confidence`
|
|
148
|
+
- `matched_identifiers`
|
|
149
|
+
- `why` metadata (confidence summary, counts, relationship hint)
|
|
150
|
+
- emitted event payloads for auditability
|
|
@@ -5,6 +5,7 @@ metaspn_entities/__init__.py
|
|
|
5
5
|
metaspn_entities/adapter.py
|
|
6
6
|
metaspn_entities/attribution.py
|
|
7
7
|
metaspn_entities/context.py
|
|
8
|
+
metaspn_entities/demo.py
|
|
8
9
|
metaspn_entities/events.py
|
|
9
10
|
metaspn_entities/models.py
|
|
10
11
|
metaspn_entities/normalize.py
|
|
@@ -18,6 +19,7 @@ metaspn_entities.egg-info/top_level.txt
|
|
|
18
19
|
tests/test_adapter.py
|
|
19
20
|
tests/test_attribution.py
|
|
20
21
|
tests/test_context.py
|
|
22
|
+
tests/test_demo_support.py
|
|
21
23
|
tests/test_event_contract.py
|
|
22
24
|
tests/test_recommendation_context.py
|
|
23
25
|
tests/test_resolver.py
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from metaspn_entities.demo import resolve_demo_social_identity
|
|
6
|
+
from metaspn_entities.resolver import EntityResolver
|
|
7
|
+
from metaspn_entities.sqlite_backend import SQLiteEntityStore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DemoSupportTests(unittest.TestCase):
|
|
11
|
+
def setUp(self) -> None:
|
|
12
|
+
self.tempdir = tempfile.TemporaryDirectory()
|
|
13
|
+
self.db_path = str(Path(self.tempdir.name) / "entities.db")
|
|
14
|
+
self.store = SQLiteEntityStore(self.db_path)
|
|
15
|
+
self.resolver = EntityResolver(self.store)
|
|
16
|
+
|
|
17
|
+
def tearDown(self) -> None:
|
|
18
|
+
self.store.close()
|
|
19
|
+
self.tempdir.cleanup()
|
|
20
|
+
|
|
21
|
+
def test_same_author_repeated_days_stable_entity(self) -> None:
|
|
22
|
+
day_1 = {
|
|
23
|
+
"platform": "x",
|
|
24
|
+
"author_handle": "@DemoAuthor",
|
|
25
|
+
"profile_url": "https://x.com/demoauthor",
|
|
26
|
+
"source": "demo.day1",
|
|
27
|
+
}
|
|
28
|
+
day_2 = {
|
|
29
|
+
"platform": "x",
|
|
30
|
+
"author_handle": "demoauthor",
|
|
31
|
+
"profile_url": "https://x.com/demoauthor/",
|
|
32
|
+
"source": "demo.day2",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
first = resolve_demo_social_identity(self.resolver, day_1)
|
|
36
|
+
second = resolve_demo_social_identity(self.resolver, day_2)
|
|
37
|
+
|
|
38
|
+
self.assertEqual(first["entity_id"], second["entity_id"])
|
|
39
|
+
|
|
40
|
+
def test_alias_collision_merge_safe_continuity(self) -> None:
|
|
41
|
+
one = resolve_demo_social_identity(
|
|
42
|
+
self.resolver,
|
|
43
|
+
{
|
|
44
|
+
"platform": "twitter",
|
|
45
|
+
"author_handle": "alpha_demo",
|
|
46
|
+
"profile_url": "https://example.com/u/shared",
|
|
47
|
+
"source": "demo.a",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
two = resolve_demo_social_identity(
|
|
51
|
+
self.resolver,
|
|
52
|
+
{
|
|
53
|
+
"platform": "linkedin",
|
|
54
|
+
"author_handle": "beta_demo",
|
|
55
|
+
"profile_url": "http://www.example.com/u/shared/",
|
|
56
|
+
"source": "demo.b",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# canonical_url collision should keep one canonical identity for both reruns.
|
|
61
|
+
self.assertEqual(one["entity_id"], two["entity_id"])
|
|
62
|
+
|
|
63
|
+
def test_digest_payload_contains_explainability_context(self) -> None:
|
|
64
|
+
payload = resolve_demo_social_identity(
|
|
65
|
+
self.resolver,
|
|
66
|
+
{
|
|
67
|
+
"platform": "bluesky",
|
|
68
|
+
"author_handle": "DigestUser",
|
|
69
|
+
"profile_url": "https://bsky.app/profile/digestuser",
|
|
70
|
+
"source": "demo.digest",
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.assertIn("entity_id", payload)
|
|
75
|
+
self.assertIn("confidence", payload)
|
|
76
|
+
self.assertIn("matched_identifiers", payload)
|
|
77
|
+
self.assertIn("why", payload)
|
|
78
|
+
self.assertIn("confidence_summary", payload["why"])
|
|
79
|
+
self.assertGreaterEqual(payload["why"]["matched_identifier_count"], 1)
|
|
80
|
+
self.assertIsInstance(payload["events"], list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metaspn_entities-0.1.7 → metaspn_entities-0.1.8}/metaspn_entities.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|