pyjstage2 0.1.2__py3-none-any.whl
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.
- pyjstage/__init__.py +1 -0
- pyjstage/entry.py +76 -0
- pyjstage/errors.py +78 -0
- pyjstage/order.py +21 -0
- pyjstage/parser.py +56 -0
- pyjstage/pyjstage.py +153 -0
- pyjstage/result.py +228 -0
- pyjstage/service.py +11 -0
- pyjstage/status.py +83 -0
- pyjstage2-0.1.2.dist-info/METADATA +160 -0
- pyjstage2-0.1.2.dist-info/RECORD +14 -0
- pyjstage2-0.1.2.dist-info/WHEEL +5 -0
- pyjstage2-0.1.2.dist-info/licenses/LICENSE +21 -0
- pyjstage2-0.1.2.dist-info/top_level.txt +1 -0
pyjstage/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .pyjstage import Pyjstage
|
pyjstage/entry.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class Entry:
|
|
2
|
+
"""Base class for Entry
|
|
3
|
+
|
|
4
|
+
Attributes:
|
|
5
|
+
issn: ISSN
|
|
6
|
+
eissn: eISSN
|
|
7
|
+
cdjournal: journal name
|
|
8
|
+
material_title: journal title
|
|
9
|
+
volume: journal volume
|
|
10
|
+
number: journal number
|
|
11
|
+
starting_page: starting page this document is.
|
|
12
|
+
pubyear: Published year.
|
|
13
|
+
systemcode: System code
|
|
14
|
+
systemname: System name
|
|
15
|
+
title: Title
|
|
16
|
+
link: Link
|
|
17
|
+
id: ID, it is always the same as link parameter.
|
|
18
|
+
updated: updated date
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize Entry class"""
|
|
22
|
+
self.issn = None
|
|
23
|
+
self.eissn = None
|
|
24
|
+
self.cdjournal = None
|
|
25
|
+
self.material_title = None
|
|
26
|
+
self.volume = None
|
|
27
|
+
self.number = None
|
|
28
|
+
self.starting_page = None
|
|
29
|
+
self.pubyear = None
|
|
30
|
+
self.systemcode = None
|
|
31
|
+
self.systemname = None
|
|
32
|
+
self.title = None
|
|
33
|
+
self.link = None
|
|
34
|
+
self.id = None
|
|
35
|
+
self.updated = None
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return str(self.__dict__.items())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ListEntry(Entry):
|
|
42
|
+
"""Entry class for List API
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
vols_title: volumes title
|
|
46
|
+
vols_link: volumes link
|
|
47
|
+
publisher_name: publisher name
|
|
48
|
+
publisher_url: publisher url
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self):
|
|
51
|
+
"""Initialize ListEntry class"""
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.vols_title = {}
|
|
54
|
+
self.vols_link = {}
|
|
55
|
+
self.publisher_name = None
|
|
56
|
+
self.publisher_url = {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SearchEntry(Entry):
|
|
60
|
+
"""Entry class for Search API
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
article_title: article title
|
|
64
|
+
article_link: article link
|
|
65
|
+
author: article author
|
|
66
|
+
ending_page: ending page
|
|
67
|
+
doi: DOI
|
|
68
|
+
"""
|
|
69
|
+
def __init__(self):
|
|
70
|
+
"""Initialize SearchEntry class"""
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.article_title = {}
|
|
73
|
+
self.article_link = None
|
|
74
|
+
self.author = None
|
|
75
|
+
self.ending_page = None
|
|
76
|
+
self.doi = None
|
pyjstage/errors.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
class JstageError(Exception):
|
|
2
|
+
"""Abstract class for errors caused by J-STAGE API"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JstageWarning(Warning):
|
|
7
|
+
"""Abstract class for warnings caused by J-STAGE API"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NoResultsError(JstageError):
|
|
12
|
+
"""ERR_001"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TooManyResultsError(JstageWarning):
|
|
17
|
+
"""WARN_001"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TooManyRequestsError(JstageError):
|
|
22
|
+
"""ERR_003"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidQueryError(JstageError):
|
|
27
|
+
"""ERR_004"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EmptyRequiredFieldError(JstageError):
|
|
32
|
+
"""ERR_005"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidYearValueError(JstageError):
|
|
37
|
+
"""ERR_006"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InvalidCountsError(JstageError):
|
|
42
|
+
"""ERR_007"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvalidIssnError(JstageError):
|
|
47
|
+
"""ERR_008"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SystemFatalError(JstageError):
|
|
52
|
+
"""SYS_ERR_009"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InvalidUrlError(JstageError):
|
|
57
|
+
"""ERR_010"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ListNoQueryError(JstageError):
|
|
62
|
+
"""ERR_011"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SearchNoQueryError(JstageError):
|
|
67
|
+
"""ERR_012"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ListUnspecifiedError(JstageError):
|
|
72
|
+
"""ERR_013"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SearchUnsortableError(JstageError):
|
|
77
|
+
"""ERR_014"""
|
|
78
|
+
pass
|
pyjstage/order.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ListOrder(Enum):
|
|
5
|
+
"""Enum for list's order
|
|
6
|
+
|
|
7
|
+
ListOrder.ASCENDING: sort by ascending
|
|
8
|
+
ListOrder.DESCENDING: sort by descending
|
|
9
|
+
"""
|
|
10
|
+
ASCENDING = 1
|
|
11
|
+
DESCENDING = 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchOrder(Enum):
|
|
15
|
+
"""Enum for search's order
|
|
16
|
+
|
|
17
|
+
SearchOrder.SCORE: sort by score
|
|
18
|
+
SearchOrder.NUMBER: sort by number ascending
|
|
19
|
+
"""
|
|
20
|
+
SCORE = 1
|
|
21
|
+
NUMBER = 2
|
pyjstage/parser.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from lxml import etree
|
|
2
|
+
from .result import Result, ListResult, SearchResult
|
|
3
|
+
from .status import Status
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Parser:
|
|
9
|
+
"""Parser Class"""
|
|
10
|
+
def __init__(self):
|
|
11
|
+
"""Initialize Parser class"""
|
|
12
|
+
self.regex = re.compile(r'\n* +')
|
|
13
|
+
|
|
14
|
+
def parse(self, xml_text: bytes) -> Result:
|
|
15
|
+
"""Parse XML returned from J-STAGE API
|
|
16
|
+
|
|
17
|
+
Parse XML document (as bytes object) into Result object.
|
|
18
|
+
This function may raise Error or Warning.
|
|
19
|
+
If that error starts with Jstage, that error was occurred by J-STAGE.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
xml_text: raw xml text's bytes encoded by UTF-8
|
|
23
|
+
Returns:
|
|
24
|
+
Result object which contains meta data and contents.
|
|
25
|
+
Raises:
|
|
26
|
+
JstageError: error depending on j-stage api
|
|
27
|
+
JstageWarning: warning depending on j-stage api
|
|
28
|
+
"""
|
|
29
|
+
result = Result()
|
|
30
|
+
|
|
31
|
+
root = etree.fromstring(xml_text)
|
|
32
|
+
result.xmlns = root.nsmap
|
|
33
|
+
result.xmlns['xml'] = 'http://www.w3.org/XML/1998/namespace'
|
|
34
|
+
result.xml_lang = root.find('[@xml:lang]', result.xmlns).attrib.values()[0]
|
|
35
|
+
result.xml_version = etree.ElementTree(root).docinfo.xml_version
|
|
36
|
+
result.encoding = etree.ElementTree(root).docinfo.encoding
|
|
37
|
+
result.servicecd = int(root.find('./servicecd', result.xmlns).text)
|
|
38
|
+
result.title = root.find('./title', result.xmlns).text
|
|
39
|
+
result.link = self.regex.sub('', root.find('./link', result.xmlns).attrib['href'])
|
|
40
|
+
result.id = self.regex.sub('', root.find('./id', result.xmlns).text)
|
|
41
|
+
result.updated = datetime.fromisoformat(root.find('./updated', result.xmlns).text)
|
|
42
|
+
result.total_results = int(root.find('./opensearch:totalResults', result.xmlns).text)
|
|
43
|
+
result.start_index = int(root.find('./opensearch:startIndex', result.xmlns).text)
|
|
44
|
+
result.items_per_page = int(root.find('./opensearch:itemsPerPage', result.xmlns).text)
|
|
45
|
+
result.entries = list(root.findall('./entry', result.xmlns))
|
|
46
|
+
result.status = root.find('./result/status', result.xmlns).text
|
|
47
|
+
result.message = root.find('./result/message', result.xmlns).text
|
|
48
|
+
Status.divide(result.status, result.message)
|
|
49
|
+
if result.servicecd == 2:
|
|
50
|
+
rresult = ListResult(result)
|
|
51
|
+
elif result.servicecd == 3:
|
|
52
|
+
rresult = SearchResult(result)
|
|
53
|
+
else:
|
|
54
|
+
raise Exception('Undefined service code')
|
|
55
|
+
|
|
56
|
+
return rresult
|
pyjstage/pyjstage.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from .result import Result
|
|
2
|
+
from .parser import Parser
|
|
3
|
+
from .service import Service
|
|
4
|
+
from .order import ListOrder, SearchOrder
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from typing import List, Union
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Pyjstage:
|
|
11
|
+
"""Pyjstage Class
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
domain: J-STAGE API domain
|
|
15
|
+
parser: Parser object
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, domain: str = 'http://api.jstage.jst.go.jp/searchapi/do?'):
|
|
18
|
+
"""Initialize Pyjstage class
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
domain: (Optional) J-STAGE API domain
|
|
22
|
+
"""
|
|
23
|
+
self.domain: str = domain
|
|
24
|
+
self.parser: Parser = Parser()
|
|
25
|
+
|
|
26
|
+
def list(
|
|
27
|
+
self,
|
|
28
|
+
pubyearfrom: int = None,
|
|
29
|
+
pubyearto: int = None,
|
|
30
|
+
material: Union[str, List[str]] = None,
|
|
31
|
+
issn: str = None,
|
|
32
|
+
cdjournal: str = None,
|
|
33
|
+
volorder: ListOrder = None
|
|
34
|
+
) -> Result:
|
|
35
|
+
"""Access LIST API
|
|
36
|
+
|
|
37
|
+
Access LIST API and parse result as ListResult Object.
|
|
38
|
+
Union[xxx, List[xxx]] arguments can multiple value as list object.
|
|
39
|
+
If you set that argument as list, Search as 'AND'
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
pubyearfrom: (Optional) Year you want to search when papers were published from.
|
|
43
|
+
pubyearto: (Optional) Year you want to search when papers were published to.
|
|
44
|
+
material: (Optional) Keyword journal should contain.
|
|
45
|
+
issn: (Optional) ISSN you want to search.
|
|
46
|
+
cdjournal: (Optional) Journal code you want to search.
|
|
47
|
+
volorder: (Optional) How order are responses sorted.
|
|
48
|
+
Returns:
|
|
49
|
+
ListResult object which contains meta data and contents
|
|
50
|
+
Raises:
|
|
51
|
+
JstageError: Error caused by J-STAGE API
|
|
52
|
+
JstageWarning: Warning caused by J-STAGE API
|
|
53
|
+
"""
|
|
54
|
+
url = self.build_query(
|
|
55
|
+
service=Service.LIST.value,
|
|
56
|
+
pubyearfrom=pubyearfrom,
|
|
57
|
+
pubyearto=pubyearto,
|
|
58
|
+
material=material,
|
|
59
|
+
issn=issn,
|
|
60
|
+
cdjournal=cdjournal,
|
|
61
|
+
volorder=volorder.value)
|
|
62
|
+
response = requests.get(url)
|
|
63
|
+
return self.parser.parse(response.text.encode('utf-8'))
|
|
64
|
+
|
|
65
|
+
def search(
|
|
66
|
+
self,
|
|
67
|
+
pubyearfrom: int = None,
|
|
68
|
+
pubyearto: int = None,
|
|
69
|
+
material: Union[str, List[str]] = None,
|
|
70
|
+
article: Union[str, List[str]] = None,
|
|
71
|
+
author: Union[str, List[str]] = None,
|
|
72
|
+
affile: Union[str, List[str]] = None,
|
|
73
|
+
keyword: Union[str, List[str]] = None,
|
|
74
|
+
abst: Union[str, List[str]] = None,
|
|
75
|
+
text: Union[str, List[str]] = None,
|
|
76
|
+
issn: str = None,
|
|
77
|
+
cdjournal: str = None,
|
|
78
|
+
sortfig: SearchOrder = None,
|
|
79
|
+
vol: int = None,
|
|
80
|
+
no: int = None,
|
|
81
|
+
start: int = None,
|
|
82
|
+
count: int = None
|
|
83
|
+
) -> Result:
|
|
84
|
+
"""Access SEARCH API
|
|
85
|
+
|
|
86
|
+
Access SEARCH API and parse result as SearchResult Object.
|
|
87
|
+
Union[xxx, List[xxx]] arguments can multiple value as list object.
|
|
88
|
+
If you set that argument as list, Search as 'AND'
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
pubyearfrom: (Optional) Year you want to search when papers were published from.
|
|
92
|
+
pubyearto: (Optional) Year you want to search when papers were published to.
|
|
93
|
+
material: (Optional) Keyword journal should contain.
|
|
94
|
+
article: Keyword document's title should contain.
|
|
95
|
+
author: Keyword document's author name should contain.
|
|
96
|
+
affile: Keyword document's author's affile name should contain.
|
|
97
|
+
keyword: Keyword document's keyword should contain.
|
|
98
|
+
abst: Keyword document's abstract should contain.
|
|
99
|
+
text: Keyword document's body should contain
|
|
100
|
+
issn: (Optional) ISSN you want to search.
|
|
101
|
+
cdjournal: (Optional) Journal code you want to search.
|
|
102
|
+
sortfig: How order are responses sorted.
|
|
103
|
+
vol: What volume is document contained in journal
|
|
104
|
+
no: What number is document contained in journal
|
|
105
|
+
start: How many offsets you want to set, default 0.
|
|
106
|
+
count: How many results you want to fetch, max & default is 1000.
|
|
107
|
+
Returns:
|
|
108
|
+
SearchResult object which contains meta data and contents
|
|
109
|
+
Raises:
|
|
110
|
+
xxxError: xxx
|
|
111
|
+
"""
|
|
112
|
+
url = self.build_query(
|
|
113
|
+
service=Service.SEARCH.value,
|
|
114
|
+
pubyearfrom=pubyearfrom,
|
|
115
|
+
pubyearto=pubyearto,
|
|
116
|
+
material=material,
|
|
117
|
+
article=article,
|
|
118
|
+
author=author,
|
|
119
|
+
affile=affile,
|
|
120
|
+
keyword=keyword,
|
|
121
|
+
abst=abst,
|
|
122
|
+
text=text,
|
|
123
|
+
issn=issn,
|
|
124
|
+
cdjournal=cdjournal,
|
|
125
|
+
sortfig=sortfig.value if sortfig else None,
|
|
126
|
+
vol=vol,
|
|
127
|
+
no=no,
|
|
128
|
+
start=start,
|
|
129
|
+
count=count
|
|
130
|
+
)
|
|
131
|
+
response = requests.get(url)
|
|
132
|
+
result = self.parser.parse(response.text.encode('utf-8'))
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def build_query(self, **kwargs):
|
|
136
|
+
"""Build url with queries
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
kwargs: key-value pairs for querying. If multiple values, use list.
|
|
140
|
+
Returns:
|
|
141
|
+
URL-string which can access J-STAGE API
|
|
142
|
+
"""
|
|
143
|
+
return self.domain + '&'.join(
|
|
144
|
+
[
|
|
145
|
+
f'{quote(k, encoding="utf8")}={quote(str(v) if type(v) is not list else " ".join(v), encoding="utf8")}'
|
|
146
|
+
for k, v in kwargs.items() if v is not None
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if __name__ == '__main__':
|
|
151
|
+
pj = Pyjstage()
|
|
152
|
+
ret = pj.search(text=['統合失調症', '精神分裂病'], count=10)
|
|
153
|
+
print(ret)
|
pyjstage/result.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from typing import Dict, Optional, List
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from .entry import ListEntry, SearchEntry
|
|
5
|
+
from lxml import etree
|
|
6
|
+
|
|
7
|
+
def _safe_text(el, default=''):
|
|
8
|
+
"""Extract text from an element, handling None and empty tags."""
|
|
9
|
+
if el is None:
|
|
10
|
+
return default
|
|
11
|
+
text = el.text
|
|
12
|
+
return text.replace('\n', '').strip() if text else default
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_int(el, default=0):
|
|
16
|
+
"""Extract integer from an element, handling None/empty/non-numeric."""
|
|
17
|
+
text = _safe_text(el)
|
|
18
|
+
if not text:
|
|
19
|
+
return default
|
|
20
|
+
try:
|
|
21
|
+
return int(text)
|
|
22
|
+
except (ValueError, TypeError):
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _lang_dict(parent, xmlns):
|
|
27
|
+
"""Build {lang: text} dict from child elements, handling empty tags."""
|
|
28
|
+
if parent is None:
|
|
29
|
+
return {}
|
|
30
|
+
regex = re.compile(r'{.+\}')
|
|
31
|
+
return {
|
|
32
|
+
regex.sub('', t.tag): _safe_text(t)
|
|
33
|
+
for t in parent
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Result:
|
|
38
|
+
"""Base class for Result
|
|
39
|
+
Attributes:
|
|
40
|
+
encoding: Document encoding, it may always be utf-8.
|
|
41
|
+
xml_version: XML version.
|
|
42
|
+
xmlns: What namespaces does this document refer.
|
|
43
|
+
xml_lang: What language is this document written in.
|
|
44
|
+
status: Status code.
|
|
45
|
+
message: Status message.
|
|
46
|
+
title: returned title
|
|
47
|
+
link: returned link
|
|
48
|
+
id: document id, it is always same as link value.
|
|
49
|
+
servicecd: API service code, 2 is list and 3 is search.
|
|
50
|
+
updated: When was this document updated.
|
|
51
|
+
total_results: How many results did API return.
|
|
52
|
+
start_index: What index do results starts.
|
|
53
|
+
items_per_page: How many items does a page contain.
|
|
54
|
+
entries: Entries API returned.
|
|
55
|
+
"""
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.encoding: Optional[str] = None
|
|
58
|
+
self.xml_version: Optional[str] = None
|
|
59
|
+
self.xmlns: Dict[str, str] = {}
|
|
60
|
+
self.xml_lang: Optional[str] = None
|
|
61
|
+
self.status: str = Optional[None]
|
|
62
|
+
self.message: Optional[str] = None
|
|
63
|
+
self.title: Optional[str] = None
|
|
64
|
+
self.link: Optional[str] = None
|
|
65
|
+
self.id: Optional[str] = None
|
|
66
|
+
self.servicecd: Optional[str] = None
|
|
67
|
+
self.updated: Optional[datetime] = None
|
|
68
|
+
self.total_results: Optional[int] = None
|
|
69
|
+
self.start_index: Optional[int] = None
|
|
70
|
+
self.items_per_page: Optional[int] = None
|
|
71
|
+
self.entries: List[etree] = []
|
|
72
|
+
self.entries_temp: List[etree] = []
|
|
73
|
+
|
|
74
|
+
def _finish_setup(self):
|
|
75
|
+
"""remove unused temporary attribute"""
|
|
76
|
+
del self.entries_temp
|
|
77
|
+
|
|
78
|
+
def __str__(self):
|
|
79
|
+
return str(self.__dict__.items())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SearchResult(Result):
|
|
83
|
+
"""Search Result Class
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
encoding: Document encoding, it may always be utf-8.
|
|
87
|
+
xml_version: XML version.
|
|
88
|
+
xmlns: What namespaces does this document refer.
|
|
89
|
+
xml_lang: What language is this document written in.
|
|
90
|
+
status: Status code.
|
|
91
|
+
message: Status message.
|
|
92
|
+
title: returned title
|
|
93
|
+
link: returned link
|
|
94
|
+
id: document id, it is always same as link value.
|
|
95
|
+
servicecd: API service code, 2 is list and 3 is search.
|
|
96
|
+
updated: When was this document updated.
|
|
97
|
+
total_results: How many results did API return.
|
|
98
|
+
start_index: What index do results starts.
|
|
99
|
+
items_per_page: How many items does a page contain.
|
|
100
|
+
entries: Entries API returned.
|
|
101
|
+
"""
|
|
102
|
+
def __init__(self, result: Result):
|
|
103
|
+
"""Initialize SearchResult class
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
result: Result object
|
|
107
|
+
"""
|
|
108
|
+
super().__init__()
|
|
109
|
+
self.encoding = result.encoding
|
|
110
|
+
self.xml_version = result.xml_version
|
|
111
|
+
self.xmlns = result.xmlns
|
|
112
|
+
self.xml_lang = result.xml_lang
|
|
113
|
+
self.status = result.status
|
|
114
|
+
self.message = result.message
|
|
115
|
+
self.title = result.title
|
|
116
|
+
self.link = result.link
|
|
117
|
+
self.id = result.id
|
|
118
|
+
self.servicecd = result.servicecd
|
|
119
|
+
self.updated = result.updated
|
|
120
|
+
self.total_results = result.total_results
|
|
121
|
+
self.start_index = result.start_index
|
|
122
|
+
self.items_per_page = result.items_per_page
|
|
123
|
+
self.entries_temp = result.entries
|
|
124
|
+
self.entries = []
|
|
125
|
+
|
|
126
|
+
for e in self.entries_temp:
|
|
127
|
+
ent = SearchEntry()
|
|
128
|
+
ent.article_title = _lang_dict(e.find('./article_title', self.xmlns), self.xmlns)
|
|
129
|
+
ent.article_link = _lang_dict(e.find('./article_link', self.xmlns), self.xmlns)
|
|
130
|
+
author_el = e.find('./author', self.xmlns)
|
|
131
|
+
if author_el is not None:
|
|
132
|
+
ent.author = {}
|
|
133
|
+
regex = re.compile(r'{.+\}')
|
|
134
|
+
for t in author_el:
|
|
135
|
+
children = list(t)
|
|
136
|
+
ent.author[regex.sub('', t.tag)] = _safe_text(children[0]) if children else ''
|
|
137
|
+
else:
|
|
138
|
+
ent.author = {}
|
|
139
|
+
ent.cdjournal = _safe_text(e.find('./cdjournal', self.xmlns))
|
|
140
|
+
ent.material_title = _lang_dict(e.find('./material_title', self.xmlns), self.xmlns)
|
|
141
|
+
ent.issn = _safe_text(e.find('./prism:issn', self.xmlns))
|
|
142
|
+
ent.eissn = _safe_text(e.find('./prism:eIssn', self.xmlns))
|
|
143
|
+
ent.volume = _safe_int(e.find('./prism:volume', self.xmlns))
|
|
144
|
+
ent.number = _safe_int(e.find('./prism:number', self.xmlns))
|
|
145
|
+
ent.starting_page = _safe_text(e.find('./prism:startingPage', self.xmlns))
|
|
146
|
+
ent.ending_page = _safe_text(e.find('./prism:endingPage', self.xmlns))
|
|
147
|
+
ent.pubyear = _safe_text(e.find('./pubyear', self.xmlns))
|
|
148
|
+
ent.doi = _safe_text(e.find('./prism:doi', self.xmlns))
|
|
149
|
+
ent.systemcode = _safe_int(e.find('./systemcode', self.xmlns))
|
|
150
|
+
ent.systemname = _safe_text(e.find('./systemname', self.xmlns))
|
|
151
|
+
ent.title = _safe_text(e.find('./title', self.xmlns))
|
|
152
|
+
link_el = e.find('./link', self.xmlns)
|
|
153
|
+
ent.link = (link_el.attrib.get('href', '').replace('\n', '').strip()) if link_el is not None else ''
|
|
154
|
+
ent.id = _safe_text(e.find('./id', self.xmlns))
|
|
155
|
+
updated_text = _safe_text(e.find('./updated', self.xmlns))
|
|
156
|
+
ent.updated = datetime.fromisoformat(updated_text) if updated_text else None
|
|
157
|
+
self.entries.append(ent)
|
|
158
|
+
self._finish_setup()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ListResult(Result):
|
|
162
|
+
"""List Result Class
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
encoding: Document encoding, it may always be utf-8.
|
|
166
|
+
xml_version: XML version.
|
|
167
|
+
xmlns: What namespaces does this document refer.
|
|
168
|
+
xml_lang: What language is this document written in.
|
|
169
|
+
status: Status code.
|
|
170
|
+
message: Status message.
|
|
171
|
+
title: returned title
|
|
172
|
+
link: returned link
|
|
173
|
+
id: document id, it is always same as link value.
|
|
174
|
+
servicecd: API service code, 2 is list and 3 is search.
|
|
175
|
+
updated: When was this document updated.
|
|
176
|
+
total_results: How many results did API return.
|
|
177
|
+
start_index: What index do results starts.
|
|
178
|
+
items_per_page: How many items does a page contain.
|
|
179
|
+
entries: Entries API returned.
|
|
180
|
+
"""
|
|
181
|
+
def __init__(self, result: Result = None):
|
|
182
|
+
"""Initialize ListResult class
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
result: Result object
|
|
186
|
+
"""
|
|
187
|
+
super().__init__()
|
|
188
|
+
self.encoding = result.encoding
|
|
189
|
+
self.xml_version = result.xml_version
|
|
190
|
+
self.xmlns = result.xmlns
|
|
191
|
+
self.xml_lang = result.xml_lang
|
|
192
|
+
self.status = result.status
|
|
193
|
+
self.message = result.message
|
|
194
|
+
self.title = result.title
|
|
195
|
+
self.link = result.link
|
|
196
|
+
self.id = result.id
|
|
197
|
+
self.servicecd = result.servicecd
|
|
198
|
+
self.updated = result.updated
|
|
199
|
+
self.total_results = result.total_results
|
|
200
|
+
self.start_index = result.start_index
|
|
201
|
+
self.items_per_page = result.items_per_page
|
|
202
|
+
self.entries_temp = result.entries
|
|
203
|
+
self.entries = []
|
|
204
|
+
|
|
205
|
+
for e in self.entries_temp:
|
|
206
|
+
ent = ListEntry()
|
|
207
|
+
ent.vols_title = _lang_dict(e.find('./vols_title', self.xmlns), self.xmlns)
|
|
208
|
+
ent.vols_link = _lang_dict(e.find('./vols_link', self.xmlns), self.xmlns)
|
|
209
|
+
ent.issn = _safe_text(e.find('./prism:issn', self.xmlns))
|
|
210
|
+
ent.eissn = _safe_text(e.find('./prism:eIssn', self.xmlns))
|
|
211
|
+
ent.publisher_name = _lang_dict(e.find('./publisher/name', self.xmlns), self.xmlns)
|
|
212
|
+
ent.publisher_url = _lang_dict(e.find('./publisher/url', self.xmlns), self.xmlns)
|
|
213
|
+
ent.cdjournal = _safe_text(e.find('./cdjournal', self.xmlns))
|
|
214
|
+
ent.material_title = _lang_dict(e.find('./material_title', self.xmlns), self.xmlns)
|
|
215
|
+
ent.volume = _safe_int(e.find('./prism:volume', self.xmlns))
|
|
216
|
+
ent.number = _safe_int(e.find('./prism:number', self.xmlns))
|
|
217
|
+
ent.starting_page = _safe_int(e.find('./prism:startingPage', self.xmlns))
|
|
218
|
+
ent.pubyear = _safe_text(e.find('./pubyear', self.xmlns))
|
|
219
|
+
ent.systemcode = _safe_int(e.find('./systemcode', self.xmlns))
|
|
220
|
+
ent.systemname = _safe_text(e.find('./systemname', self.xmlns))
|
|
221
|
+
ent.title = _safe_text(e.find('./title', self.xmlns))
|
|
222
|
+
link_el = e.find('./link', self.xmlns)
|
|
223
|
+
ent.link = (link_el.attrib.get('href', '').replace('\n', '').strip()) if link_el is not None else ''
|
|
224
|
+
ent.id = _safe_text(e.find('./id', self.xmlns))
|
|
225
|
+
updated_text = _safe_text(e.find('./updated', self.xmlns))
|
|
226
|
+
ent.updated = datetime.fromisoformat(updated_text) if updated_text else None
|
|
227
|
+
self.entries.append(ent)
|
|
228
|
+
self._finish_setup()
|
pyjstage/service.py
ADDED
pyjstage/status.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from . import errors
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Status(Enum):
|
|
7
|
+
"""Enum for Status
|
|
8
|
+
|
|
9
|
+
Status.OK: No Error
|
|
10
|
+
Status.NO_RESULTS: No results found
|
|
11
|
+
Status.TOO_MANY_RESULTS: Too many results found
|
|
12
|
+
Status.TOO_MANY_REQUESTS: Too many requests were occurred
|
|
13
|
+
Status.INVALID_QUERY: Query is invalid
|
|
14
|
+
Status.EMPTY_REQUIRED_FIELD: Any of required fields were not filled
|
|
15
|
+
Status.INVALID_YEAR_VALUE: Year value is invalid
|
|
16
|
+
Status.INVALID_COUNTS: Count value is invalid
|
|
17
|
+
Status.INVALID_ISSN: ISSN format is invalid.
|
|
18
|
+
Status.SYSTEM_FATAL: Fatal error has been occurred.
|
|
19
|
+
Status.INVALID_URL: URL is invalid
|
|
20
|
+
Status.LIST_NO_QUERY: Any of required queries were not sent.
|
|
21
|
+
Status.SEARCH_NO_QUERY: Any of required queries were not sent.
|
|
22
|
+
Status.LIST_UNSPECIFIED: Cannot specify result.
|
|
23
|
+
Status.SEARCH_UNSORTABLE: Cannot sort result.
|
|
24
|
+
"""
|
|
25
|
+
OK = '0'
|
|
26
|
+
NO_RESULTS = 'ERR_001'
|
|
27
|
+
TOO_MANY_RESULTS = 'WARN_002'
|
|
28
|
+
TOO_MANY_REQUESTS = 'ERR_003'
|
|
29
|
+
INVALID_QUERY = 'ERR_004'
|
|
30
|
+
EMPTY_REQUIRED_FIELD = 'ERR_005'
|
|
31
|
+
INVALID_YEAR_VALUE = 'ERR_006'
|
|
32
|
+
INVALID_COUNTS = 'ERR_007'
|
|
33
|
+
INVALID_ISSN = 'ERR_008'
|
|
34
|
+
SYSTEM_FATAL = 'SYS_ERR_009'
|
|
35
|
+
INVALID_URL = 'ERR_010'
|
|
36
|
+
LIST_NO_QUERY = 'ERR_011'
|
|
37
|
+
SEARCH_NO_QUERY = 'ERR_012'
|
|
38
|
+
LIST_UNSPECIFIED = 'ERR_013'
|
|
39
|
+
SEARCH_UNSORTABLE = 'ERR_014'
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def divide(status: str, message: Optional[str]):
|
|
43
|
+
"""Divide status and raise Error or Warning if necessary.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
status: status code J-STAGE API returned
|
|
47
|
+
message: message J-STAGE API returned
|
|
48
|
+
Raises:
|
|
49
|
+
JstageError: error depending on j-stage api
|
|
50
|
+
JstageWarning: warning depending on j-stage api
|
|
51
|
+
"""
|
|
52
|
+
if status == Status.OK.value:
|
|
53
|
+
pass
|
|
54
|
+
elif status == Status.NO_RESULTS.value:
|
|
55
|
+
raise errors.NoResultsError(message)
|
|
56
|
+
elif status == Status.TOO_MANY_RESULTS.value:
|
|
57
|
+
pass # warning, not an error — API still returns valid data
|
|
58
|
+
elif status == Status.TOO_MANY_REQUESTS.value:
|
|
59
|
+
raise errors.TooManyRequestsError(message)
|
|
60
|
+
elif status == Status.INVALID_QUERY.value:
|
|
61
|
+
raise errors.InvalidQueryError(f'{message} invalid.')
|
|
62
|
+
elif status == Status.EMPTY_REQUIRED_FIELD.value:
|
|
63
|
+
raise errors.EmptyRequiredFieldError(f'{message} required.')
|
|
64
|
+
elif status == Status.INVALID_YEAR_VALUE.value:
|
|
65
|
+
raise errors.InvalidYearValueError(f'{message} invalid.')
|
|
66
|
+
elif status == Status.INVALID_COUNTS.value:
|
|
67
|
+
raise errors.InvalidCountsError(f'{message} invalid')
|
|
68
|
+
elif status == Status.INVALID_ISSN.value:
|
|
69
|
+
raise errors.InvalidIssnError(f'{message} invalid')
|
|
70
|
+
elif status == Status.SYSTEM_FATAL.value:
|
|
71
|
+
raise errors.SystemFatalError(message)
|
|
72
|
+
elif status == Status.INVALID_URL.value:
|
|
73
|
+
raise errors.InvalidUrlError(message)
|
|
74
|
+
elif status == Status.LIST_NO_QUERY.value:
|
|
75
|
+
raise errors.ListNoQueryError(f'{message} required')
|
|
76
|
+
elif status == Status.SEARCH_NO_QUERY.value:
|
|
77
|
+
raise errors.SearchNoQueryError(f'{message}: any parameter is required')
|
|
78
|
+
elif status == Status.LIST_UNSPECIFIED.value:
|
|
79
|
+
raise errors.ListUnspecifiedError(message)
|
|
80
|
+
elif status == Status.SEARCH_UNSORTABLE.value:
|
|
81
|
+
raise errors.SearchUnsortableError(message)
|
|
82
|
+
else:
|
|
83
|
+
raise Exception(message)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyjstage2
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: J-STAGE API wrapper for Python - Python 3.12 Compatible
|
|
5
|
+
Home-page: https://github.com/lanshi17/pyjstage
|
|
6
|
+
Author: lanshi17
|
|
7
|
+
Author-email: lanshi17@users.noreply.github.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Source, https://github.com/lanshi17/pyjstage
|
|
10
|
+
Project-URL: Original, https://github.com/matsurih/pyjstage
|
|
11
|
+
Keywords: jstage api wrapper japan science technology academic papers
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: certifi>=2023.0.0
|
|
22
|
+
Requires-Dist: chardet>=5.0.0
|
|
23
|
+
Requires-Dist: idna>=3.0
|
|
24
|
+
Requires-Dist: lxml>=5.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Requires-Dist: urllib3>=2.0.0
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: keywords
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: project-url
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
# pyjstage2
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
## Overview
|
|
48
|
+
|
|
49
|
+
[J-STAGE WebAPI](https://www.jstage.jst.go.jp/static/pages/OtherJstageServices/TAB2/-char/ja) Wrapper for Python 3.
|
|
50
|
+
|
|
51
|
+
- J-STAGE is an electronic journal platform for science and technology information in Japan, developed and managed by the Japan Science and Technology Agency (JST).
|
|
52
|
+
- This package is a **Python 3.12 compatible fork** of [pyjstage](https://pypi.org/project/pyjstage/).
|
|
53
|
+
|
|
54
|
+
## Acknowledgments
|
|
55
|
+
|
|
56
|
+
This project is modified based on [matsurih/pyjstage](https://github.com/matsurih/pyjstage) (v0.0.2).
|
|
57
|
+
|
|
58
|
+
Special thanks to the original author [@matsurih](https://github.com/matsurih) for creating this useful J-STAGE API wrapper.
|
|
59
|
+
|
|
60
|
+
## What's Changed
|
|
61
|
+
|
|
62
|
+
| Package | Original | Forked |
|
|
63
|
+
|---------|----------|--------|
|
|
64
|
+
| lxml | 4.4.2 | >=5.0.0 |
|
|
65
|
+
| requests | 2.22.0 | >=2.31.0 |
|
|
66
|
+
| urllib3 | 1.25.7 | >=2.0.0 |
|
|
67
|
+
| certifi | 2019.11.28 | >=2023.0.0 |
|
|
68
|
+
|
|
69
|
+
- Fixed absolute imports to relative imports
|
|
70
|
+
- Fixed private method name mangling (`__finish_setup` → `_finish_setup`)
|
|
71
|
+
- Added `__init__.py` export for cleaner imports
|
|
72
|
+
|
|
73
|
+
## Prerequisites
|
|
74
|
+
|
|
75
|
+
- Python >= 3.12
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```shell
|
|
80
|
+
$ pip install pyjstage2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Basic Usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pyjstage.pyjstage import Pyjstage
|
|
89
|
+
|
|
90
|
+
jstage = Pyjstage()
|
|
91
|
+
|
|
92
|
+
# Search by ISSN
|
|
93
|
+
ret_search = jstage.search(issn='2186-6619', count=3)
|
|
94
|
+
|
|
95
|
+
# List articles in a journal
|
|
96
|
+
ret_list = jstage.list(issn='2186-6619')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Accessing Results
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
for entry in ret_search.entries:
|
|
103
|
+
print(f"Title: {entry.title}")
|
|
104
|
+
print(f"Author: {entry.author.get('ja', '')}")
|
|
105
|
+
print(f"Journal: {entry.material_title.get('ja', '')}")
|
|
106
|
+
print(f"Year: {entry.pubyear}")
|
|
107
|
+
print(f"DOI: {entry.doi}")
|
|
108
|
+
print(f"Link: {entry.link}")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Search Parameters
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
jstage.search(
|
|
115
|
+
pubyearfrom=2020, # Search from year
|
|
116
|
+
pubyearto=2024, # Search to year
|
|
117
|
+
material='journal', # Journal name contains
|
|
118
|
+
article='title', # Article title contains
|
|
119
|
+
author='name', # Author name contains
|
|
120
|
+
keyword='AI', # Keyword contains
|
|
121
|
+
issn='2186-6619', # ISSN
|
|
122
|
+
count=10 # Number of results (max 1000)
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Important Notes
|
|
127
|
+
|
|
128
|
+
### What J-STAGE API Provides
|
|
129
|
+
|
|
130
|
+
- ✅ **Literature search** - Search by ISSN, keyword, author, year, etc.
|
|
131
|
+
- ✅ **Metadata** - Title, author, journal, year, DOI, page numbers
|
|
132
|
+
- ✅ **Article links** - Links to J-STAGE article pages
|
|
133
|
+
|
|
134
|
+
### What J-STAGE API Does NOT Provide
|
|
135
|
+
|
|
136
|
+
- ❌ **Full-text PDF download** - The API does not provide direct PDF downloads
|
|
137
|
+
- ❌ **Full-text content** - The API returns metadata, not full article text
|
|
138
|
+
|
|
139
|
+
To access full-text content, use the `link` or `doi` fields to visit the publisher's website.
|
|
140
|
+
|
|
141
|
+
## Changelog
|
|
142
|
+
|
|
143
|
+
### v0.1.2 (2025-06-16)
|
|
144
|
+
|
|
145
|
+
- **Fix**: Null-safe XML parsing in `SearchResult` and `ListResult` — handles missing/empty XML elements without raising `AttributeError`
|
|
146
|
+
- **Fix**: `WARN_002` (too many results) is now treated as a warning instead of raising an error, since the API still returns valid data
|
|
147
|
+
|
|
148
|
+
### v0.1.1 (2025-05-18)
|
|
149
|
+
|
|
150
|
+
- First PyPI release as `pyjstage2`
|
|
151
|
+
- Python 3.12 compatibility
|
|
152
|
+
- Updated dependencies (lxml >=5.0, requests >=2.31, urllib3 >=2.0)
|
|
153
|
+
|
|
154
|
+
## Source Code
|
|
155
|
+
|
|
156
|
+
https://github.com/lanshi17/pyjstage
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT License - see [LICENSE](https://github.com/lanshi17/pyjstage/blob/master/LICENSE) file.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pyjstage/__init__.py,sha256=_voPoelPe0YSf28fEqdfvw_70a66N4lNDXrOP3Tkz2M,31
|
|
2
|
+
pyjstage/entry.py,sha256=qWcBP9_RdW6DtDE6wYHOBJ23Hh2VXiXn6DKXIsIhuZo,1963
|
|
3
|
+
pyjstage/errors.py,sha256=b-GLUBz0DNpxYu8PwfmIAibNJh5akrekMvD5EvkntIQ,1159
|
|
4
|
+
pyjstage/order.py,sha256=rUo9-voGrNSQwsG-VkTMG4wjxRBjVEGiH_FuUpfaTKk,392
|
|
5
|
+
pyjstage/parser.py,sha256=owE829b1d_RWlSK8yu-kv8SIHc4nwM-TuWCkDbRBoxU,2464
|
|
6
|
+
pyjstage/pyjstage.py,sha256=YMsb_UpAGbc7SRzgn5ZV1Hykd9aNskmVfTuXqhr65Io,5613
|
|
7
|
+
pyjstage/result.py,sha256=0e_NUxSwRx9NHqO9SYuVAfko2j-T1gHtVNdO-5DudSg,9554
|
|
8
|
+
pyjstage/service.py,sha256=S_Hg2hl6ac5X65UKhKyVByvE2UaoH_t_FdQuwGc2vL4,189
|
|
9
|
+
pyjstage/status.py,sha256=3RVlTxx50UbKNpRKiXZHn05HwdDOeE_7QQ4gWLldoRQ,3481
|
|
10
|
+
pyjstage2-0.1.2.dist-info/licenses/LICENSE,sha256=dqxSbI5Cx4e5NDbJ5PM-HCfO3UqN4DaoHD5xjMndkYU,1065
|
|
11
|
+
pyjstage2-0.1.2.dist-info/METADATA,sha256=vBZMzK6IBfmDm1wxwfI5-_qgfZVYTdR-pd7vurTj4fg,4773
|
|
12
|
+
pyjstage2-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
pyjstage2-0.1.2.dist-info/top_level.txt,sha256=FbjfZZ2LZ-Y5Gm9sNRTUmxImh4QEhNblt_C0WESxgS8,9
|
|
14
|
+
pyjstage2-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 matsurih
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyjstage
|