zefix-parser 0.1.0__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.
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.4
2
+ Name: zefix-parser
3
+ Version: 0.1.0
4
+ Summary: Typed Python client for Zefix, the Swiss Central Business Name Index (Zentraler Firmenindex)
5
+ Keywords: zefix,swiss,commercial-register,handelsregister,lindas,sparql,uid
6
+ Author: Prospex
7
+ Author-email: Prospex <hello@prospex.ch>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Topic :: Office/Business :: Financial
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: pytest>=8 ; extra == 'dev'
16
+ Requires-Dist: httpx>=0.27 ; extra == 'dev'
17
+ Requires-Dist: httpx>=0.27 ; extra == 'http'
18
+ Requires-Python: >=3.14
19
+ Project-URL: Homepage, https://prospex.ch
20
+ Project-URL: Repository, https://github.com/prospex-ch/python-zefix-parser
21
+ Project-URL: Documentation, https://zefix-parser.readthedocs.io
22
+ Provides-Extra: dev
23
+ Provides-Extra: http
24
+ Description-Content-Type: text/markdown
25
+
26
+ # zefix-parser
27
+
28
+ [![Documentation](https://readthedocs.org/projects/zefix-parser/badge/?version=latest)](https://zefix-parser.readthedocs.io/en/latest/)
29
+
30
+ Typed Python client for Zefix, the Swiss Central Business Name Index (Zentraler Firmenindex).
31
+
32
+ Covers both ways into the register: the open [LINDAS](https://lindas.admin.ch) SPARQL
33
+ dataset, which carries every company in Switzerland and needs no credentials, and the
34
+ [Zefix PublicREST API](https://www.zefix.admin.ch), which adds share capital, legal status,
35
+ auditors and corporate relations. Responses parse into plain dataclasses.
36
+
37
+ Built and maintained by [Prospex](https://prospex.ch), a Swiss B2B sales intelligence platform.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install zefix-parser
43
+ ```
44
+
45
+ To use the HTTP clients (for fetching from the live endpoints):
46
+
47
+ ```bash
48
+ pip install zefix-parser[http]
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ### Read the register from LINDAS
54
+
55
+ No credentials needed.
56
+
57
+ ```python
58
+ from zefix_parser.client import LindasClient
59
+
60
+ with LindasClient() as client:
61
+ print(client.count()) # 812,000 or so
62
+
63
+ for entity in client.iter_entities():
64
+ print(entity.legal_name, entity.uid, entity.canton, entity.legal_form_code)
65
+ ```
66
+
67
+ Or look up companies you already have UIDs for, in one request:
68
+
69
+ ```python
70
+ from zefix_parser.client import LindasClient
71
+
72
+ with LindasClient() as client:
73
+ entities = client.fetch_by_uids(["CHE-105.215.703", "CHE-411.462.297"])
74
+
75
+ for entity in entities:
76
+ print(entity.legal_name) # "Alpenblick Handel AG"
77
+ print(entity.purpose) # "Handel mit Waren aller Art"
78
+ print(entity.municipality_name, entity.canton)
79
+ ```
80
+
81
+ ### Fetch details from the PublicREST API
82
+
83
+ The REST API is gated behind HTTP Basic auth. Credentials are issued on request by
84
+ `zefix@bj.admin.ch`.
85
+
86
+ ```python
87
+ from zefix_parser.client import ZefixRestClient
88
+
89
+ with ZefixRestClient(username="...", password="...") as client:
90
+ company = client.get_by_uid("CHE-105.215.703")
91
+
92
+ print(company.name) # "Alpenblick Handel AG"
93
+ print(company.capital_nominal) # Decimal("250000.00")
94
+ print(company.status) # "ACTIVE"
95
+
96
+ for auditor in company.audit_companies:
97
+ print(auditor.name, auditor.legal_seat)
98
+ ```
99
+
100
+ Both clients rate-limit themselves to one request every half second and retry transient
101
+ failures with exponential backoff.
102
+
103
+ ### Parse responses you already have
104
+
105
+ Every parser is importable without the `http` extra, so you can point them at recorded
106
+ responses or use your own HTTP library:
107
+
108
+ ```python
109
+ from zefix_parser import parse_company, parse_entity_page
110
+
111
+ company = parse_company(rest_json) # a dict from the REST API
112
+ entities = parse_entity_page(sparql_bytes) # a SPARQL JSON results document
113
+ ```
114
+
115
+ ## The two access paths
116
+
117
+ | | LINDAS | PublicREST |
118
+ |---|---|---|
119
+ | Endpoint | `https://ld.admin.ch/query` | `https://www.zefix.admin.ch/ZefixPublicREST/api/v1` |
120
+ | Credentials | none | HTTP Basic |
121
+ | Shape | the whole register, keyset-paginated | one company per request |
122
+ | Carries | name, legal form, address, canton, purpose | the above plus capital, status, auditors, branches, mergers, former names |
123
+ | Class | `RegistryEntity` | `Company` |
124
+
125
+ LINDAS has no modification-date predicate, so a server-side delta fetch is impossible: you
126
+ either walk the whole register or drive updates from another change feed, such as
127
+ [SHAB publications](https://shab-parser.readthedocs.io).
128
+
129
+ ## Data model
130
+
131
+ `parse_entity_page()` returns `RegistryEntity` objects:
132
+
133
+ ```python
134
+ @dataclass(frozen=True)
135
+ class RegistryEntity:
136
+ zefix_uri: str
137
+ uid: str # CHE123456789, unpunctuated
138
+ chid: str
139
+ ehra_id: str
140
+ legal_name: str
141
+ alternate_names: list[str] # the other language variants
142
+ legal_form_code: str # eCH-0097, e.g. "0106"
143
+ legal_form_name: str
144
+ municipality_id: str # federal municipality number
145
+ municipality_name: str
146
+ canton: str
147
+ street_address: str
148
+ postal_code: str
149
+ locality: str
150
+ purpose: str
151
+ purpose_language: str
152
+ fingerprint: str # SHA-256 over the identity fields
153
+ ```
154
+
155
+ `parse_company()` returns `Company` objects:
156
+
157
+ ```python
158
+ @dataclass(frozen=True)
159
+ class Company:
160
+ name: str
161
+ ehraid: int
162
+ uid: str
163
+ chid: str
164
+ canton: str
165
+ status: str # "ACTIVE", "IN_LIQUIDATION", "DELETED"
166
+ capital_nominal: Decimal | None
167
+ capital_currency: str
168
+ deletion_date: date | None
169
+ cantonal_excerpt_web: str
170
+ head_offices: list[RelatedEntity]
171
+ further_head_offices: list[RelatedEntity]
172
+ branch_offices: list[RelatedEntity]
173
+ has_taken_over: list[RelatedEntity]
174
+ was_taken_over_by: list[RelatedEntity]
175
+ audit_companies: list[RelatedEntity]
176
+ old_names: list[OldName]
177
+ ```
178
+
179
+ ## Identifiers
180
+
181
+ The same UID appears in three forms depending on where you read it, so the library
182
+ converts between them:
183
+
184
+ ```python
185
+ from zefix_parser import clean_uid, format_uid, is_valid_uid, normalize_uid
186
+
187
+ normalize_uid("CHE-105.215.703") # "CHE105215703" (LINDAS form)
188
+ format_uid("CHE105215703") # "CHE-105.215.703" (REST form)
189
+ clean_uid("VAT: CHE-109.807.630 MWST") # "CHE109807630"
190
+ is_valid_uid("CHE123456789") # False - the check digit fails
191
+ ```
192
+
193
+ `is_valid_uid()` runs the ISO 7064 mod 11-10 check digit. It is worth calling before you
194
+ spend a request: a mistyped UID can never match the register, and the register's own
195
+ placeholder `CHE123456789` fails it.
196
+
197
+ ## Legal forms
198
+
199
+ Both registers identify a legal form by its four-digit eCH-0097 code, and the name differs
200
+ by language:
201
+
202
+ ```python
203
+ from zefix_parser import LegalForm, legal_form_abbreviation, legal_form_name
204
+
205
+ LegalForm.CORPORATION # "0106"
206
+ legal_form_name("0106", "de") # "Aktiengesellschaft"
207
+ legal_form_name("0106", "fr") # "Société anonyme"
208
+ legal_form_name("0106", "it") # "Società anonima"
209
+ legal_form_abbreviation("0107", "fr") # "Sàrl"
210
+ ```
211
+
212
+ The full table is in the [legal forms reference](https://zefix-parser.readthedocs.io/en/latest/legal-forms.html).
213
+
214
+ ## Former names
215
+
216
+ The register re-typesets a company's name whenever anything else about it changes, so
217
+ `company.old_names` fills up with entries that differ from the current name only in casing
218
+ or spacing: "QualiCasa AG" against "Qualicasa AG". Apply them and you retire the company's
219
+ live name. `meaningful_old_names()` drops them:
220
+
221
+ ```python
222
+ from zefix_parser import meaningful_old_names
223
+
224
+ for old in meaningful_old_names(company):
225
+ print(old.sequence_nr, old.name) # only the genuine renames, oldest first
226
+ ```
227
+
228
+ ## API reference
229
+
230
+ ### `zefix_parser.parse_entity_page(content: bytes) -> list[RegistryEntity]`
231
+
232
+ Parse a SPARQL JSON results document into companies. One company spans several rows, one
233
+ per language-tagged literal; rows are grouped by entity URI and folded down to a single
234
+ deterministic value per field.
235
+
236
+ ### `zefix_parser.parse_company(data: dict | list) -> Company`
237
+
238
+ Parse one `/company/...` response. The API is inconsistent about whether a single company
239
+ comes back as an object or a one-element list; both work.
240
+
241
+ ### `zefix_parser.build_combined_page_query(*, cursor_after=None, limit=500) -> str`
242
+
243
+ Build the SPARQL query for one keyset page of full records. Use this to run the crawl
244
+ yourself, outside `LindasClient`.
245
+
246
+ ### `zefix_parser.client.LindasClient`
247
+
248
+ SPARQL client. `count()`, `iter_entities()`, `iter_pages()` (which yields `RawPage` objects
249
+ carrying the cursor, so a crawl can be resumed), `fetch_by_uids()`, `query()`.
250
+
251
+ ### `zefix_parser.client.ZefixRestClient`
252
+
253
+ PublicREST client. `get_by_uid()`, `get_by_ehraid()`, `get_by_chid()`, `search()`,
254
+ `sogc_by_date()`, `sogc()`.
255
+
256
+ ## Background
257
+
258
+ Zefix (the Zentraler Firmenindex, *index central des raisons de commerce*, *indice centrale
259
+ delle ditte*, officially the Swiss Central Business Name Index) is the federal index over
260
+ the 26 cantonal commercial registers: Handelsregister, registre du commerce, registro di
261
+ commercio. Every company registered in Switzerland appears in it, identified by a UID
262
+ (`CHE-123.456.789`), a CHID and an EHRAID.
263
+
264
+ The Federal Office of Justice publishes the index twice. LINDAS carries every company as
265
+ linked data, open to anyone, holding each company's current state. The PublicREST API holds
266
+ more per company, behind credentials, and answers one lookup at a time. Neither publishes a
267
+ change feed; for that you need the gazette, which is what
268
+ [shab-parser](https://shab-parser.readthedocs.io) reads.
269
+
270
+ ## License
271
+
272
+ MIT
@@ -0,0 +1,247 @@
1
+ # zefix-parser
2
+
3
+ [![Documentation](https://readthedocs.org/projects/zefix-parser/badge/?version=latest)](https://zefix-parser.readthedocs.io/en/latest/)
4
+
5
+ Typed Python client for Zefix, the Swiss Central Business Name Index (Zentraler Firmenindex).
6
+
7
+ Covers both ways into the register: the open [LINDAS](https://lindas.admin.ch) SPARQL
8
+ dataset, which carries every company in Switzerland and needs no credentials, and the
9
+ [Zefix PublicREST API](https://www.zefix.admin.ch), which adds share capital, legal status,
10
+ auditors and corporate relations. Responses parse into plain dataclasses.
11
+
12
+ Built and maintained by [Prospex](https://prospex.ch), a Swiss B2B sales intelligence platform.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install zefix-parser
18
+ ```
19
+
20
+ To use the HTTP clients (for fetching from the live endpoints):
21
+
22
+ ```bash
23
+ pip install zefix-parser[http]
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ### Read the register from LINDAS
29
+
30
+ No credentials needed.
31
+
32
+ ```python
33
+ from zefix_parser.client import LindasClient
34
+
35
+ with LindasClient() as client:
36
+ print(client.count()) # 812,000 or so
37
+
38
+ for entity in client.iter_entities():
39
+ print(entity.legal_name, entity.uid, entity.canton, entity.legal_form_code)
40
+ ```
41
+
42
+ Or look up companies you already have UIDs for, in one request:
43
+
44
+ ```python
45
+ from zefix_parser.client import LindasClient
46
+
47
+ with LindasClient() as client:
48
+ entities = client.fetch_by_uids(["CHE-105.215.703", "CHE-411.462.297"])
49
+
50
+ for entity in entities:
51
+ print(entity.legal_name) # "Alpenblick Handel AG"
52
+ print(entity.purpose) # "Handel mit Waren aller Art"
53
+ print(entity.municipality_name, entity.canton)
54
+ ```
55
+
56
+ ### Fetch details from the PublicREST API
57
+
58
+ The REST API is gated behind HTTP Basic auth. Credentials are issued on request by
59
+ `zefix@bj.admin.ch`.
60
+
61
+ ```python
62
+ from zefix_parser.client import ZefixRestClient
63
+
64
+ with ZefixRestClient(username="...", password="...") as client:
65
+ company = client.get_by_uid("CHE-105.215.703")
66
+
67
+ print(company.name) # "Alpenblick Handel AG"
68
+ print(company.capital_nominal) # Decimal("250000.00")
69
+ print(company.status) # "ACTIVE"
70
+
71
+ for auditor in company.audit_companies:
72
+ print(auditor.name, auditor.legal_seat)
73
+ ```
74
+
75
+ Both clients rate-limit themselves to one request every half second and retry transient
76
+ failures with exponential backoff.
77
+
78
+ ### Parse responses you already have
79
+
80
+ Every parser is importable without the `http` extra, so you can point them at recorded
81
+ responses or use your own HTTP library:
82
+
83
+ ```python
84
+ from zefix_parser import parse_company, parse_entity_page
85
+
86
+ company = parse_company(rest_json) # a dict from the REST API
87
+ entities = parse_entity_page(sparql_bytes) # a SPARQL JSON results document
88
+ ```
89
+
90
+ ## The two access paths
91
+
92
+ | | LINDAS | PublicREST |
93
+ |---|---|---|
94
+ | Endpoint | `https://ld.admin.ch/query` | `https://www.zefix.admin.ch/ZefixPublicREST/api/v1` |
95
+ | Credentials | none | HTTP Basic |
96
+ | Shape | the whole register, keyset-paginated | one company per request |
97
+ | Carries | name, legal form, address, canton, purpose | the above plus capital, status, auditors, branches, mergers, former names |
98
+ | Class | `RegistryEntity` | `Company` |
99
+
100
+ LINDAS has no modification-date predicate, so a server-side delta fetch is impossible: you
101
+ either walk the whole register or drive updates from another change feed, such as
102
+ [SHAB publications](https://shab-parser.readthedocs.io).
103
+
104
+ ## Data model
105
+
106
+ `parse_entity_page()` returns `RegistryEntity` objects:
107
+
108
+ ```python
109
+ @dataclass(frozen=True)
110
+ class RegistryEntity:
111
+ zefix_uri: str
112
+ uid: str # CHE123456789, unpunctuated
113
+ chid: str
114
+ ehra_id: str
115
+ legal_name: str
116
+ alternate_names: list[str] # the other language variants
117
+ legal_form_code: str # eCH-0097, e.g. "0106"
118
+ legal_form_name: str
119
+ municipality_id: str # federal municipality number
120
+ municipality_name: str
121
+ canton: str
122
+ street_address: str
123
+ postal_code: str
124
+ locality: str
125
+ purpose: str
126
+ purpose_language: str
127
+ fingerprint: str # SHA-256 over the identity fields
128
+ ```
129
+
130
+ `parse_company()` returns `Company` objects:
131
+
132
+ ```python
133
+ @dataclass(frozen=True)
134
+ class Company:
135
+ name: str
136
+ ehraid: int
137
+ uid: str
138
+ chid: str
139
+ canton: str
140
+ status: str # "ACTIVE", "IN_LIQUIDATION", "DELETED"
141
+ capital_nominal: Decimal | None
142
+ capital_currency: str
143
+ deletion_date: date | None
144
+ cantonal_excerpt_web: str
145
+ head_offices: list[RelatedEntity]
146
+ further_head_offices: list[RelatedEntity]
147
+ branch_offices: list[RelatedEntity]
148
+ has_taken_over: list[RelatedEntity]
149
+ was_taken_over_by: list[RelatedEntity]
150
+ audit_companies: list[RelatedEntity]
151
+ old_names: list[OldName]
152
+ ```
153
+
154
+ ## Identifiers
155
+
156
+ The same UID appears in three forms depending on where you read it, so the library
157
+ converts between them:
158
+
159
+ ```python
160
+ from zefix_parser import clean_uid, format_uid, is_valid_uid, normalize_uid
161
+
162
+ normalize_uid("CHE-105.215.703") # "CHE105215703" (LINDAS form)
163
+ format_uid("CHE105215703") # "CHE-105.215.703" (REST form)
164
+ clean_uid("VAT: CHE-109.807.630 MWST") # "CHE109807630"
165
+ is_valid_uid("CHE123456789") # False - the check digit fails
166
+ ```
167
+
168
+ `is_valid_uid()` runs the ISO 7064 mod 11-10 check digit. It is worth calling before you
169
+ spend a request: a mistyped UID can never match the register, and the register's own
170
+ placeholder `CHE123456789` fails it.
171
+
172
+ ## Legal forms
173
+
174
+ Both registers identify a legal form by its four-digit eCH-0097 code, and the name differs
175
+ by language:
176
+
177
+ ```python
178
+ from zefix_parser import LegalForm, legal_form_abbreviation, legal_form_name
179
+
180
+ LegalForm.CORPORATION # "0106"
181
+ legal_form_name("0106", "de") # "Aktiengesellschaft"
182
+ legal_form_name("0106", "fr") # "Société anonyme"
183
+ legal_form_name("0106", "it") # "Società anonima"
184
+ legal_form_abbreviation("0107", "fr") # "Sàrl"
185
+ ```
186
+
187
+ The full table is in the [legal forms reference](https://zefix-parser.readthedocs.io/en/latest/legal-forms.html).
188
+
189
+ ## Former names
190
+
191
+ The register re-typesets a company's name whenever anything else about it changes, so
192
+ `company.old_names` fills up with entries that differ from the current name only in casing
193
+ or spacing: "QualiCasa AG" against "Qualicasa AG". Apply them and you retire the company's
194
+ live name. `meaningful_old_names()` drops them:
195
+
196
+ ```python
197
+ from zefix_parser import meaningful_old_names
198
+
199
+ for old in meaningful_old_names(company):
200
+ print(old.sequence_nr, old.name) # only the genuine renames, oldest first
201
+ ```
202
+
203
+ ## API reference
204
+
205
+ ### `zefix_parser.parse_entity_page(content: bytes) -> list[RegistryEntity]`
206
+
207
+ Parse a SPARQL JSON results document into companies. One company spans several rows, one
208
+ per language-tagged literal; rows are grouped by entity URI and folded down to a single
209
+ deterministic value per field.
210
+
211
+ ### `zefix_parser.parse_company(data: dict | list) -> Company`
212
+
213
+ Parse one `/company/...` response. The API is inconsistent about whether a single company
214
+ comes back as an object or a one-element list; both work.
215
+
216
+ ### `zefix_parser.build_combined_page_query(*, cursor_after=None, limit=500) -> str`
217
+
218
+ Build the SPARQL query for one keyset page of full records. Use this to run the crawl
219
+ yourself, outside `LindasClient`.
220
+
221
+ ### `zefix_parser.client.LindasClient`
222
+
223
+ SPARQL client. `count()`, `iter_entities()`, `iter_pages()` (which yields `RawPage` objects
224
+ carrying the cursor, so a crawl can be resumed), `fetch_by_uids()`, `query()`.
225
+
226
+ ### `zefix_parser.client.ZefixRestClient`
227
+
228
+ PublicREST client. `get_by_uid()`, `get_by_ehraid()`, `get_by_chid()`, `search()`,
229
+ `sogc_by_date()`, `sogc()`.
230
+
231
+ ## Background
232
+
233
+ Zefix (the Zentraler Firmenindex, *index central des raisons de commerce*, *indice centrale
234
+ delle ditte*, officially the Swiss Central Business Name Index) is the federal index over
235
+ the 26 cantonal commercial registers: Handelsregister, registre du commerce, registro di
236
+ commercio. Every company registered in Switzerland appears in it, identified by a UID
237
+ (`CHE-123.456.789`), a CHID and an EHRAID.
238
+
239
+ The Federal Office of Justice publishes the index twice. LINDAS carries every company as
240
+ linked data, open to anyone, holding each company's current state. The PublicREST API holds
241
+ more per company, behind credentials, and answers one lookup at a time. Neither publishes a
242
+ change feed; for that you need the gazette, which is what
243
+ [shab-parser](https://shab-parser.readthedocs.io) reads.
244
+
245
+ ## License
246
+
247
+ MIT
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "zefix-parser"
3
+ version = "0.1.0"
4
+ description = "Typed Python client for Zefix, the Swiss Central Business Name Index (Zentraler Firmenindex)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Prospex", email = "hello@prospex.ch" },
9
+ ]
10
+ requires-python = ">=3.14"
11
+ keywords = ["zefix", "swiss", "commercial-register", "handelsregister", "lindas", "sparql", "uid"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Topic :: Office/Business :: Financial",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.optional-dependencies]
23
+ http = ["httpx>=0.27"]
24
+ dev = ["pytest>=8", "httpx>=0.27"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://prospex.ch"
28
+ Repository = "https://github.com/prospex-ch/python-zefix-parser"
29
+ Documentation = "https://zefix-parser.readthedocs.io"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.11.32,<0.12.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
@@ -0,0 +1,103 @@
1
+ """Typed Python client for Zefix, the Swiss Central Business Name Index."""
2
+
3
+ from .schemas import (
4
+ AuthenticationError,
5
+ Company,
6
+ CompanyStatus,
7
+ Language,
8
+ OldName,
9
+ ParserError,
10
+ RawPage,
11
+ RegistryEntity,
12
+ RelatedEntity,
13
+ RelationType,
14
+ RetryableError,
15
+ SogcPublication,
16
+ TransportError,
17
+ ZefixError,
18
+ )
19
+ from .identifiers import (
20
+ clean_uid,
21
+ format_chid,
22
+ format_uid,
23
+ is_valid_uid,
24
+ normalize_chid,
25
+ normalize_uid,
26
+ uid_check_digit_ok,
27
+ )
28
+ from .legal_forms import (
29
+ LEGAL_FORM_ABBREVIATIONS,
30
+ LEGAL_FORM_LABELS,
31
+ LegalForm,
32
+ legal_form_abbreviation,
33
+ legal_form_name,
34
+ )
35
+ from .lindas import (
36
+ QUERY_VERSION,
37
+ build_combined_page_query,
38
+ build_count_query,
39
+ build_detail_query,
40
+ build_detail_query_by_uids,
41
+ build_uri_page_query,
42
+ compute_fingerprint,
43
+ parse_count,
44
+ parse_entity_page,
45
+ parse_sparql_json,
46
+ parse_uri_page,
47
+ )
48
+ from .rest import (
49
+ PARSER_VERSION,
50
+ meaningful_old_names,
51
+ normalize_name,
52
+ parse_companies,
53
+ parse_company,
54
+ parse_sogc,
55
+ parse_sogc_list,
56
+ )
57
+
58
+ __all__ = [
59
+ "AuthenticationError",
60
+ "Company",
61
+ "CompanyStatus",
62
+ "LEGAL_FORM_ABBREVIATIONS",
63
+ "LEGAL_FORM_LABELS",
64
+ "Language",
65
+ "LegalForm",
66
+ "OldName",
67
+ "PARSER_VERSION",
68
+ "QUERY_VERSION",
69
+ "ParserError",
70
+ "RawPage",
71
+ "RegistryEntity",
72
+ "RelatedEntity",
73
+ "RelationType",
74
+ "RetryableError",
75
+ "SogcPublication",
76
+ "TransportError",
77
+ "ZefixError",
78
+ "build_combined_page_query",
79
+ "build_count_query",
80
+ "build_detail_query",
81
+ "build_detail_query_by_uids",
82
+ "build_uri_page_query",
83
+ "clean_uid",
84
+ "compute_fingerprint",
85
+ "format_chid",
86
+ "format_uid",
87
+ "is_valid_uid",
88
+ "legal_form_abbreviation",
89
+ "legal_form_name",
90
+ "meaningful_old_names",
91
+ "normalize_chid",
92
+ "normalize_name",
93
+ "normalize_uid",
94
+ "parse_companies",
95
+ "parse_company",
96
+ "parse_count",
97
+ "parse_entity_page",
98
+ "parse_sogc",
99
+ "parse_sogc_list",
100
+ "parse_sparql_json",
101
+ "parse_uri_page",
102
+ "uid_check_digit_ok",
103
+ ]
@@ -0,0 +1,37 @@
1
+ """Shared text normalization for language-tagged register literals.
2
+
3
+ Both LINDAS and the REST API return the same company under several language
4
+ tags. These helpers pick one, always the same one for a given input.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ #: Order in which a language-tagged literal is preferred. The empty string is
12
+ #: an untagged literal, which the register uses for language-neutral values.
13
+ LANGUAGE_PRIORITY = ("de", "fr", "it", "en", "")
14
+
15
+ _WHITESPACE = re.compile(r"\s+")
16
+
17
+
18
+ def normalize_whitespace(text: str) -> str:
19
+ """*text* with runs of whitespace collapsed to single spaces and stripped."""
20
+ return _WHITESPACE.sub(" ", text or "").strip()
21
+
22
+
23
+ def pick_label(labels: dict[str, str]) -> str:
24
+ """The best label from a ``{language: value}`` mapping, or ``""``."""
25
+ value, _ = pick_label_with_language(labels)
26
+ return value
27
+
28
+
29
+ def pick_label_with_language(labels: dict[str, str]) -> tuple[str, str]:
30
+ """The best label and the language tag it came from, or ``("", "")``."""
31
+ for lang in LANGUAGE_PRIORITY:
32
+ if lang in labels:
33
+ return labels[lang], lang
34
+ if labels:
35
+ lang, value = next(iter(labels.items()))
36
+ return value, lang
37
+ return "", ""