swissco 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.
swissco-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: swissco
3
+ Version: 0.1.0
4
+ Summary: Swiss company data from the shell: Zefix and SHAB in one command
5
+ Keywords: swiss,zefix,shab,sogc,commercial-register,handelsregister,cli,lindas
6
+ Author: Prospex
7
+ Author-email: Prospex <hello@prospex.ch>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Office/Business :: Financial
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: zefix-parser[http]>=0.1
18
+ Requires-Dist: shab-parser[http]>=0.2
19
+ Requires-Dist: pytest>=8 ; extra == 'dev'
20
+ Requires-Python: >=3.14
21
+ Project-URL: Homepage, https://prospex.ch
22
+ Project-URL: Repository, https://github.com/prospex-ch/swissco-cli
23
+ Project-URL: Documentation, https://swissco.readthedocs.io
24
+ Provides-Extra: dev
25
+ Description-Content-Type: text/markdown
26
+
27
+ # swissco
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/swissco)](https://pypi.org/project/swissco/)
30
+ [![Documentation](https://readthedocs.org/projects/swissco/badge/?version=latest)](https://swissco.readthedocs.io/en/latest/)
31
+
32
+ Swiss company data from the shell. Look a company up by UID, search 790,000 of them
33
+ by name or by what they say they do, list gazette publications, or watch a list of
34
+ companies for change.
35
+
36
+ ```bash
37
+ uvx swissco lookup CHE-444.420.929
38
+ ```
39
+
40
+ No signup, no API key, no config file. Two federal open-data sources do the work:
41
+ [Zefix on LINDAS](https://ld.admin.ch/) for the commercial register, and the
42
+ [Amtsblattportal](https://amtsblattportal.ch) for the Swiss Official Gazette of Commerce.
43
+
44
+ Built and maintained by [Prospex](https://prospex.ch), a Swiss B2B sales intelligence platform.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ uvx swissco --help # run it without installing
50
+ pip install swissco # or install it
51
+ ```
52
+
53
+ Python 3.14 or newer.
54
+
55
+ ## Commands
56
+
57
+ Every command takes `--format table|json|csv`, `--limit` and `--quiet`.
58
+
59
+ ### `swissco lookup`
60
+
61
+ Everything the register publishes about one company.
62
+
63
+ ```console
64
+ $ swissco lookup CHE-444.420.929
65
+ legal name Baumberger Bau AG
66
+ uid CHE-444.420.929
67
+ chid CH-036.9.103.786-9
68
+ ehra id 1702823
69
+ legal form code 0151
70
+ legal form Schweizerische Zweigniederlassung im Handelsregister eingetragen
71
+ municipality Koppigen
72
+ canton BE
73
+ address Hauptstrasse 6, 3425 Koppigen
74
+ purpose Anbieten von Kleintransporte aller Art.
75
+ zefix uri https://register.ld.admin.ch/zefix/company/1702823
76
+ ```
77
+
78
+ Accepts `CHE-444.420.929`, `CHE444420929`, or the UID buried in other text. The check
79
+ digit is verified before any request goes out.
80
+
81
+ ### `swissco search`
82
+
83
+ Companies by legal name **or by statutory purpose**: how a company describes what it
84
+ does, in its own words, in the register.
85
+
86
+ ```console
87
+ $ swissco search "usinage" --canton VD --limit 5
88
+ LEGAL_NAME UID LEGAL_FORM MUNICIPALITY CANTON PURPOSE
89
+ ------------------------------------- --------------- ------------------------------------------------- ------------ ------ ------------------------------------------------------------
90
+ Atelier roue libre S.A. CHE-261.821.895 Aktiengesellschaft Penthalaz VD La société a pour but l'usinage, la réparation, la révision…
91
+ Cute Cut Sàrl CHE-403.577.924 Gesellschaft mit beschränkter Haftung GMBH / SARL Lausanne VD la société a pour but toutes activités, notamment la fabric…
92
+ DecoupART CNC Sàrl CHE-342.723.896 Gesellschaft mit beschränkter Haftung GMBH / SARL Paudex VD la société a pour but tous types de travaux dans les domain…
93
+ LAVA Technologies Sàrl en liquidation CHE-217.431.384 Gesellschaft mit beschränkter Haftung GMBH / SARL Nyon VD la société a pour but: développement de machines d'usinage …
94
+ Pousaz Mécanique SA CHE-166.712.190 Aktiengesellschaft Oron VD La société a pour but l'exploitation d'un atelier mécanique…
95
+ ```
96
+
97
+ `--canton` takes a two-letter code, `--legal-form` an eCH-0097 code (`0106` is an AG,
98
+ `0107` a GmbH).
99
+
100
+ ### `swissco publications`
101
+
102
+ Commercial-register publications from the gazette, in a date range.
103
+
104
+ ```bash
105
+ swissco publications --canton ZH --since 2026-08-01
106
+ swissco publications --canton ZH --since 2026-08-01 --type CAPITAL_INCREASED
107
+ ```
108
+
109
+ Without `--type`, only the list pages are read: one request per 2,000 publications.
110
+ With `--type`, each surviving publication's body is fetched and classified into the
111
+ [eleven event types](https://shab-parser.readthedocs.io/en/latest/events.html)
112
+ `shab-parser` recognises, so narrow the range and the canton first.
113
+
114
+ Ranges past about ten days are split into windows automatically. The API rejects any
115
+ request whose page offset reaches 10,000, and the gazette publishes around a thousand
116
+ commercial-register entries a day.
117
+
118
+ ### `swissco events`
119
+
120
+ One company's registry history.
121
+
122
+ ```bash
123
+ swissco events CHE-444.420.929 --since 2024-01-01
124
+ ```
125
+
126
+ The gazette's list pages carry no UID, only a title, so this resolves the UID to a
127
+ legal name, keeps the publications whose title looks like that name, then fetches
128
+ those bodies and keeps the events whose own UID matches. The title match decides
129
+ what is worth downloading; the body's UID decides what is reported. Fetched bodies
130
+ are cached under the state directory, so an overlapping re-run costs nothing.
131
+
132
+ ### `swissco watch`
133
+
134
+ What changed since last time.
135
+
136
+ ```bash
137
+ printf 'CHE-444.420.929\nCHE-105.943.826\n' > uids.txt
138
+ swissco watch uids.txt --state ~/.swissco/
139
+ ```
140
+
141
+ Each company is compared by fingerprint, a digest over its identity, address and
142
+ purpose fields. Exits `10` when something changed and `0` when nothing did, so cron
143
+ can branch on it:
144
+
145
+ ```crontab
146
+ 0 7 * * * swissco watch ~/uids.txt --quiet --format json > ~/changes.json \
147
+ || mail -s "registry changes" me@example.com < ~/changes.json
148
+ ```
149
+
150
+ A UID that has left the dataset is reported as **no longer in the dataset**, never as
151
+ deleted. LINDAS carries active entities and is not a historical export, so a UID can
152
+ leave it for reasons that have nothing to do with the company ending.
153
+
154
+ ## Output
155
+
156
+ `--format` is the only thing that changes the output. A command piped into `jq` and
157
+ the same command watched by a person produce identical bytes, so a script that works
158
+ in your terminal works in CI.
159
+
160
+ `table` and `csv` are rendered from the same rows that `json` serialises, so a column
161
+ cannot appear in one format and be missing from another. Progress notes go to stderr,
162
+ where `--quiet` silences them; errors go to stderr as a single JSON object.
163
+
164
+ ## Access and terms
165
+
166
+ **SHAB / Amtsblattportal.** The REST API is the channel the operator offers for
167
+ machine access: freely accessible, no authentication for published data, no
168
+ documented rate limit, page size capped at 2,000. The website UI is disallowed by
169
+ `robots.txt`, which does not reach the API. The operator disclaims completeness, and
170
+ only the signed PDF is legally binding.
171
+
172
+ **Zefix on LINDAS.** Published on [opendata.swiss](https://opendata.swiss), no
173
+ credentials. Commercial-use terms have never been settled; the
174
+ [dataset page](https://opendata.swiss/en/dataset/zefix-lindas) states what applies.
175
+
176
+ **Zefix PublicREST.** Requires credentials issued by `zefix@bj.admin.ch`. Every
177
+ command here works without them. Supplying them through `--user`/`--password` or
178
+ `ZEFIX_USER`/`ZEFIX_PASSWORD` adds capital, status, deletion date, former names and
179
+ corporate relations to `lookup`, and a name-prefix search to `search`.
180
+
181
+ ## Being a good citizen
182
+
183
+ These are small public services run by federal offices. `swissco` sends one request
184
+ every 0.5 seconds at most, backs off exponentially on failure, and identifies itself
185
+ with a real `User-Agent` carrying this repository's URL. `--interval` can raise that
186
+ floor and cannot lower it.
187
+
188
+ ## Built on
189
+
190
+ | Package | Does |
191
+ |---|---|
192
+ | [`zefix-parser`](https://pypi.org/project/zefix-parser/) | Zefix: LINDAS SPARQL, PublicREST, UID validation |
193
+ | [`shab-parser`](https://pypi.org/project/shab-parser/) | SHAB: discovery, fetch, parse, eleven-type event classification |
194
+
195
+ Both are MIT and maintained alongside this one. `swissco` is the shell over them.
196
+
197
+ ## Watching more than a list
198
+
199
+ `swissco watch` from cron is the free version of what [Prospex](https://prospex.ch)
200
+ sells. Prospex watches the whole register continuously, joins it to hiring, funding,
201
+ tenders and web signals, and tells you which of those changes is worth a call. If a
202
+ cron job and a UID list cover it, this tool is all you need.
203
+
204
+ ## License
205
+
206
+ MIT
@@ -0,0 +1,180 @@
1
+ # swissco
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/swissco)](https://pypi.org/project/swissco/)
4
+ [![Documentation](https://readthedocs.org/projects/swissco/badge/?version=latest)](https://swissco.readthedocs.io/en/latest/)
5
+
6
+ Swiss company data from the shell. Look a company up by UID, search 790,000 of them
7
+ by name or by what they say they do, list gazette publications, or watch a list of
8
+ companies for change.
9
+
10
+ ```bash
11
+ uvx swissco lookup CHE-444.420.929
12
+ ```
13
+
14
+ No signup, no API key, no config file. Two federal open-data sources do the work:
15
+ [Zefix on LINDAS](https://ld.admin.ch/) for the commercial register, and the
16
+ [Amtsblattportal](https://amtsblattportal.ch) for the Swiss Official Gazette of Commerce.
17
+
18
+ Built and maintained by [Prospex](https://prospex.ch), a Swiss B2B sales intelligence platform.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ uvx swissco --help # run it without installing
24
+ pip install swissco # or install it
25
+ ```
26
+
27
+ Python 3.14 or newer.
28
+
29
+ ## Commands
30
+
31
+ Every command takes `--format table|json|csv`, `--limit` and `--quiet`.
32
+
33
+ ### `swissco lookup`
34
+
35
+ Everything the register publishes about one company.
36
+
37
+ ```console
38
+ $ swissco lookup CHE-444.420.929
39
+ legal name Baumberger Bau AG
40
+ uid CHE-444.420.929
41
+ chid CH-036.9.103.786-9
42
+ ehra id 1702823
43
+ legal form code 0151
44
+ legal form Schweizerische Zweigniederlassung im Handelsregister eingetragen
45
+ municipality Koppigen
46
+ canton BE
47
+ address Hauptstrasse 6, 3425 Koppigen
48
+ purpose Anbieten von Kleintransporte aller Art.
49
+ zefix uri https://register.ld.admin.ch/zefix/company/1702823
50
+ ```
51
+
52
+ Accepts `CHE-444.420.929`, `CHE444420929`, or the UID buried in other text. The check
53
+ digit is verified before any request goes out.
54
+
55
+ ### `swissco search`
56
+
57
+ Companies by legal name **or by statutory purpose**: how a company describes what it
58
+ does, in its own words, in the register.
59
+
60
+ ```console
61
+ $ swissco search "usinage" --canton VD --limit 5
62
+ LEGAL_NAME UID LEGAL_FORM MUNICIPALITY CANTON PURPOSE
63
+ ------------------------------------- --------------- ------------------------------------------------- ------------ ------ ------------------------------------------------------------
64
+ Atelier roue libre S.A. CHE-261.821.895 Aktiengesellschaft Penthalaz VD La société a pour but l'usinage, la réparation, la révision…
65
+ Cute Cut Sàrl CHE-403.577.924 Gesellschaft mit beschränkter Haftung GMBH / SARL Lausanne VD la société a pour but toutes activités, notamment la fabric…
66
+ DecoupART CNC Sàrl CHE-342.723.896 Gesellschaft mit beschränkter Haftung GMBH / SARL Paudex VD la société a pour but tous types de travaux dans les domain…
67
+ LAVA Technologies Sàrl en liquidation CHE-217.431.384 Gesellschaft mit beschränkter Haftung GMBH / SARL Nyon VD la société a pour but: développement de machines d'usinage …
68
+ Pousaz Mécanique SA CHE-166.712.190 Aktiengesellschaft Oron VD La société a pour but l'exploitation d'un atelier mécanique…
69
+ ```
70
+
71
+ `--canton` takes a two-letter code, `--legal-form` an eCH-0097 code (`0106` is an AG,
72
+ `0107` a GmbH).
73
+
74
+ ### `swissco publications`
75
+
76
+ Commercial-register publications from the gazette, in a date range.
77
+
78
+ ```bash
79
+ swissco publications --canton ZH --since 2026-08-01
80
+ swissco publications --canton ZH --since 2026-08-01 --type CAPITAL_INCREASED
81
+ ```
82
+
83
+ Without `--type`, only the list pages are read: one request per 2,000 publications.
84
+ With `--type`, each surviving publication's body is fetched and classified into the
85
+ [eleven event types](https://shab-parser.readthedocs.io/en/latest/events.html)
86
+ `shab-parser` recognises, so narrow the range and the canton first.
87
+
88
+ Ranges past about ten days are split into windows automatically. The API rejects any
89
+ request whose page offset reaches 10,000, and the gazette publishes around a thousand
90
+ commercial-register entries a day.
91
+
92
+ ### `swissco events`
93
+
94
+ One company's registry history.
95
+
96
+ ```bash
97
+ swissco events CHE-444.420.929 --since 2024-01-01
98
+ ```
99
+
100
+ The gazette's list pages carry no UID, only a title, so this resolves the UID to a
101
+ legal name, keeps the publications whose title looks like that name, then fetches
102
+ those bodies and keeps the events whose own UID matches. The title match decides
103
+ what is worth downloading; the body's UID decides what is reported. Fetched bodies
104
+ are cached under the state directory, so an overlapping re-run costs nothing.
105
+
106
+ ### `swissco watch`
107
+
108
+ What changed since last time.
109
+
110
+ ```bash
111
+ printf 'CHE-444.420.929\nCHE-105.943.826\n' > uids.txt
112
+ swissco watch uids.txt --state ~/.swissco/
113
+ ```
114
+
115
+ Each company is compared by fingerprint, a digest over its identity, address and
116
+ purpose fields. Exits `10` when something changed and `0` when nothing did, so cron
117
+ can branch on it:
118
+
119
+ ```crontab
120
+ 0 7 * * * swissco watch ~/uids.txt --quiet --format json > ~/changes.json \
121
+ || mail -s "registry changes" me@example.com < ~/changes.json
122
+ ```
123
+
124
+ A UID that has left the dataset is reported as **no longer in the dataset**, never as
125
+ deleted. LINDAS carries active entities and is not a historical export, so a UID can
126
+ leave it for reasons that have nothing to do with the company ending.
127
+
128
+ ## Output
129
+
130
+ `--format` is the only thing that changes the output. A command piped into `jq` and
131
+ the same command watched by a person produce identical bytes, so a script that works
132
+ in your terminal works in CI.
133
+
134
+ `table` and `csv` are rendered from the same rows that `json` serialises, so a column
135
+ cannot appear in one format and be missing from another. Progress notes go to stderr,
136
+ where `--quiet` silences them; errors go to stderr as a single JSON object.
137
+
138
+ ## Access and terms
139
+
140
+ **SHAB / Amtsblattportal.** The REST API is the channel the operator offers for
141
+ machine access: freely accessible, no authentication for published data, no
142
+ documented rate limit, page size capped at 2,000. The website UI is disallowed by
143
+ `robots.txt`, which does not reach the API. The operator disclaims completeness, and
144
+ only the signed PDF is legally binding.
145
+
146
+ **Zefix on LINDAS.** Published on [opendata.swiss](https://opendata.swiss), no
147
+ credentials. Commercial-use terms have never been settled; the
148
+ [dataset page](https://opendata.swiss/en/dataset/zefix-lindas) states what applies.
149
+
150
+ **Zefix PublicREST.** Requires credentials issued by `zefix@bj.admin.ch`. Every
151
+ command here works without them. Supplying them through `--user`/`--password` or
152
+ `ZEFIX_USER`/`ZEFIX_PASSWORD` adds capital, status, deletion date, former names and
153
+ corporate relations to `lookup`, and a name-prefix search to `search`.
154
+
155
+ ## Being a good citizen
156
+
157
+ These are small public services run by federal offices. `swissco` sends one request
158
+ every 0.5 seconds at most, backs off exponentially on failure, and identifies itself
159
+ with a real `User-Agent` carrying this repository's URL. `--interval` can raise that
160
+ floor and cannot lower it.
161
+
162
+ ## Built on
163
+
164
+ | Package | Does |
165
+ |---|---|
166
+ | [`zefix-parser`](https://pypi.org/project/zefix-parser/) | Zefix: LINDAS SPARQL, PublicREST, UID validation |
167
+ | [`shab-parser`](https://pypi.org/project/shab-parser/) | SHAB: discovery, fetch, parse, eleven-type event classification |
168
+
169
+ Both are MIT and maintained alongside this one. `swissco` is the shell over them.
170
+
171
+ ## Watching more than a list
172
+
173
+ `swissco watch` from cron is the free version of what [Prospex](https://prospex.ch)
174
+ sells. Prospex watches the whole register continuously, joins it to hiring, funding,
175
+ tenders and web signals, and tells you which of those changes is worth a call. If a
176
+ cron job and a UID list cover it, this tool is all you need.
177
+
178
+ ## License
179
+
180
+ MIT
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "swissco"
3
+ version = "0.1.0"
4
+ description = "Swiss company data from the shell: Zefix and SHAB in one command"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Prospex", email = "hello@prospex.ch" },
9
+ ]
10
+ requires-python = ">=3.14"
11
+ keywords = ["swiss", "zefix", "shab", "sogc", "commercial-register", "handelsregister", "cli", "lindas"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Office/Business :: Financial",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "httpx>=0.27",
23
+ "zefix-parser[http]>=0.1",
24
+ "shab-parser[http]>=0.2",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8"]
29
+
30
+ [project.scripts]
31
+ swissco = "swissco.cli:main"
32
+
33
+ [project.urls]
34
+ Homepage = "https://prospex.ch"
35
+ Repository = "https://github.com/prospex-ch/swissco-cli"
36
+ Documentation = "https://swissco.readthedocs.io"
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.11.32,<0.12.0"]
40
+ build-backend = "uv_build"
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ """Swiss company data from the shell: Zefix and SHAB in one command."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,85 @@
1
+ """The SELECT list and graph pattern a searched entity is read back with.
2
+
3
+ ``zefix-parser`` owns the shape of a :class:`~zefix_parser.RegistryEntity` and
4
+ the parser that folds SPARQL rows into one. It builds queries for the three
5
+ accesses it supports — count, keyset page, detail by URI or UID — but a
6
+ filtered search is not among them, so the pattern has to be restated here.
7
+
8
+ It is a restatement rather than an import because ``_ENTITY_FIELDS`` and
9
+ ``_ENTITY_PATTERN`` are private to that library: reaching into them would tie
10
+ this CLI to an internal name, and a rename upstream would break the search with
11
+ no warning. The coupling that remains is the honest one — every variable below
12
+ must keep the name ``zefix_parser.parse_entity_page`` reads it under, which the
13
+ tests assert against a recorded response.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ #: One row per language-tagged literal, folded down by ``parse_entity_page``.
19
+ ENTITY_FIELDS = """\
20
+ SELECT ?entity ?legalName ?name ?uid ?chid ?ehraId
21
+ ?legalFormUri ?legalFormLabel
22
+ ?municipalityUri ?municipalityLabel
23
+ ?canton
24
+ ?streetAddress ?postalCode ?locality
25
+ ?purpose
26
+ """
27
+
28
+ ENTITY_PATTERN = """\
29
+ ?entity schema:legalName ?legalName .
30
+
31
+ OPTIONAL { ?entity schema:name ?name . }
32
+
33
+ OPTIONAL {
34
+ ?entity schema:identifier ?uidNode .
35
+ ?uidNode schema:name "CompanyUID" ;
36
+ schema:value ?uid .
37
+ }
38
+
39
+ OPTIONAL {
40
+ ?entity schema:identifier ?chidNode .
41
+ ?chidNode schema:name "CompanyCHID" ;
42
+ schema:value ?chid .
43
+ }
44
+
45
+ OPTIONAL {
46
+ ?entity schema:identifier ?ehraIdNode .
47
+ ?ehraIdNode schema:name "CompanyEHRAID" ;
48
+ schema:value ?ehraId .
49
+ }
50
+
51
+ OPTIONAL {
52
+ ?entity schema:additionalType ?legalFormUri .
53
+ OPTIONAL { ?legalFormUri (schema:name|rdfs:label) ?legalFormLabel . }
54
+ }
55
+
56
+ OPTIONAL {
57
+ ?entity admin:municipality ?municipalityUri .
58
+ ?municipalityUri schema:name ?municipalityLabel .
59
+ }
60
+
61
+ OPTIONAL { ?entity schema:address/schema:addressRegion ?canton . }
62
+ OPTIONAL { ?entity schema:address/schema:streetAddress ?streetAddress . }
63
+ OPTIONAL { ?entity schema:address/schema:postalCode ?postalCode . }
64
+ OPTIONAL { ?entity schema:address/schema:addressLocality ?locality . }
65
+
66
+ OPTIONAL { ?entity schema:description ?purpose . }
67
+ """
68
+
69
+
70
+ def escape_literal(value: str) -> str:
71
+ """*value* made safe to sit inside a double-quoted SPARQL literal.
72
+
73
+ Nothing upstream does this: every query builder in ``zefix-parser`` takes
74
+ values it produced itself, where this CLI takes them from a shell argument.
75
+ A search term containing a quote would close the literal and let the rest
76
+ of the term be read as query syntax, so the two characters that can do that
77
+ — the quote and the backslash that would smuggle one back in — are escaped,
78
+ along with the newlines a literal may not contain.
79
+ """
80
+ return (
81
+ value.replace("\\", "\\\\")
82
+ .replace('"', '\\"')
83
+ .replace("\n", "\\n")
84
+ .replace("\r", "\\r")
85
+ )