tablassert 7.0.0__py3-none-any.whl

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.
tablassert/fullmap.py ADDED
@@ -0,0 +1,167 @@
1
+ from tablassert.enums import Categories
2
+ from tablassert.utils import samphash
3
+ from tablassert.log import logger
4
+ from tempfile import gettempdir
5
+ from typing import Optional
6
+ from pathlib import Path
7
+ from operator import add
8
+ import polars as pl
9
+
10
+
11
+ def distinct(lf: pl.LazyFrame, l0: str, l1: str) -> pl.LazyFrame:
12
+ # ? Extract Unique Terms From Two Text Normalization Columns As LazyFrame
13
+ t0: pl.LazyFrame = lf.select(pl.col(l0).alias("term")).unique()
14
+ t0 = t0.with_columns(pl.lit(0).alias("nlp level"))
15
+
16
+ t1: pl.LazyFrame = lf.select(pl.col(l1).alias("term")).unique()
17
+ t1 = t1.with_columns(pl.lit(1).alias("nlp level"))
18
+
19
+ terms: pl.LazyFrame = pl.concat([t0, t1]).unique(subset=["term"])
20
+
21
+ bad: str = r"^\d+$|^(none|nan|na|null|unknown)$|^$"
22
+ return terms.filter(~pl.col("term").str.contains(bad))
23
+
24
+
25
+ def to_temp(lf: pl.LazyFrame, tmp: Path = Path(gettempdir())) -> Path:
26
+ # ? Writes LazyFrame To A Tempfile To Be Used In Fullmap
27
+ # ! Collection Point: samphash And write_parquet Require Eager
28
+ df: pl.DataFrame = lf.collect()
29
+ p: Path = tmp / samphash(df)
30
+ p = p.with_suffix(".parquet")
31
+ df.write_parquet(p)
32
+ return p
33
+
34
+
35
+ def query_builder(
36
+ p: Path, prioritize: Optional[list[Categories]], avoid: Optional[list[Categories]], taxon: Optional[str]
37
+ ) -> str:
38
+ # ? Build Query With UNION For Better Index Utilization
39
+ base: str = """
40
+ SELECT
41
+ PA.term,
42
+ CU.CURIE,
43
+ CU.PREFERRED_NAME,
44
+ CA.CATEGORY_NAME,
45
+ CU.TAXON_ID,
46
+ SO.SOURCE_NAME,
47
+ SO.SOURCE_VERSION,
48
+ PA."nlp level" AS NLP_LEVEL,
49
+ CASE
50
+ {priority_case}
51
+ ELSE 50
52
+ END AS PR
53
+ FROM SYNONYMS SY
54
+ JOIN SOURCES SO ON SY.SOURCE_ID = SO.SOURCE_ID
55
+ JOIN CURIES CU ON SY.CURIE_ID = CU.CURIE_ID
56
+ JOIN CATEGORIES CA ON CU.CATEGORY_ID = CA.CATEGORY_ID
57
+ {avoid_filter}
58
+ JOIN read_parquet('{parquet}') PA ON PA.term = SY.SYNONYM
59
+ {taxon_filter}
60
+ """
61
+
62
+ priority_case: str = (
63
+ f"WHEN CA.CATEGORY_NAME IN ({', '.join(f"'{x}'" for x in prioritize)}) THEN 1"
64
+ if prioritize
65
+ else "WHEN TRUE THEN 50"
66
+ )
67
+ avoid_filter: str = f"AND CA.CATEGORY_NAME NOT IN ({', '.join(f"'{x}'" for x in avoid)})" if avoid else ""
68
+ taxon_filter: str = f"WHERE CU.TAXON_ID = {taxon} OR CA.CATEGORY_NAME != 'Gene'" if taxon else ""
69
+
70
+ return base.format(priority_case=priority_case, avoid_filter=avoid_filter, taxon_filter=taxon_filter, parquet=p)
71
+
72
+
73
+ def query_distinct(
74
+ p: Path, conn: object, taxon: Optional[str], prioritize: Optional[list[Categories]], avoid: Optional[list[Categories]]
75
+ ) -> pl.DataFrame:
76
+ # ? Query Database For Distinct Terms Only Using Persistent Connection
77
+ # * Added Column Prioritization Logic From 4.2.0
78
+ query: str = query_builder(p, prioritize, avoid, taxon)
79
+ results: pl.DataFrame = conn.execute(query).pl() # pyright: ignore
80
+
81
+ frequency: pl.DataFrame = results.group_by("CATEGORY_NAME").agg(pl.len().alias("FREQUENCY"))
82
+ results = results.join(frequency, on="CATEGORY_NAME", how="left")
83
+
84
+ results = results.sort(["term", "PR", "NLP_LEVEL", "FREQUENCY"], descending=[False, False, False, True])
85
+ results = results.unique(subset=["term"], keep="first")
86
+
87
+ p.unlink(missing_ok=True)
88
+ return results
89
+
90
+
91
+ def version4(
92
+ lf: pl.LazyFrame,
93
+ col: str,
94
+ conn: object,
95
+ taxon: Optional[str],
96
+ prioritize: Optional[list[Categories]],
97
+ avoid: Optional[list[Categories]],
98
+ section_hash: str,
99
+ config_file: str,
100
+ tag: str = " one",
101
+ ) -> pl.LazyFrame:
102
+ # ? Case Dependant, Provenance Rich Name Entity Recognition
103
+ l0: str = col
104
+ l1: str = add(l0, tag)
105
+
106
+ terms: pl.LazyFrame = distinct(lf, l0, l1)
107
+ p: Path = to_temp(terms)
108
+ matches: pl.DataFrame = query_distinct(p, conn, taxon, prioritize, avoid)
109
+
110
+ # * Log Unmatched Entities
111
+ antimatches: pl.LazyFrame = terms.join(matches.lazy().select("term"), left_on="term", right_on="term", how="anti")
112
+
113
+ # ! Collection Point: Requires Eager
114
+ unnmatched: pl.DataFrame = antimatches.select("term").unique().collect()
115
+ if unnmatched.height > 0:
116
+ for term in unnmatched.get_column("term").to_list():
117
+ logger.info(f"FAILED FULLMAP | STORE: {section_hash} | CONFIG: {config_file} | COL: {col} | VALUE: {term!r}")
118
+
119
+ # ! Collection Point: Join After DuckDB Query, Then Re-Lazy
120
+ df: pl.DataFrame = lf.collect()
121
+ result: pl.DataFrame = df.join(
122
+ matches.filter(pl.col("NLP_LEVEL").eq(0)), left_on=l0, right_on="term", how="left", suffix=" l0"
123
+ )
124
+
125
+ l1_matches: pl.DataFrame = matches.filter(pl.col("NLP_LEVEL").eq(1))
126
+ result = result.join(l1_matches, left_on=l1, right_on="term", how="left", suffix=" l1")
127
+
128
+ result = result.with_columns(
129
+ [
130
+ pl.when(pl.col("CURIE").is_not_null()).then(pl.col("CURIE")).otherwise(pl.col("CURIE l1")).alias(col),
131
+ pl.when(pl.col("PREFERRED_NAME").is_not_null())
132
+ .then(pl.col("PREFERRED_NAME"))
133
+ .otherwise(pl.col("PREFERRED_NAME l1"))
134
+ .alias(add(col, " name")),
135
+ pl.when(pl.col("CATEGORY_NAME").is_not_null())
136
+ .then(add(pl.lit("biolink:"), pl.col("CATEGORY_NAME")))
137
+ .otherwise(add(pl.lit("biolink:"), pl.col("CATEGORY_NAME l1")))
138
+ .alias(add(col, " category")),
139
+ pl.when(pl.col("TAXON_ID").is_not_null())
140
+ .then(add(pl.lit("NCBITaxon:"), pl.col("TAXON_ID").cast(pl.String)))
141
+ .otherwise(add(pl.lit("NCBITaxon:"), pl.col("TAXON_ID l1").cast(pl.String)))
142
+ .alias(add(col, " taxon")),
143
+ pl.when(pl.col("SOURCE_NAME").is_not_null())
144
+ .then(pl.col("SOURCE_NAME"))
145
+ .otherwise(pl.col("SOURCE_NAME l1"))
146
+ .alias(add(col, " source")),
147
+ pl.when(pl.col("SOURCE_VERSION").is_not_null())
148
+ .then(pl.col("SOURCE_VERSION"))
149
+ .otherwise(pl.col("SOURCE_VERSION l1"))
150
+ .alias(add(col, " source version")),
151
+ pl.when(pl.col("NLP_LEVEL").is_not_null())
152
+ .then(pl.col("NLP_LEVEL"))
153
+ .otherwise(pl.col("NLP_LEVEL l1"))
154
+ .alias(add(col, " nlp level")),
155
+ ]
156
+ )
157
+
158
+ result = result.select(
159
+ pl.exclude(
160
+ r"^(CURIE|PREFERRED_NAME|CATEGORY_NAME|TAXON_ID|SOURCE_NAME|SOURCE_VERSION|NLP_LEVEL|PR|FREQUENCY)( l1)?$"
161
+ )
162
+ )
163
+ result = result.select(pl.exclude(add(col, " one")))
164
+ result = result.with_columns(pl.col(add(col, " taxon")).replace("NCBITaxon:0", None))
165
+ result = result.filter(pl.col(col).is_not_null())
166
+
167
+ return result.lazy()
tablassert/ingests.py ADDED
@@ -0,0 +1,43 @@
1
+ from copy import deepcopy
2
+ from yaml import CLoader
3
+ from pathlib import Path
4
+ from typing import Union
5
+ from typing import Any
6
+ import yaml
7
+
8
+
9
+ def fastmerge(a: Union[list[Any], dict[str, Any]], b: Union[list[Any], dict[str, Any]]) -> Any:
10
+ # ? Streamlined (Fast) Implementation Of Deepmerge Config
11
+ if isinstance(a, dict) and isinstance(b, dict):
12
+ for k, v in b.items():
13
+ if k in a:
14
+ av: Any = a[k]
15
+ if isinstance(av, dict) and isinstance(v, dict):
16
+ fastmerge(av, v)
17
+ elif isinstance(av, list) and isinstance(v, list):
18
+ av.extend(v)
19
+ else:
20
+ a[k] = v
21
+ else:
22
+ a[k] = v
23
+ return a
24
+
25
+ elif isinstance(a, list) and isinstance(b, list):
26
+ a.extend(b)
27
+ return a
28
+ else:
29
+ return b
30
+
31
+
32
+ def from_yaml(p: Path) -> object:
33
+ # ? Reads YAML Config To Dict
34
+ with p.open("r") as f:
35
+ return yaml.load(f, Loader=CLoader)
36
+
37
+
38
+ def to_sections(instructions: dict[str, Any], table: Path) -> list[list[dict[str, Any]]]:
39
+ # ? Converts Dict To Sections
40
+ template: dict[str, Any] = instructions.get("template", {})
41
+ template["config"] = table
42
+ sections: list[dict[str, Any]] = instructions.get("sections", [{}])
43
+ return [fastmerge(deepcopy(template), x) for x in sections]