File size: 7,318 Bytes
5796881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""Unit tests for ops.py's pure JSON-walking helpers.

OPS's XML-to-JSON conversion is the trickiest part of this module: text
nodes become {"$": "..."}, attributes become "@name" keys, and a repeated
XML element becomes a list *unless there's only one*, in which case it stays
a bare dict. Every helper here has to handle both shapes; these tests pin
both for each one.
"""

import pytest

from ops import (
    _as_list,
    _text,
    _pick_lang,
    _epodoc_number,
    _extract_title,
    _extract_abstract,
    _extract_classifications,
    _fulltext_paragraphs,
    _to_cql,
    _normalize_epodoc,
)


class TestAsList:
    def test_none_becomes_empty_list(self):
        assert _as_list(None) == []

    def test_single_dict_is_wrapped(self):
        assert _as_list({"a": 1}) == [{"a": 1}]

    def test_list_passes_through_unchanged(self):
        assert _as_list([{"a": 1}, {"a": 2}]) == [{"a": 1}, {"a": 2}]


class TestText:
    def test_extracts_dollar_key(self):
        assert _text({"$": "hello"}) == "hello"

    def test_plain_string_passes_through(self):
        assert _text("hello") == "hello"

    def test_none_for_anything_else(self):
        assert _text(None) is None
        assert _text({"@lang": "en"}) is None


class TestPickLang:
    def test_picks_requested_language_from_list(self):
        nodes = [{"@lang": "de", "$": "Hallo"}, {"@lang": "en", "$": "Hello"}]
        assert _pick_lang(nodes) == {"@lang": "en", "$": "Hello"}

    def test_falls_back_to_first_when_language_missing(self):
        nodes = [{"@lang": "de", "$": "Hallo"}, {"@lang": "fr", "$": "Bonjour"}]
        assert _pick_lang(nodes) == {"@lang": "de", "$": "Hallo"}

    def test_single_dict_not_wrapped_in_a_list_still_works(self):
        node = {"@lang": "en", "$": "Hello"}
        assert _pick_lang(node) == node

    def test_empty_input_returns_none(self):
        assert _pick_lang(None) is None
        assert _pick_lang([]) is None


class TestEpodocNumber:
    def test_prefers_the_epodoc_document_id(self):
        exchange_doc = {
            "@country": "US",
            "@doc-number": "11930446",
            "bibliographic-data": {
                "publication-reference": {
                    "document-id": [
                        {"@document-id-type": "docdb", "doc-number": {"$": "11930446"}},
                        {"@document-id-type": "epodoc", "doc-number": {"$": "US11930446"}},
                    ]
                }
            },
        }
        assert _epodoc_number(exchange_doc) == "US11930446"

    def test_falls_back_to_country_plus_doc_number(self):
        exchange_doc = {"@country": "US", "@doc-number": "11930446", "bibliographic-data": {}}
        assert _epodoc_number(exchange_doc) == "US11930446"

    def test_none_when_nothing_available(self):
        assert _epodoc_number({"bibliographic-data": {}}) is None


class TestExtractTitle:
    def test_from_a_list_of_languages(self):
        bd = {"invention-title": [{"@lang": "en", "$": "Widget"}, {"@lang": "de", "$": "Widget-DE"}]}
        assert _extract_title(bd) == "Widget"

    def test_from_a_single_bare_dict(self):
        bd = {"invention-title": {"@lang": "en", "$": "Widget"}}
        assert _extract_title(bd) == "Widget"

    def test_none_when_missing(self):
        assert _extract_title({}) is None


class TestExtractAbstract:
    def test_joins_paragraphs_from_a_list(self):
        doc = {"abstract": [{"@lang": "en", "p": [{"$": "Part one."}, {"$": "Part two."}]}]}
        assert _extract_abstract(doc) == "Part one. Part two."

    def test_single_paragraph_not_wrapped_in_a_list(self):
        doc = {"abstract": {"@lang": "en", "p": {"$": "Only paragraph."}}}
        assert _extract_abstract(doc) == "Only paragraph."

    def test_none_when_missing(self):
        assert _extract_abstract({}) is None


class TestExtractClassifications:
    def test_builds_codes_from_the_five_part_symbol(self):
        bd = {
            "patent-classifications": {
                "patent-classification": [
                    {
                        "section": {"$": "G"},
                        "class": {"$": "06"},
                        "subclass": {"$": "F"},
                        "main-group": {"$": "17"},
                        "subgroup": {"$": "30"},
                    }
                ]
            }
        }
        codes = _extract_classifications(bd)
        assert len(codes) == 1
        assert codes[0].code == "G06F17/30"
        assert codes[0].description == ""

    def test_dedupes_and_skips_incomplete_entries(self):
        bd = {
            "patent-classifications": {
                "patent-classification": [
                    {"section": {"$": "G"}, "class": {"$": "06"}, "subclass": {"$": "F"}, "main-group": {"$": "17"}, "subgroup": {"$": "30"}},
                    {"section": {"$": "G"}, "class": {"$": "06"}, "subclass": {"$": "F"}, "main-group": {"$": "17"}, "subgroup": {"$": "30"}},
                    {"section": {"$": "H"}},  # incomplete - no class/subclass/main-group
                ]
            }
        }
        codes = _extract_classifications(bd)
        assert [c.code for c in codes] == ["G06F17/30"]

    def test_none_when_no_classifications(self):
        assert _extract_classifications({}) is None


class TestFulltextParagraphs:
    def test_claims_join_claim_text_lines(self):
        data = {
            "ops:world-patent-data": {
                "ftxt:fulltext-documents": {
                    "ftxt:fulltext-document": {
                        "claims": {
                            "@lang": "en",
                            "claim": [
                                {"claim-text": {"$": "1. A widget."}},
                                {"claim-text": [{"$": "2. A gadget,"}, {"$": "further comprising a mechanism."}]},
                            ],
                        }
                    }
                }
            }
        }
        assert _fulltext_paragraphs(data, "claims") == (
            "1. A widget.\n2. A gadget,\nfurther comprising a mechanism."
        )

    def test_description_joins_paragraphs(self):
        data = {
            "ops:world-patent-data": {
                "ftxt:fulltext-documents": {
                    "ftxt:fulltext-document": {
                        "description": {"@lang": "en", "p": [{"$": "Para one."}, {"$": "Para two."}]}
                    }
                }
            }
        }
        assert _fulltext_paragraphs(data, "description") == "Para one.\nPara two."

    def test_none_when_document_missing(self):
        assert _fulltext_paragraphs({"ops:world-patent-data": {}}, "claims") is None


@pytest.mark.parametrize(
    "raw, expected",
    [
        ("widget", 'txt all "widget"'),
        ("agentic ai", 'txt all "agentic ai"'),
        ('pn = "US1234"', 'pn = "US1234"'),
        ("", ""),
        ('has "quotes"', 'txt all "has  quotes"'),
    ],
)
def test_to_cql(raw, expected):
    assert _to_cql(raw) == expected


@pytest.mark.parametrize(
    "raw, expected",
    [
        ("US11930446B2", "US11930446"),
        ("EP4760514A1", "EP4760514"),
        ("US 11930446 B2", "US11930446"),
        ("US11930446", "US11930446"),
    ],
)
def test_normalize_epodoc(raw, expected):
    assert _normalize_epodoc(raw) == expected