pub-analyzer 0.2.0__py3-none-any.whl → 0.4.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 +18 -12
- 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_summary.typ +112 -0
- pub_analyzer/internal/templates/author/report.typ +16 -3
- pub_analyzer/internal/templates/author/sources.typ +7 -5
- pub_analyzer/internal/templates/author/works.typ +119 -32
- pub_analyzer/internal/templates/author/works_extended.typ +5 -6
- pub_analyzer/main.py +8 -3
- 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.4.0.dist-info}/METADATA +8 -7
- pub_analyzer-0.4.0.dist-info/RECORD +69 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/WHEEL +1 -1
- pub_analyzer/css/author.tcss +0 -82
- pub_analyzer/css/institution.tcss +0 -82
- pub_analyzer/internal/templates/author/author_resume.typ +0 -96
- pub_analyzer-0.2.0.dist-info/RECORD +0 -64
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,46 +1,133 @@
|
|
|
1
1
|
// Works
|
|
2
2
|
= Works.
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
#let works_metrics_card(title: "Title", graph, body) = {
|
|
5
|
+
grid(
|
|
6
|
+
rows: (18pt, 175pt, 60pt),
|
|
7
|
+
columns: 100%,
|
|
8
|
+
|
|
9
|
+
[
|
|
10
|
+
#block(width: 100%, height: 100%)[
|
|
11
|
+
#align(center + horizon)[#text(style: "italic")[#title]]
|
|
12
|
+
]
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
#block(width: 100%, height: 100%)[
|
|
16
|
+
#align(center + horizon)[#graph]
|
|
17
|
+
]
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
#block(width: 100%, height: 100%, inset: (x: 5pt, y: 10pt))[#body]
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
#grid(
|
|
7
26
|
columns: (1fr, 1fr, 1fr),
|
|
8
|
-
column-gutter:
|
|
27
|
+
column-gutter: 15pt,
|
|
9
28
|
[
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
#let graph = {
|
|
30
|
+
canvas(length: 35%, {
|
|
31
|
+
chart.piechart(
|
|
32
|
+
(
|
|
33
|
+
{{ report.citation_summary.type_a_count }}, // Type A
|
|
34
|
+
{{ report.citation_summary.type_b_count }} // Type B
|
|
35
|
+
),
|
|
36
|
+
radius: 1,
|
|
37
|
+
slice-style: (BLUE, GREEN),
|
|
38
|
+
outer-label: (content: "%", radius: 115%),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#works_metrics_card(title: "Citation metrics", graph)[
|
|
44
|
+
#grid(
|
|
45
|
+
rows: auto, row-gutter: 10pt,
|
|
46
|
+
columns: (1fr, 1fr),
|
|
47
|
+
|
|
48
|
+
grid.cell(colspan: 2)[
|
|
49
|
+
*Count:* {{ report.citation_summary.type_a_count + report.citation_summary.type_b_count }}
|
|
50
|
+
],
|
|
51
|
+
[#box(height: 7pt, width: 7pt, fill: BLUE) *Type A:* {{ report.citation_summary.type_a_count }}],
|
|
52
|
+
[#box(height: 7pt, width: 7pt, fill: GREEN) *Type B:* {{ report.citation_summary.type_b_count }}],
|
|
53
|
+
)
|
|
54
|
+
]
|
|
15
55
|
],
|
|
16
56
|
[
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
57
|
+
#let graph = {
|
|
58
|
+
canvas(length: 35%, {
|
|
59
|
+
chart.columnchart(
|
|
60
|
+
size: (2.45, 2.0),
|
|
61
|
+
y-grid: false,
|
|
62
|
+
bar-style: palette.new(
|
|
63
|
+
base: (stroke: none, fill: none),
|
|
64
|
+
colors: colors
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
{% for work_type in report.works_type_summary[:4] %}
|
|
68
|
+
("{{ work_type.type_name[:2]|capitalize }}", {{ work_type.count }}),
|
|
69
|
+
{% endfor %}
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
#works_metrics_card(title: "Work Type", graph)[
|
|
75
|
+
#grid(
|
|
76
|
+
rows: auto, row-gutter: 10pt,
|
|
77
|
+
columns: (1fr, 1fr),
|
|
78
|
+
column-gutter: 5pt,
|
|
79
|
+
|
|
80
|
+
grid.cell(colspan: 2)[
|
|
81
|
+
*Count:* {{ report.open_access_summary.model_dump().items()|sum(attribute="1") }}
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
{% for work_type in report.works_type_summary[:4] %}
|
|
85
|
+
[
|
|
86
|
+
#box(height: 7pt, width: 7pt, fill: colors.at({{ loop.index0 }})) *{{ work_type.type_name|capitalize }}:* {{ work_type.count }}
|
|
87
|
+
],
|
|
88
|
+
{% endfor %}
|
|
89
|
+
)
|
|
90
|
+
]
|
|
22
91
|
],
|
|
23
92
|
[
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
93
|
+
#let graph = {
|
|
94
|
+
canvas(length: 35%, {
|
|
95
|
+
chart.piechart(
|
|
96
|
+
(
|
|
97
|
+
{{report.open_access_summary.gold}}, // Gold
|
|
98
|
+
{{report.open_access_summary.green}}, // Green
|
|
99
|
+
{{report.open_access_summary.hybrid}}, // Hybrid
|
|
100
|
+
{{report.open_access_summary.bronze}}, // Bronze
|
|
101
|
+
{{report.open_access_summary.closed}}, // Closed
|
|
102
|
+
),
|
|
103
|
+
radius: 1,
|
|
104
|
+
inner-radius: .4,
|
|
105
|
+
slice-style: (YELLOW, GREEN, RED, BLUE, GRAY),
|
|
106
|
+
outer-label: (content: "%", radius: 115%),
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
#works_metrics_card(title: "Open Access", graph)[
|
|
111
|
+
#grid(
|
|
112
|
+
rows: auto, row-gutter: 10pt,
|
|
113
|
+
columns: (1fr, 1fr, 1fr),
|
|
114
|
+
column-gutter: 5pt,
|
|
115
|
+
|
|
116
|
+
grid.cell(colspan: 3)[
|
|
117
|
+
*Count:* {{ report.open_access_summary.model_dump().items()|sum(attribute="1") }}
|
|
118
|
+
],
|
|
119
|
+
|
|
120
|
+
[#box(height: 7pt, width: 7pt, fill: YELLOW) *Gold:* {{report.open_access_summary.gold}}],
|
|
121
|
+
[#box(height: 7pt, width: 7pt, fill: GREEN) *Green:* {{report.open_access_summary.green}}],
|
|
122
|
+
[#box(height: 7pt, width: 7pt, fill: BLUE) *Bronze:* {{report.open_access_summary.bronze}}],
|
|
123
|
+
|
|
124
|
+
[#box(height: 7pt, width: 7pt, fill: GRAY) *Closed:* {{report.open_access_summary.closed}}],
|
|
125
|
+
[#box(height: 7pt, width: 7pt, fill: RED) *Hybrid:* {{report.open_access_summary.hybrid}}],
|
|
126
|
+
)
|
|
127
|
+
]
|
|
39
128
|
],
|
|
40
129
|
)
|
|
41
130
|
|
|
42
|
-
#linebreak()
|
|
43
|
-
|
|
44
131
|
#align(center, text(11pt)[Works from {{ report.works[0].work.publication_year }} to {{ report.works[-1].work.publication_year }}])
|
|
45
132
|
#table(
|
|
46
133
|
columns: (auto, 3fr, auto, auto, auto, auto, auto, auto, auto),
|
|
@@ -56,9 +143,9 @@
|
|
|
56
143
|
[{{ work.work.type }}],
|
|
57
144
|
[{% if work.work.ids.doi %}#underline([#link("{{ work.work.ids.doi }}")[DOI]]){% else %}-{% endif %}],
|
|
58
145
|
[{{ work.work.publication_date }}],
|
|
59
|
-
[{{ work.
|
|
60
|
-
[{{ work.
|
|
61
|
-
[{{ work.
|
|
146
|
+
[{{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}],
|
|
147
|
+
[{{ work.citation_summary.type_a_count }}],
|
|
148
|
+
[{{ work.citation_summary.type_b_count }}],
|
|
62
149
|
[{{ work.work.open_access.oa_status.value }}],
|
|
63
150
|
{% endfor %}
|
|
64
151
|
)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
== Extended info.
|
|
2
1
|
{% for work in report.works %}
|
|
3
2
|
|
|
4
3
|
{% if not loop.first %}
|
|
5
4
|
#pagebreak()
|
|
6
5
|
{% endif %}
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
== #text()[#"{{ work.work.title.replace('"', '\\"') }}"] <work_{{ loop.index }}>
|
|
9
8
|
|
|
10
9
|
#linebreak()
|
|
11
10
|
|
|
@@ -39,9 +38,9 @@
|
|
|
39
38
|
[
|
|
40
39
|
#align(center)[_Citation_]
|
|
41
40
|
#parbreak()
|
|
42
|
-
- *Count:* {{ work.
|
|
43
|
-
- *Type A:* {{ work.
|
|
44
|
-
- *Type B:* {{ work.
|
|
41
|
+
- *Count:* {{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}
|
|
42
|
+
- *Type A:* {{ work.citation_summary.type_a_count }}
|
|
43
|
+
- *Type B:* {{ work.citation_summary.type_b_count }}
|
|
45
44
|
],
|
|
46
45
|
)
|
|
47
46
|
|
|
@@ -83,7 +82,7 @@
|
|
|
83
82
|
// Content
|
|
84
83
|
{% for location in work.work.locations %}
|
|
85
84
|
{% if location.source %}
|
|
86
|
-
[{{ loop.index }}],
|
|
85
|
+
[#underline([#link(label("source_{{ location.source.id.path.rpartition("/")[2] }}"))[{{ loop.index }}]])],
|
|
87
86
|
[#underline([#link("{{ location.landing_page_url }}")[#"{{ location.source.display_name }}"]])],
|
|
88
87
|
[{{ location.source.host_organization_name or "-" }}],
|
|
89
88
|
[{{ 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",
|
|
@@ -42,8 +43,12 @@ class PubAnalyzerApp(App[DOMNode]):
|
|
|
42
43
|
|
|
43
44
|
def compose(self) -> ComposeResult:
|
|
44
45
|
"""Create child widgets for the app."""
|
|
46
|
+
footer = Footer()
|
|
47
|
+
footer.upper_case_keys = True
|
|
48
|
+
footer.ctrl_to_caret = False
|
|
49
|
+
|
|
45
50
|
yield Body()
|
|
46
|
-
yield
|
|
51
|
+
yield footer
|
|
47
52
|
|
|
48
53
|
def action_toggle_dark(self) -> None:
|
|
49
54
|
"""Toggle dark mode."""
|
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)."""
|
|
@@ -77,6 +77,12 @@ class InstitutionRole(BaseModel):
|
|
|
77
77
|
works_count: int
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
class International(BaseModel):
|
|
81
|
+
"""The institution's display name in different languages."""
|
|
82
|
+
|
|
83
|
+
display_name: dict[str, str]
|
|
84
|
+
|
|
85
|
+
|
|
80
86
|
class Institution(BaseModel):
|
|
81
87
|
"""Universities and other organizations to which authors claim affiliations."""
|
|
82
88
|
|
|
@@ -87,11 +93,15 @@ class Institution(BaseModel):
|
|
|
87
93
|
country_code: str
|
|
88
94
|
type: InstitutionType
|
|
89
95
|
homepage_url: HttpUrl | None = None
|
|
96
|
+
image_url: HttpUrl | None = None
|
|
97
|
+
|
|
98
|
+
display_name_acronyms: list[str]
|
|
99
|
+
international: International
|
|
90
100
|
|
|
91
101
|
works_count: int
|
|
92
102
|
cited_by_count: int
|
|
93
|
-
counts_by_year: list[InstitutionYearCount]
|
|
94
103
|
summary_stats: InstitutionSummaryStats
|
|
104
|
+
counts_by_year: list[InstitutionYearCount]
|
|
95
105
|
|
|
96
106
|
geo: InstitutionGeo
|
|
97
107
|
roles: list[InstitutionRole]
|
pub_analyzer/models/report.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic import BaseModel
|
|
|
6
6
|
|
|
7
7
|
from .author import Author
|
|
8
8
|
from .institution import Institution
|
|
9
|
-
from .source import
|
|
9
|
+
from .source import Source
|
|
10
10
|
from .work import OpenAccessStatus, Work
|
|
11
11
|
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ class CitationReport(BaseModel):
|
|
|
24
24
|
citation_type: CitationType
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class
|
|
27
|
+
class CitationSummary(BaseModel):
|
|
28
28
|
"""Summary of citation information in all works."""
|
|
29
29
|
|
|
30
30
|
type_a_count: int = 0
|
|
@@ -38,7 +38,7 @@ class CitationResume(BaseModel):
|
|
|
38
38
|
self.type_b_count += 1
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
class
|
|
41
|
+
class OpenAccessSummary(BaseModel):
|
|
42
42
|
"""Open Access Type counter."""
|
|
43
43
|
|
|
44
44
|
gold: int = 0
|
|
@@ -75,13 +75,13 @@ class WorkReport(BaseModel):
|
|
|
75
75
|
work: Work
|
|
76
76
|
cited_by: list[CitationReport]
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
citation_summary: CitationSummary
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
class
|
|
81
|
+
class SourcesSummary(BaseModel):
|
|
82
82
|
"""Sources model with stats."""
|
|
83
83
|
|
|
84
|
-
sources: list[
|
|
84
|
+
sources: list[Source]
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
class AuthorReport(BaseModel):
|
|
@@ -90,10 +90,10 @@ class AuthorReport(BaseModel):
|
|
|
90
90
|
author: Author
|
|
91
91
|
works: list[WorkReport]
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
citation_summary: CitationSummary
|
|
94
|
+
open_access_summary: OpenAccessSummary
|
|
95
|
+
works_type_summary: list[WorkTypeCounter]
|
|
96
|
+
sources_summary: SourcesSummary
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
class InstitutionReport(BaseModel):
|
|
@@ -102,7 +102,7 @@ class InstitutionReport(BaseModel):
|
|
|
102
102
|
institution: Institution
|
|
103
103
|
works: list[WorkReport]
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
citation_summary: CitationSummary
|
|
106
|
+
open_access_summary: OpenAccessSummary
|
|
107
|
+
works_type_summary: list[WorkTypeCounter]
|
|
108
|
+
sources_summary: SourcesSummary
|
pub_analyzer/models/source.py
CHANGED
|
@@ -1,21 +1,77 @@
|
|
|
1
1
|
"""Sources models from OpenAlex API Schema definition."""
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, HttpUrl
|
|
3
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SourceSummaryStats(BaseModel):
|
|
7
|
+
"""Citation metrics for this Source."""
|
|
8
|
+
|
|
9
|
+
two_yr_mean_citedness: float = Field(..., alias="2yr_mean_citedness")
|
|
10
|
+
"""The 2-year mean citedness for this source. Also known as impact factor."""
|
|
11
|
+
h_index: int
|
|
12
|
+
"""The h-index for this source."""
|
|
13
|
+
i10_index: int
|
|
14
|
+
"""The i-10 index for this source."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SourceYearCount(BaseModel):
|
|
18
|
+
"""Summary of published papers and number of citations in a year."""
|
|
19
|
+
|
|
20
|
+
year: int
|
|
21
|
+
"""Year."""
|
|
22
|
+
works_count: int
|
|
23
|
+
"""The number of Works this source hosts in this year."""
|
|
24
|
+
cited_by_count: int
|
|
25
|
+
"""The total number of Works that cite a Work hosted in this source in this year."""
|
|
4
26
|
|
|
5
27
|
|
|
6
28
|
class DehydratedSource(BaseModel):
|
|
7
29
|
"""Stripped-down Source Model."""
|
|
8
30
|
|
|
9
31
|
id: HttpUrl
|
|
32
|
+
"""The OpenAlex ID for this source."""
|
|
10
33
|
display_name: str
|
|
34
|
+
"""The name of the source."""
|
|
11
35
|
|
|
12
36
|
issn_l: str | None = None
|
|
37
|
+
"""The ISSN-L identifying this source. The ISSN-L designating a single canonical ISSN
|
|
38
|
+
for all media versions of the title. It's usually the same as the print ISSN.
|
|
39
|
+
"""
|
|
13
40
|
issn: list[str] | None = None
|
|
41
|
+
"""The ISSNs used by this source. An ISSN identifies all continuing resources, irrespective
|
|
42
|
+
of their medium (print or electronic). [More info](https://www.issn.org/){target=_blank}.
|
|
43
|
+
"""
|
|
14
44
|
|
|
15
|
-
is_oa: bool
|
|
16
|
-
|
|
45
|
+
is_oa: bool
|
|
46
|
+
"""Whether this is currently fully-open-access source."""
|
|
47
|
+
is_in_doaj: bool
|
|
48
|
+
"""Whether this is a journal listed in the [Directory of Open Access Journals](https://doaj.org){target=_blank} (DOAJ)."""
|
|
17
49
|
|
|
18
50
|
host_organization: HttpUrl | None = None
|
|
51
|
+
"""The host organization for this source as an OpenAlex ID. This will be an
|
|
52
|
+
[Institution.id][pub_analyzer.models.institution.Institution.id] if the source is a repository,
|
|
53
|
+
and a Publisher.id if the source is a journal, conference, or eBook platform
|
|
54
|
+
"""
|
|
19
55
|
host_organization_name: str | None = None
|
|
56
|
+
"""The display_name from the host_organization."""
|
|
20
57
|
|
|
21
58
|
type: str
|
|
59
|
+
"""The type of source, which will be one of: `journal`, `repository`, `conference`,
|
|
60
|
+
`ebook platform`, or `book series`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Source(DehydratedSource):
|
|
65
|
+
"""Where works are hosted."""
|
|
66
|
+
|
|
67
|
+
homepage_url: HttpUrl | None = None
|
|
68
|
+
"""The homepage for this source's website."""
|
|
69
|
+
|
|
70
|
+
is_in_doaj: bool
|
|
71
|
+
"""Whether this is a journal listed in the Directory of Open Access Journals (DOAJ)."""
|
|
72
|
+
|
|
73
|
+
summary_stats: SourceSummaryStats
|
|
74
|
+
"""Citation metrics for this source."""
|
|
75
|
+
|
|
76
|
+
counts_by_year: list[SourceYearCount]
|
|
77
|
+
"""works_count and cited_by_count for each of the last ten years, binned by year."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Topics models from OpenAlex API Schema definition."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, HttpUrl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TopicIDs(BaseModel):
|
|
7
|
+
"""External identifiers for a Topic."""
|
|
8
|
+
|
|
9
|
+
openalex: HttpUrl
|
|
10
|
+
"""The OpenAlex ID for this Topic."""
|
|
11
|
+
wikipedia: HttpUrl | None = None
|
|
12
|
+
"""This topic's Wikipedia page URL."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TopicLevel(BaseModel):
|
|
16
|
+
"""Topic level information."""
|
|
17
|
+
|
|
18
|
+
id: HttpUrl
|
|
19
|
+
"""ID for the topic level. For more info, consult the
|
|
20
|
+
[OpenAlex topic mapping table](https://docs.google.com/spreadsheets/d/1v-MAq64x4YjhO7RWcB-yrKV5D_2vOOsxl4u6GBKEXY8/){target=_blank}.
|
|
21
|
+
"""
|
|
22
|
+
display_name: str
|
|
23
|
+
"""The English-language label of the level."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DehydratedTopic(BaseModel):
|
|
27
|
+
"""Stripped-down Topic Model."""
|
|
28
|
+
|
|
29
|
+
id: HttpUrl
|
|
30
|
+
"""The OpenAlex ID for this Topic."""
|
|
31
|
+
display_name: str
|
|
32
|
+
"""The English-language label of the topic."""
|
|
33
|
+
score: float
|
|
34
|
+
"""The strength of the connection between the work and this topic (higher is stronger)."""
|
|
35
|
+
|
|
36
|
+
domain: TopicLevel
|
|
37
|
+
"""The highest level in the Topics structure."""
|
|
38
|
+
field: TopicLevel
|
|
39
|
+
"""The second-highest level in the Topics structure."""
|
|
40
|
+
subfield: TopicLevel
|
|
41
|
+
"""The third-highest level in the Topics structure."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Topic(DehydratedTopic):
|
|
45
|
+
"""Labels which can be used to describe what a paper is about."""
|
|
46
|
+
|
|
47
|
+
ids: TopicIDs
|
|
48
|
+
"""All the external identifiers for a Topic."""
|
|
49
|
+
description: str
|
|
50
|
+
"""A description of this topic, generated by AI."""
|
|
51
|
+
keywords: list[str]
|
|
52
|
+
"""Keywords consisting of one or several words each, meant to represent
|
|
53
|
+
the content of the papers in the topic.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
works_count: int
|
|
57
|
+
"""The number of works tagged with this topic."""
|
|
58
|
+
cited_by_count: int
|
|
59
|
+
"""The number of citations to works that have been tagged with this topic."""
|
pub_analyzer/models/work.py
CHANGED
|
@@ -6,7 +6,9 @@ from typing import Any
|
|
|
6
6
|
from pydantic import BaseModel, HttpUrl, field_validator
|
|
7
7
|
|
|
8
8
|
from .author import DehydratedAuthor
|
|
9
|
+
from .concept import DehydratedConcept
|
|
9
10
|
from .source import DehydratedSource
|
|
11
|
+
from .topic import DehydratedTopic
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class WorkIDs(BaseModel):
|
|
@@ -72,6 +74,22 @@ class ArticleProcessingCharge(BaseModel):
|
|
|
72
74
|
value_usd: int | None
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
class Grant(BaseModel):
|
|
78
|
+
"""Grant Model Object from OpenAlex API definition."""
|
|
79
|
+
|
|
80
|
+
funder: HttpUrl
|
|
81
|
+
funder_display_name: str
|
|
82
|
+
award_id: str | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Keyword(BaseModel):
|
|
86
|
+
"""Keyword extracted from the work's title and confidence score."""
|
|
87
|
+
|
|
88
|
+
id: HttpUrl
|
|
89
|
+
display_name: str
|
|
90
|
+
score: float
|
|
91
|
+
|
|
92
|
+
|
|
75
93
|
class Work(BaseModel):
|
|
76
94
|
"""Work Model Object from OpenAlex API definition."""
|
|
77
95
|
|
|
@@ -97,6 +115,11 @@ class Work(BaseModel):
|
|
|
97
115
|
To use a verified number that respects the applied filters use [WorkReport][pub_analyzer.models.report.WorkReport].
|
|
98
116
|
"""
|
|
99
117
|
|
|
118
|
+
grants: list[Grant]
|
|
119
|
+
keywords: list[Keyword]
|
|
120
|
+
concepts: list[DehydratedConcept]
|
|
121
|
+
topics: list[DehydratedTopic]
|
|
122
|
+
|
|
100
123
|
referenced_works: list[HttpUrl]
|
|
101
124
|
cited_by_api_url: HttpUrl
|
|
102
125
|
|
|
@@ -54,11 +54,12 @@ class LastInstitutionCard(Card):
|
|
|
54
54
|
"""Compose card."""
|
|
55
55
|
yield Label("[italic]Last Institution:[/italic]", classes="card-title")
|
|
56
56
|
|
|
57
|
-
if self.author.
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if self.author.last_known_institutions:
|
|
58
|
+
last_known_institution = self.author.last_known_institutions[0]
|
|
59
|
+
ror = last_known_institution.ror
|
|
60
|
+
institution_name = last_known_institution.display_name
|
|
60
61
|
|
|
61
62
|
with Vertical(classes="card-container"):
|
|
62
63
|
yield Label(f"""[bold]Name:[/bold] [@click=app.open_link('{quote(str(ror))}')]{institution_name}[/]""")
|
|
63
|
-
yield Label(f"[bold]Country:[/bold] {
|
|
64
|
-
yield Label(f"[bold]Type:[/bold] {
|
|
64
|
+
yield Label(f"[bold]Country:[/bold] {last_known_institution.country_code}")
|
|
65
|
+
yield Label(f"[bold]Type:[/bold] {last_known_institution.type.value}")
|