pub-analyzer 0.2.0__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of pub-analyzer might be problematic. Click here for more details.
- pub_analyzer/css/body.tcss +48 -35
- pub_analyzer/css/buttons.tcss +0 -4
- pub_analyzer/css/main.tcss +1 -1
- pub_analyzer/css/summary.tcss +75 -0
- pub_analyzer/internal/identifier.py +26 -0
- pub_analyzer/internal/report.py +73 -31
- pub_analyzer/internal/templates/author/{author_resume.typ → author_summary.typ} +4 -3
- pub_analyzer/internal/templates/author/report.typ +4 -3
- pub_analyzer/internal/templates/author/sources.typ +7 -5
- pub_analyzer/internal/templates/author/works.typ +12 -12
- pub_analyzer/internal/templates/author/works_extended.typ +4 -4
- pub_analyzer/main.py +3 -2
- pub_analyzer/models/author.py +9 -25
- pub_analyzer/models/concept.py +19 -0
- pub_analyzer/models/institution.py +11 -1
- pub_analyzer/models/report.py +14 -14
- pub_analyzer/models/source.py +59 -3
- pub_analyzer/models/topic.py +59 -0
- pub_analyzer/models/work.py +23 -0
- pub_analyzer/widgets/author/cards.py +6 -5
- pub_analyzer/widgets/author/core.py +11 -10
- pub_analyzer/widgets/common/summary.py +7 -0
- pub_analyzer/widgets/institution/core.py +10 -9
- pub_analyzer/widgets/report/cards.py +10 -11
- pub_analyzer/widgets/report/concept.py +47 -0
- pub_analyzer/widgets/report/core.py +14 -0
- pub_analyzer/widgets/report/grants.py +46 -0
- pub_analyzer/widgets/report/source.py +11 -4
- pub_analyzer/widgets/report/topic.py +55 -0
- pub_analyzer/widgets/report/work.py +45 -9
- pub_analyzer/widgets/search/results.py +8 -8
- pub_analyzer/widgets/sidebar.py +11 -2
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/METADATA +8 -7
- pub_analyzer-0.3.0.dist-info/RECORD +69 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/WHEEL +1 -1
- pub_analyzer/css/author.tcss +0 -82
- pub_analyzer/css/institution.tcss +0 -82
- pub_analyzer-0.2.0.dist-info/RECORD +0 -64
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
pub_analyzer/css/body.tcss
CHANGED
|
@@ -14,38 +14,51 @@ SideBar {
|
|
|
14
14
|
|
|
15
15
|
background: $bg-secondary-color;
|
|
16
16
|
color: $text-primary-color;
|
|
17
|
-
transition: width 250ms in_out_cubic;
|
|
18
|
-
}
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
18
|
+
Button {
|
|
19
|
+
text-align: start;
|
|
20
|
+
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
#sidebar-title {
|
|
23
|
+
width: 100%;
|
|
24
|
+
content-align: left middle;
|
|
25
|
+
text-style: bold;
|
|
26
|
+
border-bottom: heavy $text-primary-color;
|
|
27
|
+
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
29
|
+
.sidebar-options-column {
|
|
30
|
+
margin: 1;
|
|
31
|
+
padding: 1 1;
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
.sidebar-buttons-column {
|
|
35
|
+
height: 1fr;
|
|
36
|
+
|
|
37
|
+
.sidebar-option {
|
|
38
|
+
content-align: left middle;
|
|
39
|
+
margin: 1 0;
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
#module-version-label {
|
|
45
|
+
color: $text-primary-color 25%;
|
|
46
|
+
text-align: center;
|
|
47
|
+
width: 100%;
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
SideBar
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
.-dark-mode SideBar {
|
|
52
|
+
background: $bg-secondary-color-darken;
|
|
53
|
+
color: $text-primary-color-darken;
|
|
54
|
+
|
|
55
|
+
#sidebar-title {
|
|
56
|
+
border-bottom: heavy $text-primary-color-darken;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#module-version-label {
|
|
60
|
+
color: $text-primary-color-darken 25%;
|
|
61
|
+
}
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
/* Main Content CSS */
|
|
@@ -55,20 +68,20 @@ MainContent {
|
|
|
55
68
|
|
|
56
69
|
padding: 2 1 0 1;
|
|
57
70
|
height: 100%;
|
|
71
|
+
|
|
72
|
+
.title {
|
|
73
|
+
content-align: left middle;
|
|
74
|
+
text-style: bold;
|
|
75
|
+
border-bottom: heavy $text-primary-color;
|
|
76
|
+
width: 100%
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
.-dark-mode MainContent {
|
|
61
81
|
background: $bg-main-color-darken;
|
|
62
82
|
color: $text-primary-color-darken;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
MainContent .title {
|
|
66
|
-
content-align: left middle;
|
|
67
|
-
text-style: bold;
|
|
68
|
-
border-bottom: heavy $text-primary-color;
|
|
69
|
-
width: 100%
|
|
70
|
-
}
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
.title {
|
|
85
|
+
border-bottom: heavy $text-primary-color-darken;
|
|
86
|
+
}
|
|
74
87
|
}
|
pub_analyzer/css/buttons.tcss
CHANGED
pub_analyzer/css/main.tcss
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* COLORS */
|
|
2
|
+
$bg-main-color: white;
|
|
3
|
+
$bg-secondary-color: #e5e7eb;
|
|
4
|
+
$bg-secondary-color-accent: #d1d5db;
|
|
5
|
+
$text-primary-color: black;
|
|
6
|
+
|
|
7
|
+
$bg-main-color-darken: #1e293b;
|
|
8
|
+
$bg-secondary-color-darken: #0f172a;
|
|
9
|
+
$text-primary-color-darken: black;
|
|
10
|
+
|
|
11
|
+
SummaryWidget {
|
|
12
|
+
height: 1fr;
|
|
13
|
+
margin: 1 2;
|
|
14
|
+
|
|
15
|
+
/* Titles */
|
|
16
|
+
.block-title {
|
|
17
|
+
text-align: center;
|
|
18
|
+
width: 100%;
|
|
19
|
+
border-bottom: solid $text-primary-color;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Block Container */
|
|
23
|
+
.block-container {
|
|
24
|
+
padding: 1;
|
|
25
|
+
height: auto;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Cards */
|
|
29
|
+
.cards-container {
|
|
30
|
+
height: auto;
|
|
31
|
+
margin: 1 0 0 0 ;
|
|
32
|
+
|
|
33
|
+
layout: grid;
|
|
34
|
+
grid-size: 3 1;
|
|
35
|
+
grid-rows: 14;
|
|
36
|
+
grid-columns: 1fr;
|
|
37
|
+
grid-gutter: 1 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Info Container */
|
|
41
|
+
.info-container {
|
|
42
|
+
height: auto;
|
|
43
|
+
|
|
44
|
+
Label {
|
|
45
|
+
text-align: center;
|
|
46
|
+
width: 1fr;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Table */
|
|
51
|
+
.table-container {
|
|
52
|
+
height: auto;
|
|
53
|
+
margin: 1 0 0 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Filter Container */
|
|
57
|
+
.filter-collapsible {
|
|
58
|
+
margin-top: 1;
|
|
59
|
+
|
|
60
|
+
DateRangeFilter {
|
|
61
|
+
margin-top: 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Buttons */
|
|
66
|
+
.button-container {
|
|
67
|
+
align: center middle;
|
|
68
|
+
height: 5;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.-dark-mode SummaryWidget {
|
|
73
|
+
color: $text-primary-color-darken;
|
|
74
|
+
background: $bg-secondary-color;
|
|
75
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from pub_analyzer.models.author import Author, AuthorOpenAlexKey, AuthorResult, DehydratedAuthor
|
|
4
4
|
from pub_analyzer.models.institution import DehydratedInstitution, Institution, InstitutionOpenAlexKey, InstitutionResult
|
|
5
|
+
from pub_analyzer.models.source import DehydratedSource, Source
|
|
5
6
|
from pub_analyzer.models.work import Work
|
|
6
7
|
|
|
7
8
|
|
|
@@ -78,3 +79,28 @@ def get_work_id(work: Work) -> str:
|
|
|
78
79
|
return work.id.path.rpartition("/")[2]
|
|
79
80
|
else:
|
|
80
81
|
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_source_id(source: DehydratedSource | Source) -> str:
|
|
85
|
+
"""Extract OpenAlex ID from Source Model.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
source: Source model instance.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Source OpenAlex ID.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
```python
|
|
95
|
+
from pub_analyzer.internal.identifier import get_source_id
|
|
96
|
+
from pub_analyzer.models.source import Source
|
|
97
|
+
|
|
98
|
+
source = Source(id="https://openalex.org/S000000000", **kwargs)
|
|
99
|
+
print(get_source_id(source))
|
|
100
|
+
# 'S000000000'
|
|
101
|
+
```
|
|
102
|
+
"""
|
|
103
|
+
if source.id.path:
|
|
104
|
+
return source.id.path.rpartition("/")[2]
|
|
105
|
+
else:
|
|
106
|
+
return ""
|
pub_analyzer/internal/report.py
CHANGED
|
@@ -13,14 +13,15 @@ from pub_analyzer.models.institution import DehydratedInstitution, Institution,
|
|
|
13
13
|
from pub_analyzer.models.report import (
|
|
14
14
|
AuthorReport,
|
|
15
15
|
CitationReport,
|
|
16
|
-
|
|
16
|
+
CitationSummary,
|
|
17
17
|
CitationType,
|
|
18
18
|
InstitutionReport,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
OpenAccessSummary,
|
|
20
|
+
SourcesSummary,
|
|
21
21
|
WorkReport,
|
|
22
22
|
WorkTypeCounter,
|
|
23
23
|
)
|
|
24
|
+
from pub_analyzer.models.source import DehydratedSource, Source
|
|
24
25
|
from pub_analyzer.models.work import Authorship, Work
|
|
25
26
|
|
|
26
27
|
FromDate = NewType("FromDate", datetime.datetime)
|
|
@@ -171,6 +172,25 @@ async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
|
|
|
171
172
|
return TypeAdapter(list[Work]).validate_python(works_data)
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
async def _get_source(client: httpx.AsyncClient, url: str) -> Source:
|
|
176
|
+
"""Get source given a URL.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
client: HTTPX asynchronous client to be used to make the requests.
|
|
180
|
+
url: URL of works with all filters.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Source Model.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
|
|
187
|
+
"""
|
|
188
|
+
response = await client.get(url=url)
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
|
|
191
|
+
return Source(**response.json())
|
|
192
|
+
|
|
193
|
+
|
|
174
194
|
async def make_author_report(
|
|
175
195
|
author: Author,
|
|
176
196
|
extra_profiles: list[Author | AuthorResult | DehydratedAuthor] | None = None,
|
|
@@ -216,10 +236,10 @@ async def make_author_report(
|
|
|
216
236
|
|
|
217
237
|
# Report fields.
|
|
218
238
|
works: list[WorkReport] = []
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
report_citation_summary = CitationSummary()
|
|
240
|
+
open_access_summary = OpenAccessSummary()
|
|
221
241
|
works_type_counter: list[WorkTypeCounter] = []
|
|
222
|
-
|
|
242
|
+
dehydrated_sources: list[DehydratedSource] = []
|
|
223
243
|
|
|
224
244
|
# Getting all works that have cited the author.
|
|
225
245
|
for author_work in author_works:
|
|
@@ -230,7 +250,7 @@ async def make_author_report(
|
|
|
230
250
|
)
|
|
231
251
|
|
|
232
252
|
# Adding the type of OpenAccess in the counter.
|
|
233
|
-
|
|
253
|
+
open_access_summary.add_oa_type(author_work.open_access.oa_status)
|
|
234
254
|
|
|
235
255
|
# Adding the work type to works type counter.
|
|
236
256
|
work_type = next((work_type for work_type in works_type_counter if work_type.type_name == author_work.type), None)
|
|
@@ -241,31 +261,42 @@ async def make_author_report(
|
|
|
241
261
|
|
|
242
262
|
# Add Sources to global list.
|
|
243
263
|
for location in author_work.locations:
|
|
244
|
-
if location.source and not any(source.
|
|
245
|
-
|
|
264
|
+
if location.source and not any(source.id == location.source.id for source in dehydrated_sources):
|
|
265
|
+
dehydrated_sources.append(location.source)
|
|
246
266
|
|
|
247
267
|
cited_by_works = await _get_works(client, cited_by_api_url)
|
|
248
268
|
cited_by: list[CitationReport] = []
|
|
249
|
-
|
|
269
|
+
work_citation_summary = CitationSummary()
|
|
250
270
|
for cited_by_work in cited_by_works:
|
|
251
271
|
cited_authors = _get_authors_list(authorships=cited_by_work.authorships)
|
|
252
272
|
citation_type = _get_citation_type(work_authors, cited_authors)
|
|
253
273
|
|
|
254
274
|
# Adding the type of cites in the counters.
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
report_citation_summary.add_cite_type(citation_type)
|
|
276
|
+
work_citation_summary.add_cite_type(citation_type)
|
|
257
277
|
|
|
258
278
|
cited_by.append(CitationReport(work=cited_by_work, citation_type=citation_type))
|
|
259
279
|
|
|
260
|
-
works.append(WorkReport(work=author_work, cited_by=cited_by,
|
|
280
|
+
works.append(WorkReport(work=author_work, cited_by=cited_by, citation_summary=work_citation_summary))
|
|
281
|
+
|
|
282
|
+
# Get sources full info.
|
|
283
|
+
sources: list[Source] = []
|
|
284
|
+
for dehydrated_source in dehydrated_sources:
|
|
285
|
+
source_id = identifier.get_source_id(dehydrated_source)
|
|
286
|
+
source_url = f"https://api.openalex.org/sources/{source_id}"
|
|
287
|
+
sources.append(await _get_source(client, source_url))
|
|
288
|
+
|
|
289
|
+
# Sort sources by h_index
|
|
290
|
+
sources_sorted = sorted(sources, key=lambda source: source.summary_stats.two_yr_mean_citedness, reverse=True)
|
|
291
|
+
sources_summary = SourcesSummary(sources=sources_sorted)
|
|
261
292
|
|
|
262
293
|
return AuthorReport(
|
|
263
294
|
author=author,
|
|
264
295
|
works=works,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
296
|
+
citation_summary=report_citation_summary,
|
|
297
|
+
open_access_summary=open_access_summary,
|
|
298
|
+
works_type_summary=works_type_counter,
|
|
299
|
+
sources_summary=sources_summary,
|
|
269
300
|
)
|
|
270
301
|
|
|
271
302
|
|
|
@@ -312,10 +343,10 @@ async def make_institution_report(
|
|
|
312
343
|
|
|
313
344
|
# Report fields.
|
|
314
345
|
works: list[WorkReport] = []
|
|
315
|
-
|
|
316
|
-
|
|
346
|
+
report_citation_summary = CitationSummary()
|
|
347
|
+
open_access_summary = OpenAccessSummary()
|
|
317
348
|
works_type_counter: list[WorkTypeCounter] = []
|
|
318
|
-
|
|
349
|
+
dehydrated_sources: list[DehydratedSource] = []
|
|
319
350
|
|
|
320
351
|
# Getting all works that have cited a work.
|
|
321
352
|
for institution_work in institution_works:
|
|
@@ -326,7 +357,7 @@ async def make_institution_report(
|
|
|
326
357
|
)
|
|
327
358
|
|
|
328
359
|
# Adding the type of OpenAccess in the counter.
|
|
329
|
-
|
|
360
|
+
open_access_summary.add_oa_type(institution_work.open_access.oa_status)
|
|
330
361
|
|
|
331
362
|
# Adding the work type to works type counter.
|
|
332
363
|
work_type = next((work_type for work_type in works_type_counter if work_type.type_name == institution_work.type), None)
|
|
@@ -337,29 +368,40 @@ async def make_institution_report(
|
|
|
337
368
|
|
|
338
369
|
# Add Sources to global list.
|
|
339
370
|
for location in institution_work.locations:
|
|
340
|
-
if location.source and not any(source.
|
|
341
|
-
|
|
371
|
+
if location.source and not any(source.id == location.source.id for source in dehydrated_sources):
|
|
372
|
+
dehydrated_sources.append(location.source)
|
|
342
373
|
|
|
343
374
|
cited_by_works = await _get_works(client, cited_by_api_url)
|
|
344
375
|
cited_by: list[CitationReport] = []
|
|
345
|
-
|
|
376
|
+
work_citation_summary = CitationSummary()
|
|
346
377
|
for cited_by_work in cited_by_works:
|
|
347
378
|
cited_authors = _get_authors_list(authorships=cited_by_work.authorships)
|
|
348
379
|
citation_type = _get_citation_type(work_authors, cited_authors)
|
|
349
380
|
|
|
350
381
|
# Adding the type of cites in the counters.
|
|
351
|
-
|
|
352
|
-
|
|
382
|
+
report_citation_summary.add_cite_type(citation_type)
|
|
383
|
+
work_citation_summary.add_cite_type(citation_type)
|
|
353
384
|
|
|
354
385
|
cited_by.append(CitationReport(work=cited_by_work, citation_type=citation_type))
|
|
355
386
|
|
|
356
|
-
works.append(WorkReport(work=institution_work, cited_by=cited_by,
|
|
387
|
+
works.append(WorkReport(work=institution_work, cited_by=cited_by, citation_summary=work_citation_summary))
|
|
388
|
+
|
|
389
|
+
# Get sources full info.
|
|
390
|
+
sources: list[Source] = []
|
|
391
|
+
for dehydrated_source in dehydrated_sources:
|
|
392
|
+
source_id = identifier.get_source_id(dehydrated_source)
|
|
393
|
+
source_url = f"https://api.openalex.org/sources/{source_id}"
|
|
394
|
+
sources.append(await _get_source(client, source_url))
|
|
395
|
+
|
|
396
|
+
# Sort sources by h_index
|
|
397
|
+
sources_sorted = sorted(sources, key=lambda source: source.summary_stats.two_yr_mean_citedness, reverse=True)
|
|
398
|
+
sources_summary = SourcesSummary(sources=sources_sorted)
|
|
357
399
|
|
|
358
400
|
return InstitutionReport(
|
|
359
401
|
institution=institution,
|
|
360
402
|
works=works,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
403
|
+
citation_summary=report_citation_summary,
|
|
404
|
+
open_access_summary=open_access_summary,
|
|
405
|
+
works_type_summary=works_type_counter,
|
|
406
|
+
sources_summary=sources_summary,
|
|
365
407
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Author
|
|
1
|
+
// Author Summary
|
|
2
2
|
= Author.
|
|
3
3
|
|
|
4
4
|
// Cards
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
[#align(center)[#text(size: 12pt)[Last institution:]]],
|
|
23
23
|
|
|
24
24
|
// Card content
|
|
25
|
-
{% if report.author.
|
|
26
|
-
|
|
25
|
+
{% if report.author.last_known_institutions%}
|
|
26
|
+
{% set last_known_institution = report.author.last_known_institutions[0] %}
|
|
27
|
+
[#align(left)[#text(size: 10pt)[- *Name:* {{ last_known_institution.display_name }}]]],
|
|
27
28
|
[#align(left)[#text(size: 10pt)[- *Country:* MX]]],
|
|
28
29
|
[#align(left)[#text(size: 10pt)[- *Type:* education]]],
|
|
29
30
|
{% endif %}
|
|
@@ -39,12 +39,13 @@
|
|
|
39
39
|
columns: (1fr),
|
|
40
40
|
row-gutter: 11pt,
|
|
41
41
|
[#align(center, text(size: 17pt, weight: "bold")[{{ report.author.display_name }}])],
|
|
42
|
-
{% if report.author.
|
|
43
|
-
|
|
42
|
+
{% if report.author.last_known_institutions %}
|
|
43
|
+
{% set last_known_institution = report.author.last_known_institutions[0] %}
|
|
44
|
+
[#align(center, text(size: 15pt, weight: "thin")[{{ last_known_institution.display_name }}])],
|
|
44
45
|
{% endif %}
|
|
45
46
|
)
|
|
46
47
|
|
|
47
|
-
{% include '
|
|
48
|
+
{% include 'author_summary.typ' %}
|
|
48
49
|
|
|
49
50
|
{% include 'works.typ' %}
|
|
50
51
|
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
= Sources.
|
|
3
3
|
|
|
4
4
|
#table(
|
|
5
|
-
columns: (auto, 3fr, 2fr, auto, auto, auto),
|
|
5
|
+
columns: (auto, 3fr, 2fr, auto, auto, auto, auto, auto),
|
|
6
6
|
inset: 8pt,
|
|
7
7
|
align: horizon,
|
|
8
8
|
// Headers
|
|
9
|
-
[], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*
|
|
9
|
+
[], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Impact factor*], [*h-index*], [*Is OA*],
|
|
10
10
|
|
|
11
11
|
// Content
|
|
12
|
-
{% for source in report.
|
|
13
|
-
[{{ loop.index }}],
|
|
14
|
-
[#underline([#link("{{ source.
|
|
12
|
+
{% for source in report.sources_summary.sources %}
|
|
13
|
+
[#underline[3.{{ loop.index }}. #label("source_{{ source.id.path.rpartition("/")[2] }}")]],
|
|
14
|
+
[#underline([#link("{{ source.homepage_url }}")[#"{{ source.display_name }}"]])],
|
|
15
15
|
[{{ source.host_organization_name or "-" }}],
|
|
16
16
|
[{{source.type }}],
|
|
17
17
|
[{{ source.issn_l or "-" }}],
|
|
18
|
+
[{{ source.summary_stats.two_yr_mean_citedness|round(3) }}],
|
|
19
|
+
[{{ source.summary_stats.h_index }}],
|
|
18
20
|
[{% if source.is_oa %}#text(rgb("909d63"))[True]{% else %}#text(rgb("bc5653"))[False]{% endif %}],
|
|
19
21
|
{% endfor %}
|
|
20
22
|
)
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
[
|
|
10
10
|
#align(center)[_Citation metrics_]
|
|
11
11
|
#parbreak()
|
|
12
|
-
- *Count:* {{ report.
|
|
13
|
-
- *Type A:* {{ report.
|
|
14
|
-
- *Type B:* {{ report.
|
|
12
|
+
- *Count:* {{ report.citation_summary.type_a_count + report.citation_summary.type_b_count }}
|
|
13
|
+
- *Type A:* {{ report.citation_summary.type_a_count }}
|
|
14
|
+
- *Type B:* {{ report.citation_summary.type_b_count }}
|
|
15
15
|
],
|
|
16
16
|
[
|
|
17
17
|
#align(center)[_Work Type_]
|
|
18
18
|
#parbreak()
|
|
19
|
-
{% for work_type in report.
|
|
19
|
+
{% for work_type in report.works_type_summary %}
|
|
20
20
|
- *{{ work_type.type_name }}:* {{ work_type.count }}
|
|
21
21
|
{% endfor %}
|
|
22
22
|
],
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
columns: (1fr, 1fr),
|
|
28
28
|
column-gutter: 15pt,
|
|
29
29
|
[
|
|
30
|
-
- *gold:* {{report.
|
|
31
|
-
- *green:* {{report.
|
|
32
|
-
- *hybrid:* {{report.
|
|
30
|
+
- *gold:* {{report.open_access_summary.gold}}
|
|
31
|
+
- *green:* {{report.open_access_summary.green}}
|
|
32
|
+
- *hybrid:* {{report.open_access_summary.hybrid}}
|
|
33
33
|
],
|
|
34
34
|
[
|
|
35
|
-
- *bronze:* {{report.
|
|
36
|
-
- *closed:* {{report.
|
|
35
|
+
- *bronze:* {{report.open_access_summary.bronze}}
|
|
36
|
+
- *closed:* {{report.open_access_summary.closed}}
|
|
37
37
|
],
|
|
38
38
|
)
|
|
39
39
|
],
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
[{{ work.work.type }}],
|
|
57
57
|
[{% if work.work.ids.doi %}#underline([#link("{{ work.work.ids.doi }}")[DOI]]){% else %}-{% endif %}],
|
|
58
58
|
[{{ work.work.publication_date }}],
|
|
59
|
-
[{{ work.
|
|
60
|
-
[{{ work.
|
|
61
|
-
[{{ work.
|
|
59
|
+
[{{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}],
|
|
60
|
+
[{{ work.citation_summary.type_a_count }}],
|
|
61
|
+
[{{ work.citation_summary.type_b_count }}],
|
|
62
62
|
[{{ work.work.open_access.oa_status.value }}],
|
|
63
63
|
{% endfor %}
|
|
64
64
|
)
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
[
|
|
40
40
|
#align(center)[_Citation_]
|
|
41
41
|
#parbreak()
|
|
42
|
-
- *Count:* {{ work.
|
|
43
|
-
- *Type A:* {{ work.
|
|
44
|
-
- *Type B:* {{ work.
|
|
42
|
+
- *Count:* {{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}
|
|
43
|
+
- *Type A:* {{ work.citation_summary.type_a_count }}
|
|
44
|
+
- *Type B:* {{ work.citation_summary.type_b_count }}
|
|
45
45
|
],
|
|
46
46
|
)
|
|
47
47
|
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
// Content
|
|
84
84
|
{% for location in work.work.locations %}
|
|
85
85
|
{% if location.source %}
|
|
86
|
-
[{{ loop.index }}],
|
|
86
|
+
[#underline([#link(label("source_{{ location.source.id.path.rpartition("/")[2] }}"))[{{ loop.index }}]])],
|
|
87
87
|
[#underline([#link("{{ location.landing_page_url }}")[#"{{ location.source.display_name }}"]])],
|
|
88
88
|
[{{ location.source.host_organization_name or "-" }}],
|
|
89
89
|
[{{ location.source.type }}],
|
pub_analyzer/main.py
CHANGED
|
@@ -18,16 +18,17 @@ from pub_analyzer.widgets.sidebar import SideBar
|
|
|
18
18
|
class PubAnalyzerApp(App[DOMNode]):
|
|
19
19
|
"""Pub Analyzer App entrypoint."""
|
|
20
20
|
|
|
21
|
+
TITLE = "Pub Analyzer"
|
|
22
|
+
|
|
21
23
|
CSS_PATH: ClassVar[CSSPathType | None] = [
|
|
22
|
-
"css/author.tcss",
|
|
23
24
|
"css/body.tcss",
|
|
24
25
|
"css/buttons.tcss",
|
|
25
26
|
"css/checkbox.tcss",
|
|
26
27
|
"css/collapsible.tcss",
|
|
27
28
|
"css/datatable.tcss",
|
|
28
|
-
"css/institution.tcss",
|
|
29
29
|
"css/main.tcss",
|
|
30
30
|
"css/report.tcss",
|
|
31
|
+
"css/summary.tcss",
|
|
31
32
|
"css/search.tcss",
|
|
32
33
|
"css/tabs.tcss",
|
|
33
34
|
"css/tree.tcss",
|
pub_analyzer/models/author.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import TypeAlias
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel,
|
|
5
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
6
6
|
|
|
7
7
|
from pub_analyzer.models.institution import DehydratedInstitution
|
|
8
8
|
|
|
@@ -16,19 +16,11 @@ AuthorOpenAlexKey: TypeAlias = str
|
|
|
16
16
|
class AuthorIDs(BaseModel):
|
|
17
17
|
"""IDs from an Author."""
|
|
18
18
|
|
|
19
|
-
openalex:
|
|
20
|
-
orcid:
|
|
21
|
-
scopus:
|
|
22
|
-
twitter:
|
|
23
|
-
wikipedia:
|
|
24
|
-
|
|
25
|
-
# Allowing a value to be assigned during validation.
|
|
26
|
-
model_config = ConfigDict(validate_assignment=True)
|
|
27
|
-
|
|
28
|
-
@field_validator("scopus", "twitter", "wikipedia")
|
|
29
|
-
def set_default(cls, value: str) -> str:
|
|
30
|
-
"""Define a default text."""
|
|
31
|
-
return value or ""
|
|
19
|
+
openalex: AuthorOpenAlexID
|
|
20
|
+
orcid: HttpUrl | None = None
|
|
21
|
+
scopus: HttpUrl | None = None
|
|
22
|
+
twitter: HttpUrl | None = None
|
|
23
|
+
wikipedia: HttpUrl | None = None
|
|
32
24
|
|
|
33
25
|
|
|
34
26
|
class AuthorYearCount(BaseModel):
|
|
@@ -60,7 +52,7 @@ class Author(BaseModel):
|
|
|
60
52
|
works_count: int
|
|
61
53
|
cited_by_count: int
|
|
62
54
|
|
|
63
|
-
|
|
55
|
+
last_known_institutions: list[DehydratedInstitution]
|
|
64
56
|
counts_by_year: list[AuthorYearCount]
|
|
65
57
|
|
|
66
58
|
summary_stats: AuthorSummaryStats
|
|
@@ -81,16 +73,8 @@ class AuthorResult(BaseModel):
|
|
|
81
73
|
|
|
82
74
|
id: AuthorOpenAlexID
|
|
83
75
|
display_name: str
|
|
84
|
-
hint: str | None =
|
|
76
|
+
hint: str | None = None
|
|
85
77
|
cited_by_count: int
|
|
86
78
|
works_count: int
|
|
87
79
|
entity_type: str
|
|
88
|
-
external_id: str | None =
|
|
89
|
-
|
|
90
|
-
# Allowing a value to be assigned during validation.
|
|
91
|
-
model_config = ConfigDict(validate_assignment=True)
|
|
92
|
-
|
|
93
|
-
@field_validator("hint", "external_id")
|
|
94
|
-
def set_default(cls, value: str) -> str:
|
|
95
|
-
"""Define a default text."""
|
|
96
|
-
return value or ""
|
|
80
|
+
external_id: str | None = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Concept model from OpenAlex API Schema definition."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, HttpUrl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DehydratedConcept(BaseModel):
|
|
7
|
+
"""Stripped-down Concept Model."""
|
|
8
|
+
|
|
9
|
+
id: HttpUrl
|
|
10
|
+
"""The OpenAlex ID for this concept."""
|
|
11
|
+
display_name: str
|
|
12
|
+
"""The English-language label of the concept."""
|
|
13
|
+
|
|
14
|
+
wikidata: HttpUrl
|
|
15
|
+
"""The Wikidata ID for this concept. All OpenAlex concepts are also Wikidata concepts."""
|
|
16
|
+
level: int
|
|
17
|
+
"""The level in the concept. Lower-level concepts are more general, and higher-level concepts are more specific."""
|
|
18
|
+
score: float
|
|
19
|
+
"""The strength of the connection between the work and this concept (higher is stronger)."""
|