asciichem 0.31.12 → 0.31.14
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.adoc +90 -0
- data/lib/asciichem/resolver/common_chemistry.rb +19 -1
- data/lib/asciichem/resolver.rb +11 -4
- data/lib/asciichem/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 380b64ab12e3ccc18a554a5d1dc7793046bfa7ea5ba80d51330bb5d713ea618f
|
|
4
|
+
data.tar.gz: 4b3ec1535d5854626b5e2976adb060b4432bfb7f9d20331e79717ae0db4cdf73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49ca402649027d0091acb25b77848e46a9ffaa5bf876e16512f14378d507a3bf32a6dbbb6245967bc54d1eb6728f0672a530db0e92c8db476ca9bfbdb87497f6
|
|
7
|
+
data.tar.gz: 1741914b553ebe4f1828fa3a294c3ed0249a22072dd90f57185d92aafea717a97eaded37236c97ac3b406032f2919cd449818ba6cf7a6094fdd37a81c7f5c44e
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to AsciiChem are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.31.14] - 2026-11-12
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- The Common Chemistry adapter accepts a user-provided API key:
|
|
10
|
+
`CommonChemistry.new(api_key: ...)` or the
|
|
11
|
+
`COMMON_CHEMISTRY_API_KEY` environment variable (sent as the
|
|
12
|
+
`apikey` query parameter). CAS key-gated the API — without a key
|
|
13
|
+
live lookups answer 401 and the resolver skips the source.
|
|
14
|
+
|
|
15
|
+
## [0.31.13] - 2026-11-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `Resolver.resolve_all` isolates source-level failures: a live probe
|
|
19
|
+
found the Common Chemistry API now answers 401 without an API key
|
|
20
|
+
(CAS key-gated the endpoint); with it registered by default, that
|
|
21
|
+
401 raised through the multi-source fan-out and took the other
|
|
22
|
+
sources down with it. A source-level error now skips that source —
|
|
23
|
+
callers see what answered.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- The README documents the full capability surface: structures,
|
|
27
|
+
lint, local identity, resolution with provenance, citations, and
|
|
28
|
+
the command line.
|
|
29
|
+
|
|
6
30
|
## [0.31.12] - 2026-11-11
|
|
7
31
|
|
|
8
32
|
### Changed
|
data/README.adoc
CHANGED
|
@@ -45,6 +45,96 @@ formula.to_mathml # => "<math>...</math>"
|
|
|
45
45
|
formula.to_text # => "H_2O" (round trip)
|
|
46
46
|
----
|
|
47
47
|
|
|
48
|
+
The parsed model is the canonical form: MathML, HTML, LaTeX, text,
|
|
49
|
+
linear and 2D structural SVG, and CML are views over the same tree.
|
|
50
|
+
|
|
51
|
+
=== Structures: SMILES and molfile in, AsciiChem out
|
|
52
|
+
|
|
53
|
+
[source,ruby]
|
|
54
|
+
----
|
|
55
|
+
formula = AsciiChem.parse_smiles("CC(=O)Oc1ccccc1C(=O)O")
|
|
56
|
+
formula.to_structural_svg # 2D depiction, deterministic layout
|
|
57
|
+
formula.to_smiles # canonical aromatic SMILES
|
|
58
|
+
formula.to_text # AsciiChem text
|
|
59
|
+
|
|
60
|
+
formula = AsciiChem.parse_molfile(molfile_text) # V2000 in and out
|
|
61
|
+
----
|
|
62
|
+
|
|
63
|
+
=== Lint
|
|
64
|
+
|
|
65
|
+
[source,ruby]
|
|
66
|
+
----
|
|
67
|
+
AsciiChem::Linter.run(formula) # chemistry diagnostics: balance,
|
|
68
|
+
# valence, charge, isotopes, rings,
|
|
69
|
+
# identifier consistency, ...
|
|
70
|
+
AsciiChem::Linter.errors?(formula) # => true/false
|
|
71
|
+
----
|
|
72
|
+
|
|
73
|
+
=== Local identity: InChI, AuxInfo, InChIKey
|
|
74
|
+
|
|
75
|
+
[source,ruby]
|
|
76
|
+
----
|
|
77
|
+
require "asciichem/inchi"
|
|
78
|
+
|
|
79
|
+
# the native chemomer engine (byte-exact vs the IUPAC oracle;
|
|
80
|
+
# CHEMOMER_LIB points at the built cdylib) or the IUPAC binary:
|
|
81
|
+
AsciiChem::Inchi.engine = AsciiChem::Inchi::NativeEngine.new
|
|
82
|
+
|
|
83
|
+
identity = AsciiChem::Inchi.identity_for(molecule)
|
|
84
|
+
identity.inchi # "InChI=1S/C9H8O4/..."
|
|
85
|
+
identity.inchikey # "BSYNRYMUTXBXSQ-UHFFFAOYSA-N"
|
|
86
|
+
----
|
|
87
|
+
|
|
88
|
+
=== Resolution: identifiers to substance records
|
|
89
|
+
|
|
90
|
+
[source,ruby]
|
|
91
|
+
----
|
|
92
|
+
require "asciichem/resolver"
|
|
93
|
+
|
|
94
|
+
substance = AsciiChem::Resolver.resolve(value: "50-78-2", convention: "cas")
|
|
95
|
+
substance.preferred_name # => "Aspirin"
|
|
96
|
+
substance.structure.to_smiles # from the source record
|
|
97
|
+
substance.identifiers.first.provenance.source # "common_chemistry"
|
|
98
|
+
|
|
99
|
+
# all shipped sources (PubChem, OPSIN, CAS Common Chemistry) resolve
|
|
100
|
+
# by default; attribution rides every record. Limit the set when a
|
|
101
|
+
# use must stay NC-free:
|
|
102
|
+
AsciiChem::Resolver.resolve(value: "50-78-2", convention: "cas",
|
|
103
|
+
sources: [:pubchem, :opsin])
|
|
104
|
+
----
|
|
105
|
+
|
|
106
|
+
CAS key-gated the Common Chemistry API: live lookups there need a
|
|
107
|
+
key (request one at cas.org/services/commonchemistry-api) exported
|
|
108
|
+
as `COMMON_CHEMISTRY_API_KEY`. Without it, that source answers 401
|
|
109
|
+
and is skipped — PubChem and OPSIN keep resolving.
|
|
110
|
+
|
|
111
|
+
=== Citations: dataset bibitems with provenance
|
|
112
|
+
|
|
113
|
+
[source,ruby]
|
|
114
|
+
----
|
|
115
|
+
require "asciichem/citation"
|
|
116
|
+
|
|
117
|
+
molecule = AsciiChem.parse("CC(=O)OC1=CC=CC=C1C(=O)O @cas(\"50-78-2\") @cite(\"pubchem\")").nodes.first
|
|
118
|
+
AsciiChem::Citation.for_molecule(molecule)
|
|
119
|
+
# => [["pubchem", #<Relaton bibitem>]] - one dataset-type bibitem per
|
|
120
|
+
# (substance, source), anchored by InChIKey, with a provenance note
|
|
121
|
+
# (retrieved from, version, attribution).
|
|
122
|
+
----
|
|
123
|
+
|
|
124
|
+
== Command line
|
|
125
|
+
|
|
126
|
+
[source,sh]
|
|
127
|
+
----
|
|
128
|
+
asciichem convert -i "H_2O" -t mathml # also: --from smiles|molfile
|
|
129
|
+
asciichem identity -i aspirin.smiles --from smiles
|
|
130
|
+
asciichem resolve --cas 50-78-2 --source common_chemistry
|
|
131
|
+
asciichem validate -i 'H_2O @cas("50-78-2")'
|
|
132
|
+
asciichem cite --cas 50-78-2 -t model-json
|
|
133
|
+
asciichem lint -i "2H_2 + O_2 -> 2H_2O"
|
|
134
|
+
asciichem roundtrip -i "C_6H_12O_6"
|
|
135
|
+
asciichem parse-cml -i formula.xml
|
|
136
|
+
----
|
|
137
|
+
|
|
48
138
|
== Conformance
|
|
49
139
|
|
|
50
140
|
The reference implementation of https://www.asciichem.org[the AsciiChem
|
|
@@ -14,14 +14,32 @@ module AsciiChem
|
|
|
14
14
|
# that must stay NC-free limit sources with the
|
|
15
15
|
# `asciichem-sources` attribute (metanorma-plugin-asciichem) or
|
|
16
16
|
# `Resolver.resolve(sources:)`.
|
|
17
|
+
#
|
|
18
|
+
# API access: CAS key-gated the API (401 without a key; request
|
|
19
|
+
# one at cas.org/services/commonchemistry-api — the exact contract
|
|
20
|
+
# arrives with the access email). The adapter sends the key as the
|
|
21
|
+
# `apikey` query parameter; set it per instance (`api_key:`) or
|
|
22
|
+
# through COMMON_CHEMISTRY_API_KEY, which the resolver's own
|
|
23
|
+
# instantiation picks up. Without a key, live lookups answer 401
|
|
24
|
+
# and the resolver skips this source (the other sources still
|
|
25
|
+
# resolve; seeded caches keep working).
|
|
17
26
|
class CommonChemistry < Adapter
|
|
18
27
|
source_name :common_chemistry
|
|
19
28
|
supports "cas"
|
|
20
29
|
|
|
21
30
|
BASE = "https://commonchemistry.cas.org/api/detail"
|
|
22
31
|
|
|
32
|
+
attr_reader :api_key
|
|
33
|
+
|
|
34
|
+
def initialize(api_key: ENV.fetch("COMMON_CHEMISTRY_API_KEY", nil))
|
|
35
|
+
super()
|
|
36
|
+
@api_key = api_key
|
|
37
|
+
end
|
|
38
|
+
|
|
23
39
|
def fetch_record(value:, convention:, fetch:)
|
|
24
|
-
|
|
40
|
+
url = "#{BASE}?cas_rn=#{CGI.escape(value.to_s)}"
|
|
41
|
+
url = "#{url}&apikey=#{CGI.escape(api_key)}" if api_key
|
|
42
|
+
body = fetch.get(url)
|
|
25
43
|
return nil unless body
|
|
26
44
|
|
|
27
45
|
data = JSON.parse(body)
|
data/lib/asciichem/resolver.rb
CHANGED
|
@@ -43,16 +43,23 @@ module AsciiChem
|
|
|
43
43
|
raise Error, "unknown resolver source #{name.inspect} (registered: #{adapters.keys.sort.join(', ')})"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Resolves against every
|
|
47
|
-
# per source that answers (nil
|
|
48
|
-
# in registration order.
|
|
46
|
+
# Resolves against every registered source (or the selected
|
|
47
|
+
# subset), returning one Substance per source that answers (nil
|
|
48
|
+
# entries dropped). Sources answer in registration order. A
|
|
49
|
+
# source-level failure (HTTP error, rejected request) skips that
|
|
50
|
+
# source — one dead or key-gated endpoint must not poison the
|
|
51
|
+
# others; callers see what answered.
|
|
49
52
|
def resolve_all(value:, convention:, sources: nil, fetch: nil, cache: nil, refresh: false)
|
|
50
53
|
selected = sources ? Array(sources).map { |s| self[s] } : adapters.values
|
|
51
54
|
selected.filter_map do |adapter_class|
|
|
52
55
|
adapter = adapter_class.new
|
|
53
56
|
next unless adapter.supports?(convention)
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
begin
|
|
59
|
+
adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh)
|
|
60
|
+
rescue AsciiChem::Error
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
56
63
|
end
|
|
57
64
|
end
|
|
58
65
|
|
data/lib/asciichem/version.rb
CHANGED