asciichem 0.31.16 → 0.31.17
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/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +17 -0
- data/lib/asciichem/resolver/adapter.rb +10 -6
- data/lib/asciichem/resolver/common_chemistry.rb +4 -4
- data/lib/asciichem/resolver/opsin.rb +4 -4
- data/lib/asciichem/resolver/pubchem.rb +4 -4
- data/lib/asciichem/resolver.rb +4 -3
- 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: 96ad0bea754dd62a7b5b92f44cf87d1446a7afc1594634f903fd744c7c7c0326
|
|
4
|
+
data.tar.gz: 00d721ea5ba129cbd6f6b606167469178c83ec17ca53ecf1749160d337ea5639
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4d8d30b7b44a9b4b8ac460708a38e5eaf122a90a3d53c387bda976390eab2e41cf6750742c88755d5147119fa15f58758f4a55c9f846165b21b7ec0986a6bc3
|
|
7
|
+
data.tar.gz: c3e7da0afbcd8f8cce91335c6f722fc797af05d461952b743913e4e12066c25962a3add7db9deca81e7baccde11ac3fcd5b662f1c6bd787b722f608d84b459d7
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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.17] - 2026-11-13
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- The resolver clock seam: `resolve_all` and every adapter accept a
|
|
10
|
+
`now:` ISO-8601 UTC timestamp (nil = the wall clock) that pins
|
|
11
|
+
`retrievedAt` — recorded-fixture runs and the citation goldens that
|
|
12
|
+
consume them compare byte-stably. Mirrors the asciichem-py seam of
|
|
13
|
+
the same name.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- The conformance runner gates the corpus 0.11.0 parity families
|
|
17
|
+
(linter-expectations with engine posture + node matching, resolver
|
|
18
|
+
replay, citation byte-goldens, the 22 CLI contracts); the CI
|
|
19
|
+
corpus pin moves v0.8.0 → v0.11.0. Claims matrix: linter 27 run +
|
|
20
|
+
3 posture skips, resolver 6/6, citation 4/4 byte-equal, CLI 22/22
|
|
21
|
+
(asciichem-ruby#159).
|
|
22
|
+
|
|
6
23
|
## [0.31.16] - 2026-11-12
|
|
7
24
|
|
|
8
25
|
### Changed
|
|
@@ -41,7 +41,11 @@ module AsciiChem
|
|
|
41
41
|
|
|
42
42
|
# Cache-aware resolution. `fetch` must answer #get(url) -> body
|
|
43
43
|
# String (NetFetch by default; specs inject recorded responses).
|
|
44
|
-
|
|
44
|
+
# `now` pins the retrieval clock (an ISO-8601 UTC string) for
|
|
45
|
+
# recorded-fixture runs — the shared corpus pins `retrievedAt`
|
|
46
|
+
# so replayed records compare byte-stably; nil means the wall
|
|
47
|
+
# clock.
|
|
48
|
+
def resolve(value:, convention:, fetch: nil, cache: nil, refresh: false, now: nil)
|
|
45
49
|
cache ||= Cache.default
|
|
46
50
|
fetch ||= NetFetch.new
|
|
47
51
|
key = cache.key_for(self.class.source_name, convention, value)
|
|
@@ -51,22 +55,22 @@ module AsciiChem
|
|
|
51
55
|
return cached if cached
|
|
52
56
|
end
|
|
53
57
|
|
|
54
|
-
substance = fetch_record(value: value, convention: convention, fetch: fetch)
|
|
55
|
-
.tap { |s| s&.identifiers&.each { |i| i.provenance ||= default_provenance } }
|
|
58
|
+
substance = fetch_record(value: value, convention: convention, fetch: fetch, now: now)
|
|
59
|
+
.tap { |s| s&.identifiers&.each { |i| i.provenance ||= default_provenance(now) } }
|
|
56
60
|
cache.write(key, substance) if substance
|
|
57
61
|
substance
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
# Not-found is nil, not an error — sources legitimately answer
|
|
61
65
|
# "unknown substance" during multi-source resolution.
|
|
62
|
-
def fetch_record(
|
|
66
|
+
def fetch_record(value:, convention:, fetch:, now: nil)
|
|
63
67
|
raise NotImplementedError
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
def default_provenance
|
|
70
|
+
def default_provenance(now: nil)
|
|
67
71
|
Provenance.new(
|
|
68
72
|
source: self.class.source_name,
|
|
69
|
-
retrieved_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
73
|
+
retrieved_at: now || Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
70
74
|
attribution: attribution
|
|
71
75
|
)
|
|
72
76
|
end
|
|
@@ -43,7 +43,7 @@ module AsciiChem
|
|
|
43
43
|
@api_key = api_key
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def fetch_record(value:, convention:, fetch:)
|
|
46
|
+
def fetch_record(value:, convention:, fetch:, now: nil)
|
|
47
47
|
headers = {
|
|
48
48
|
"Accept" => "application/json",
|
|
49
49
|
"x-origin" => ORIGIN
|
|
@@ -55,7 +55,7 @@ module AsciiChem
|
|
|
55
55
|
data = JSON.parse(body)
|
|
56
56
|
return nil if data.empty?
|
|
57
57
|
|
|
58
|
-
substance_from(data)
|
|
58
|
+
substance_from(data, now)
|
|
59
59
|
rescue JSON::ParserError => e
|
|
60
60
|
raise Error, "common_chemistry returned malformed JSON: #{e.message}"
|
|
61
61
|
end
|
|
@@ -71,8 +71,8 @@ module AsciiChem
|
|
|
71
71
|
(File.read(KEY_FILE).strip if File.readable?(KEY_FILE))
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def substance_from(data)
|
|
75
|
-
provenance = default_provenance
|
|
74
|
+
def substance_from(data, now)
|
|
75
|
+
provenance = default_provenance(now: now)
|
|
76
76
|
identifiers = [Identifier.new(value: data["rn"], convention: "cas",
|
|
77
77
|
provenance: provenance)]
|
|
78
78
|
add_identifier(identifiers, data, "inchikey", "inchikey", provenance)
|
|
@@ -27,11 +27,11 @@ module AsciiChem
|
|
|
27
27
|
BASE = 'https://opsin.ch.cam.ac.uk/opsin'
|
|
28
28
|
private_constant :BASE
|
|
29
29
|
|
|
30
|
-
def fetch_record(value:, convention: nil, fetch:)
|
|
30
|
+
def fetch_record(value:, convention: nil, fetch:, now: nil)
|
|
31
31
|
body = fetch.get("#{BASE}/#{CGI.escape(value.to_s)}.smiles")
|
|
32
32
|
return nil unless body
|
|
33
33
|
|
|
34
|
-
substance_from(body.strip)
|
|
34
|
+
substance_from(body.strip, now)
|
|
35
35
|
rescue StandardError => e
|
|
36
36
|
raise Error, "opsin request failed: #{e.message}"
|
|
37
37
|
end
|
|
@@ -42,10 +42,10 @@ module AsciiChem
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
-
def substance_from(smiles)
|
|
45
|
+
def substance_from(smiles, now)
|
|
46
46
|
return nil if smiles.empty?
|
|
47
47
|
|
|
48
|
-
provenance = default_provenance
|
|
48
|
+
provenance = default_provenance(now: now)
|
|
49
49
|
Substance.new(
|
|
50
50
|
preferred_name: nil,
|
|
51
51
|
identifiers: [Identifier.new(value: smiles, convention: 'canonical-smiles',
|
|
@@ -26,14 +26,14 @@ module AsciiChem
|
|
|
26
26
|
MolecularWeight IUPACName InChI InChIKey].freeze
|
|
27
27
|
private_constant :BASE, :PROPERTIES
|
|
28
28
|
|
|
29
|
-
def fetch_record(value:, convention:, fetch:)
|
|
29
|
+
def fetch_record(value:, convention:, fetch:, now: nil)
|
|
30
30
|
body = fetch.get(url_for(value, convention))
|
|
31
31
|
return nil unless body
|
|
32
32
|
|
|
33
33
|
props = JSON.parse(body).dig("PropertyTable", "Properties", 0)
|
|
34
34
|
return nil unless props
|
|
35
35
|
|
|
36
|
-
substance_from(props)
|
|
36
|
+
substance_from(props, now)
|
|
37
37
|
rescue JSON::ParserError => e
|
|
38
38
|
raise Error, "pubchem returned malformed JSON: #{e.message}"
|
|
39
39
|
end
|
|
@@ -53,8 +53,8 @@ module AsciiChem
|
|
|
53
53
|
"#{BASE}/#{namespace}/#{CGI.escape(value.to_s)}/property/#{PROPERTIES.join(',')}/JSON"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def substance_from(props)
|
|
57
|
-
provenance = default_provenance
|
|
56
|
+
def substance_from(props, now)
|
|
57
|
+
provenance = default_provenance(now: now)
|
|
58
58
|
identifiers = []
|
|
59
59
|
identifiers << Identifier.new(value: props["CID"].to_s, convention: "pubchem-cid", provenance: provenance) if props["CID"]
|
|
60
60
|
add_identifier(identifiers, props, "InChIKey", "inchikey", provenance)
|
data/lib/asciichem/resolver.rb
CHANGED
|
@@ -48,15 +48,16 @@ module AsciiChem
|
|
|
48
48
|
# entries dropped). Sources answer in registration order. A
|
|
49
49
|
# source-level failure (HTTP error, rejected request) skips that
|
|
50
50
|
# source — one dead or key-gated endpoint must not poison the
|
|
51
|
-
# others; callers see what answered.
|
|
52
|
-
|
|
51
|
+
# others; callers see what answered. `now` pins the retrieval
|
|
52
|
+
# clock for recorded-fixture runs (the adapters' seam).
|
|
53
|
+
def resolve_all(value:, convention:, sources: nil, fetch: nil, cache: nil, refresh: false, now: nil)
|
|
53
54
|
selected = sources ? Array(sources).map { |s| self[s] } : adapters.values
|
|
54
55
|
selected.filter_map do |adapter_class|
|
|
55
56
|
adapter = adapter_class.new
|
|
56
57
|
next unless adapter.supports?(convention)
|
|
57
58
|
|
|
58
59
|
begin
|
|
59
|
-
adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh)
|
|
60
|
+
adapter.resolve(value: value, convention: convention, fetch: fetch, cache: cache, refresh: refresh, now: now)
|
|
60
61
|
rescue AsciiChem::Error
|
|
61
62
|
nil
|
|
62
63
|
end
|
data/lib/asciichem/version.rb
CHANGED