pub-analyzer 0.1.2__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 -1
- pub_analyzer/css/collapsible.tcss +31 -0
- pub_analyzer/css/main.tcss +4 -0
- pub_analyzer/css/summary.tcss +75 -0
- pub_analyzer/internal/identifier.py +36 -10
- pub_analyzer/internal/render.py +1 -1
- pub_analyzer/internal/report.py +177 -53
- 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 +6 -7
- pub_analyzer/models/author.py +20 -28
- pub_analyzer/models/concept.py +19 -0
- pub_analyzer/models/institution.py +22 -5
- 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 +30 -7
- pub_analyzer/widgets/author/cards.py +15 -14
- pub_analyzer/widgets/author/core.py +80 -115
- pub_analyzer/widgets/author/tables.py +1 -1
- pub_analyzer/widgets/common/__init__.py +6 -6
- pub_analyzer/widgets/common/filesystem.py +16 -13
- pub_analyzer/widgets/common/filters.py +111 -0
- pub_analyzer/widgets/common/input.py +14 -5
- pub_analyzer/widgets/common/selector.py +1 -1
- pub_analyzer/widgets/common/summary.py +7 -0
- pub_analyzer/widgets/institution/cards.py +13 -15
- pub_analyzer/widgets/institution/core.py +81 -115
- pub_analyzer/widgets/institution/tables.py +1 -1
- pub_analyzer/widgets/report/cards.py +33 -31
- pub_analyzer/widgets/report/concept.py +47 -0
- pub_analyzer/widgets/report/core.py +90 -20
- pub_analyzer/widgets/report/export.py +2 -2
- pub_analyzer/widgets/report/grants.py +46 -0
- pub_analyzer/widgets/report/locations.py +14 -12
- pub_analyzer/widgets/report/source.py +22 -14
- pub_analyzer/widgets/report/topic.py +55 -0
- pub_analyzer/widgets/report/work.py +70 -34
- pub_analyzer/widgets/search/__init__.py +4 -4
- pub_analyzer/widgets/search/results.py +15 -16
- pub_analyzer/widgets/sidebar.py +11 -9
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/METADATA +31 -7
- pub_analyzer-0.3.0.dist-info/RECORD +69 -0
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/WHEEL +1 -1
- pub_analyzer/css/author.tcss +0 -78
- pub_analyzer/css/institution.tcss +0 -78
- pub_analyzer-0.1.2.dist-info/RECORD +0 -62
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
"""Institutions models from OpenAlex API Schema definition."""
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
|
+
from typing import TypeAlias
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field, HttpUrl
|
|
6
7
|
|
|
8
|
+
InstitutionOpenAlexID: TypeAlias = HttpUrl
|
|
9
|
+
"""OpenAlex ID for Institution Objects with the format `https://openalex.org/I000000000`"""
|
|
10
|
+
|
|
11
|
+
InstitutionOpenAlexKey: TypeAlias = str
|
|
12
|
+
"""OpenAlex Institution entity Key with the format `I000000000`"""
|
|
13
|
+
|
|
7
14
|
|
|
8
15
|
class InstitutionIDs(BaseModel):
|
|
9
16
|
"""IDs from a Institution."""
|
|
10
17
|
|
|
11
|
-
openalex:
|
|
18
|
+
openalex: InstitutionOpenAlexID
|
|
12
19
|
grid: str | None = None
|
|
13
20
|
ror: HttpUrl | None = None
|
|
14
21
|
wikipedia: HttpUrl | None = None
|
|
@@ -70,21 +77,31 @@ class InstitutionRole(BaseModel):
|
|
|
70
77
|
works_count: int
|
|
71
78
|
|
|
72
79
|
|
|
80
|
+
class International(BaseModel):
|
|
81
|
+
"""The institution's display name in different languages."""
|
|
82
|
+
|
|
83
|
+
display_name: dict[str, str]
|
|
84
|
+
|
|
85
|
+
|
|
73
86
|
class Institution(BaseModel):
|
|
74
87
|
"""Universities and other organizations to which authors claim affiliations."""
|
|
75
88
|
|
|
76
|
-
id:
|
|
89
|
+
id: InstitutionOpenAlexID
|
|
77
90
|
ids: InstitutionIDs
|
|
78
91
|
|
|
79
92
|
display_name: str
|
|
80
93
|
country_code: str
|
|
81
94
|
type: InstitutionType
|
|
82
95
|
homepage_url: HttpUrl | None = None
|
|
96
|
+
image_url: HttpUrl | None = None
|
|
97
|
+
|
|
98
|
+
display_name_acronyms: list[str]
|
|
99
|
+
international: International
|
|
83
100
|
|
|
84
101
|
works_count: int
|
|
85
102
|
cited_by_count: int
|
|
86
|
-
counts_by_year: list[InstitutionYearCount]
|
|
87
103
|
summary_stats: InstitutionSummaryStats
|
|
104
|
+
counts_by_year: list[InstitutionYearCount]
|
|
88
105
|
|
|
89
106
|
geo: InstitutionGeo
|
|
90
107
|
roles: list[InstitutionRole]
|
|
@@ -95,7 +112,7 @@ class Institution(BaseModel):
|
|
|
95
112
|
class DehydratedInstitution(BaseModel):
|
|
96
113
|
"""Stripped-down Institution Model."""
|
|
97
114
|
|
|
98
|
-
id:
|
|
115
|
+
id: InstitutionOpenAlexID
|
|
99
116
|
ror: str
|
|
100
117
|
display_name: str
|
|
101
118
|
country_code: str
|
|
@@ -105,7 +122,7 @@ class DehydratedInstitution(BaseModel):
|
|
|
105
122
|
class InstitutionResult(BaseModel):
|
|
106
123
|
"""Institution result Model resulting from a search in OpenAlex."""
|
|
107
124
|
|
|
108
|
-
id:
|
|
125
|
+
id: InstitutionOpenAlexID
|
|
109
126
|
display_name: str
|
|
110
127
|
hint: str | None = None
|
|
111
128
|
|
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):
|
|
@@ -34,7 +36,7 @@ class Location(BaseModel):
|
|
|
34
36
|
license: str | None
|
|
35
37
|
pdf_url: str | None
|
|
36
38
|
version: WorkDrivenVersion | None
|
|
37
|
-
source: DehydratedSource
|
|
39
|
+
source: DehydratedSource | None = None
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class OpenAccessStatus(str, Enum):
|
|
@@ -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
|
|
|
@@ -105,20 +128,20 @@ class Work(BaseModel):
|
|
|
105
128
|
apc_paid: ArticleProcessingCharge | None = None
|
|
106
129
|
"""APC actually paid by authors."""
|
|
107
130
|
|
|
108
|
-
@field_validator(
|
|
131
|
+
@field_validator("locations", mode="before")
|
|
109
132
|
def valid_locations(cls, locations: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
110
133
|
"""Skip locations that do not contain enough data."""
|
|
111
|
-
return [location for location in locations if location[
|
|
134
|
+
return [location for location in locations if location["landing_page_url"] is not None]
|
|
112
135
|
|
|
113
|
-
@field_validator(
|
|
136
|
+
@field_validator("primary_location", "best_oa_location", mode="before")
|
|
114
137
|
def valid_location(cls, location: dict[str, Any]) -> dict[str, Any] | None:
|
|
115
138
|
"""Skip location that do not contain enough data."""
|
|
116
|
-
if location and location[
|
|
139
|
+
if location and location["landing_page_url"] is None:
|
|
117
140
|
return None
|
|
118
141
|
else:
|
|
119
142
|
return location
|
|
120
143
|
|
|
121
|
-
@field_validator(
|
|
144
|
+
@field_validator("authorships", mode="before")
|
|
122
145
|
def valid_authorships(cls, authorships: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
123
146
|
"""Skip authorships that do not contain enough data."""
|
|
124
|
-
return [authorship for authorship in authorships if authorship[
|
|
147
|
+
return [authorship for authorship in authorships if authorship["author"].get("id") is not None]
|
|
@@ -19,12 +19,12 @@ class CitationMetricsCard(Card):
|
|
|
19
19
|
|
|
20
20
|
def compose(self) -> ComposeResult:
|
|
21
21
|
"""Compose card."""
|
|
22
|
-
yield Label(
|
|
22
|
+
yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
|
|
23
23
|
|
|
24
|
-
with Vertical(classes=
|
|
25
|
-
yield Label(f
|
|
26
|
-
yield Label(f
|
|
27
|
-
yield Label(f
|
|
24
|
+
with Vertical(classes="card-container"):
|
|
25
|
+
yield Label(f"[bold]2-year mean:[/bold] {self.author.summary_stats.two_yr_mean_citedness:.5f}")
|
|
26
|
+
yield Label(f"[bold]h-index:[/bold] {self.author.summary_stats.h_index}")
|
|
27
|
+
yield Label(f"[bold]i10 index:[/bold] {self.author.summary_stats.i10_index}")
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class IdentifiersCard(Card):
|
|
@@ -36,7 +36,7 @@ class IdentifiersCard(Card):
|
|
|
36
36
|
|
|
37
37
|
def compose(self) -> ComposeResult:
|
|
38
38
|
"""Compose card."""
|
|
39
|
-
yield Label(
|
|
39
|
+
yield Label("[italic]Identifiers:[/italic]", classes="card-title")
|
|
40
40
|
|
|
41
41
|
for platform, platform_url in self.author.ids.model_dump().items():
|
|
42
42
|
if platform_url:
|
|
@@ -52,13 +52,14 @@ class LastInstitutionCard(Card):
|
|
|
52
52
|
|
|
53
53
|
def compose(self) -> ComposeResult:
|
|
54
54
|
"""Compose card."""
|
|
55
|
-
yield Label(
|
|
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
|
-
with Vertical(classes=
|
|
62
|
-
yield Label(f
|
|
63
|
-
yield Label(f
|
|
64
|
-
yield Label(f
|
|
62
|
+
with Vertical(classes="card-container"):
|
|
63
|
+
yield Label(f"""[bold]Name:[/bold] [@click=app.open_link('{quote(str(ror))}')]{institution_name}[/]""")
|
|
64
|
+
yield Label(f"[bold]Country:[/bold] {last_known_institution.country_code}")
|
|
65
|
+
yield Label(f"[bold]Type:[/bold] {last_known_institution.type.value}")
|
|
@@ -1,83 +1,84 @@
|
|
|
1
1
|
"""Module with Widgets that allows to display the complete information of an Author using OpenAlex."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
from textual import on
|
|
7
7
|
from textual.app import ComposeResult
|
|
8
|
-
from textual.containers import Container, Horizontal, Vertical
|
|
9
|
-
from textual.widgets import Button,
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
|
+
from textual.widgets import Button, Collapsible, Label, Static
|
|
10
10
|
|
|
11
11
|
from pub_analyzer.internal.identifier import get_author_id
|
|
12
12
|
from pub_analyzer.models.author import Author, AuthorResult
|
|
13
|
-
from pub_analyzer.widgets.common import
|
|
13
|
+
from pub_analyzer.widgets.common.filters import DateRangeFilter, Filter
|
|
14
|
+
from pub_analyzer.widgets.common.summary import SummaryWidget
|
|
14
15
|
from pub_analyzer.widgets.report.core import CreateAuthorReportWidget
|
|
15
16
|
|
|
16
17
|
from .cards import CitationMetricsCard, IdentifiersCard, LastInstitutionCard
|
|
17
18
|
from .tables import AuthorWorksByYearTable
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class
|
|
21
|
-
"""Author info
|
|
21
|
+
class _AuthorSummaryWidget(Static):
|
|
22
|
+
"""Author info summary."""
|
|
22
23
|
|
|
23
|
-
def __init__(self,
|
|
24
|
-
self.
|
|
25
|
-
self.author: Author
|
|
24
|
+
def __init__(self, author: Author) -> None:
|
|
25
|
+
self.author = author
|
|
26
26
|
super().__init__()
|
|
27
27
|
|
|
28
28
|
def compose(self) -> ComposeResult:
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
yield VerticalScroll(id="main-container")
|
|
29
|
+
"""Compose author info."""
|
|
30
|
+
is_report_not_available = self.author.works_count < 1
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
self.run_worker(self.load_data(), exclusive=True)
|
|
32
|
+
# Compose Cards
|
|
33
|
+
with Vertical(classes="block-container"):
|
|
34
|
+
yield Label("[bold]Author info:[/bold]", classes="block-title")
|
|
37
35
|
|
|
36
|
+
with Horizontal(classes="cards-container"):
|
|
37
|
+
yield LastInstitutionCard(author=self.author)
|
|
38
|
+
yield IdentifiersCard(author=self.author)
|
|
39
|
+
yield CitationMetricsCard(author=self.author)
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if event.checkbox.value:
|
|
43
|
-
for date_input in self.query(DateInput).results(DateInput):
|
|
44
|
-
date_input.disabled = False
|
|
45
|
-
date_input.value = ""
|
|
46
|
-
else:
|
|
47
|
-
for date_input in self.query(DateInput).results(DateInput):
|
|
48
|
-
date_input.disabled = True
|
|
49
|
-
date_input.value = ""
|
|
50
|
-
self.query_one("#make-report-button", Button).disabled = False
|
|
51
|
-
|
|
52
|
-
@on(DateInput.Changed)
|
|
53
|
-
async def enable_make_report(self, event: DateInput.Changed) -> None:
|
|
54
|
-
"""Enable make report button."""
|
|
55
|
-
checkbox = self.query_one("#filters-checkbox", Checkbox)
|
|
56
|
-
|
|
57
|
-
if event.validation_result:
|
|
58
|
-
if not event.validation_result.is_valid and checkbox.value:
|
|
59
|
-
self.query_one("#make-report-button", Button).disabled = True
|
|
60
|
-
else:
|
|
61
|
-
self.query_one("#make-report-button", Button).disabled = False
|
|
41
|
+
# Work realeted info
|
|
42
|
+
with Vertical(classes="block-container"):
|
|
43
|
+
yield Label("[bold]Work Info:[/bold]", classes="block-title")
|
|
62
44
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
checkbox = self.query_one("#filters-checkbox", Checkbox)
|
|
67
|
-
from_input = self.query_one("#from-date", DateInput)
|
|
68
|
-
to_input = self.query_one("#to-date", DateInput)
|
|
45
|
+
with Horizontal(classes="info-container"):
|
|
46
|
+
yield Label(f"[bold]Cited by count:[/bold] {self.author.cited_by_count}")
|
|
47
|
+
yield Label(f"[bold]Works count:[/bold] {self.author.works_count}")
|
|
69
48
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
to_date = datetime.datetime.strptime(to_input.value, date_format) if to_input.value else None
|
|
49
|
+
# Count by year table section
|
|
50
|
+
with Container(classes="table-container"):
|
|
51
|
+
yield AuthorWorksByYearTable(author=self.author)
|
|
74
52
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
# Make report section
|
|
54
|
+
with Vertical(classes="block-container", disabled=is_report_not_available):
|
|
55
|
+
yield Label("[bold]Make report:[/bold]", classes="block-title")
|
|
78
56
|
|
|
79
|
-
|
|
80
|
-
|
|
57
|
+
# Filters
|
|
58
|
+
with Collapsible(title="Report filters.", classes="filter-collapsible"):
|
|
59
|
+
# Author publication Date Range
|
|
60
|
+
yield DateRangeFilter(checkbox_label="Publication date range:", id="author-date-range-filter")
|
|
61
|
+
|
|
62
|
+
# Cite Date Range
|
|
63
|
+
yield DateRangeFilter(checkbox_label="Cited date range:", id="cited-date-range-filter")
|
|
64
|
+
|
|
65
|
+
# Button
|
|
66
|
+
with Vertical(classes="button-container"):
|
|
67
|
+
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AuthorSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Author info summary container."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, author_result: AuthorResult) -> None:
|
|
74
|
+
self.author_result = author_result
|
|
75
|
+
self.author: Author
|
|
76
|
+
super().__init__()
|
|
77
|
+
|
|
78
|
+
def on_mount(self) -> None:
|
|
79
|
+
"""Hide the empty container and call data in the background."""
|
|
80
|
+
self.loading = True
|
|
81
|
+
self.run_worker(self.load_data(), exclusive=True)
|
|
81
82
|
|
|
82
83
|
async def _get_info(self) -> None:
|
|
83
84
|
"""Query OpenAlex API."""
|
|
@@ -91,67 +92,31 @@ class AuthorResumeWidget(Static):
|
|
|
91
92
|
async def load_data(self) -> None:
|
|
92
93
|
"""Query OpenAlex API and composing the widget."""
|
|
93
94
|
await self._get_info()
|
|
94
|
-
|
|
95
|
-
is_report_not_available = self.author.works_count < 1
|
|
95
|
+
await self.mount(_AuthorSummaryWidget(author=self.author))
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
await container.mount(
|
|
99
|
-
Vertical(
|
|
100
|
-
Label('[bold]Author info:[/bold]', classes="block-title"),
|
|
101
|
-
Horizontal(
|
|
102
|
-
LastInstitutionCard(author=self.author),
|
|
103
|
-
IdentifiersCard(author=self.author),
|
|
104
|
-
CitationMetricsCard(author=self.author),
|
|
105
|
-
classes="cards-container"
|
|
106
|
-
),
|
|
107
|
-
classes="block-container"
|
|
108
|
-
)
|
|
109
|
-
)
|
|
97
|
+
self.loading = False
|
|
110
98
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Label(f'[bold]Cited by count:[/bold] {self.author.cited_by_count}'),
|
|
117
|
-
Label(f'[bold]Works count:[/bold] {self.author.works_count}'),
|
|
118
|
-
classes="info-container"
|
|
119
|
-
),
|
|
120
|
-
classes="block-container"
|
|
121
|
-
)
|
|
122
|
-
)
|
|
99
|
+
@on(Filter.Changed)
|
|
100
|
+
def filter_change(self) -> None:
|
|
101
|
+
"""Handle filter changes."""
|
|
102
|
+
filters = [filter for filter in self.query("_AuthorSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
103
|
+
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
123
104
|
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
classes="info-container filter-container",
|
|
143
|
-
),
|
|
144
|
-
|
|
145
|
-
# Button
|
|
146
|
-
Vertical(
|
|
147
|
-
Button("Make Report", variant="primary", id="make-report-button"),
|
|
148
|
-
classes="block-container button-container"
|
|
149
|
-
),
|
|
150
|
-
classes="block-container",
|
|
151
|
-
disabled=is_report_not_available
|
|
152
|
-
)
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Show results
|
|
156
|
-
self.query_one(LoadingIndicator).display = False
|
|
157
|
-
container.display = True
|
|
105
|
+
self.query_one("_AuthorSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
106
|
+
|
|
107
|
+
@on(Button.Pressed, "#make-report-button")
|
|
108
|
+
async def make_report(self) -> None:
|
|
109
|
+
"""Make the author report."""
|
|
110
|
+
filters: dict[str, Any] = {}
|
|
111
|
+
pub_date_range = self.query_one("#author-date-range-filter", DateRangeFilter)
|
|
112
|
+
cited_date_range = self.query_one("#cited-date-range-filter", DateRangeFilter)
|
|
113
|
+
|
|
114
|
+
if not pub_date_range.filter_disabled:
|
|
115
|
+
filters.update({"pub_from_date": pub_date_range.from_date, "pub_to_date": pub_date_range.to_date})
|
|
116
|
+
|
|
117
|
+
if not cited_date_range.filter_disabled:
|
|
118
|
+
filters.update({"cited_from_date": cited_date_range.from_date, "cited_to_date": cited_date_range.to_date})
|
|
119
|
+
|
|
120
|
+
report_widget = CreateAuthorReportWidget(author=self.author, **filters)
|
|
121
|
+
await self.app.query_one("MainContent").mount(report_widget)
|
|
122
|
+
await self.app.query_one("AuthorSummaryWidget").remove()
|
|
@@ -16,7 +16,7 @@ class AuthorWorksByYearTable(Static):
|
|
|
16
16
|
|
|
17
17
|
def compose(self) -> ComposeResult:
|
|
18
18
|
"""Compose Table."""
|
|
19
|
-
table = Table(
|
|
19
|
+
table = Table("Year", "Works Count", "Cited by Count", title="Counts by Year", expand=True)
|
|
20
20
|
for row in self.author.counts_by_year:
|
|
21
21
|
year, works_count, cited_by_count = row.model_dump().values()
|
|
22
22
|
table.add_row(str(year), str(works_count), str(cited_by_count))
|