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
|
@@ -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}")
|
|
@@ -5,20 +5,21 @@ from typing import Any
|
|
|
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
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
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
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
24
|
def __init__(self, author: Author) -> None:
|
|
24
25
|
self.author = author
|
|
@@ -62,12 +63,12 @@ class _AuthorResumeWidget(Static):
|
|
|
62
63
|
yield DateRangeFilter(checkbox_label="Cited date range:", id="cited-date-range-filter")
|
|
63
64
|
|
|
64
65
|
# Button
|
|
65
|
-
with Vertical(classes="
|
|
66
|
+
with Vertical(classes="button-container"):
|
|
66
67
|
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
class
|
|
70
|
-
"""Author info
|
|
70
|
+
class AuthorSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Author info summary container."""
|
|
71
72
|
|
|
72
73
|
def __init__(self, author_result: AuthorResult) -> None:
|
|
73
74
|
self.author_result = author_result
|
|
@@ -91,17 +92,17 @@ class AuthorResumeWidget(VerticalScroll):
|
|
|
91
92
|
async def load_data(self) -> None:
|
|
92
93
|
"""Query OpenAlex API and composing the widget."""
|
|
93
94
|
await self._get_info()
|
|
94
|
-
await self.mount(
|
|
95
|
+
await self.mount(_AuthorSummaryWidget(author=self.author))
|
|
95
96
|
|
|
96
97
|
self.loading = False
|
|
97
98
|
|
|
98
99
|
@on(Filter.Changed)
|
|
99
100
|
def filter_change(self) -> None:
|
|
100
101
|
"""Handle filter changes."""
|
|
101
|
-
filters = [filter for filter in self.query("
|
|
102
|
+
filters = [filter for filter in self.query("_AuthorSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
102
103
|
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
103
104
|
|
|
104
|
-
self.query_one("
|
|
105
|
+
self.query_one("_AuthorSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
105
106
|
|
|
106
107
|
@on(Button.Pressed, "#make-report-button")
|
|
107
108
|
async def make_report(self) -> None:
|
|
@@ -118,4 +119,4 @@ class AuthorResumeWidget(VerticalScroll):
|
|
|
118
119
|
|
|
119
120
|
report_widget = CreateAuthorReportWidget(author=self.author, **filters)
|
|
120
121
|
await self.app.query_one("MainContent").mount(report_widget)
|
|
121
|
-
await self.app.query_one("
|
|
122
|
+
await self.app.query_one("AuthorSummaryWidget").remove()
|
|
@@ -5,20 +5,21 @@ from typing import Any
|
|
|
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
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
9
|
from textual.widgets import Button, Collapsible, Label, Static
|
|
10
10
|
|
|
11
11
|
from pub_analyzer.internal.identifier import get_institution_id
|
|
12
12
|
from pub_analyzer.models.institution import Institution, InstitutionResult
|
|
13
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 CreateInstitutionReportWidget
|
|
15
16
|
|
|
16
17
|
from .cards import CitationMetricsCard, IdentifiersCard, RolesCard
|
|
17
18
|
from .tables import InstitutionWorksByYearTable
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class
|
|
21
|
-
"""Institution info
|
|
21
|
+
class _InstitutionSummaryWidget(Static):
|
|
22
|
+
"""Institution info summary."""
|
|
22
23
|
|
|
23
24
|
def __init__(self, institution: Institution) -> None:
|
|
24
25
|
self.institution = institution
|
|
@@ -66,8 +67,8 @@ class _InstitutionResumeWidget(Static):
|
|
|
66
67
|
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
class
|
|
70
|
-
"""Institution info
|
|
70
|
+
class InstitutionSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Institution info summary container."""
|
|
71
72
|
|
|
72
73
|
def __init__(self, institution_result: InstitutionResult) -> None:
|
|
73
74
|
self.institution_result = institution_result
|
|
@@ -91,17 +92,17 @@ class InstitutionResumeWidget(VerticalScroll):
|
|
|
91
92
|
async def load_data(self) -> None:
|
|
92
93
|
"""Query OpenAlex API and composing the widget."""
|
|
93
94
|
await self._get_info()
|
|
94
|
-
await self.mount(
|
|
95
|
+
await self.mount(_InstitutionSummaryWidget(institution=self.institution))
|
|
95
96
|
|
|
96
97
|
self.loading = False
|
|
97
98
|
|
|
98
99
|
@on(Filter.Changed)
|
|
99
100
|
def filter_change(self) -> None:
|
|
100
101
|
"""Handle filter changes."""
|
|
101
|
-
filters = [filter for filter in self.query("
|
|
102
|
+
filters = [filter for filter in self.query("_InstitutionSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
102
103
|
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
103
104
|
|
|
104
|
-
self.query_one("
|
|
105
|
+
self.query_one("_InstitutionSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
105
106
|
|
|
106
107
|
@on(Button.Pressed, "#make-report-button")
|
|
107
108
|
async def make_report(self) -> None:
|
|
@@ -118,4 +119,4 @@ class InstitutionResumeWidget(VerticalScroll):
|
|
|
118
119
|
|
|
119
120
|
report_widget = CreateInstitutionReportWidget(institution=self.institution, **filters)
|
|
120
121
|
await self.app.query_one("MainContent").mount(report_widget)
|
|
121
|
-
await self.app.query_one("
|
|
122
|
+
await self.app.query_one("InstitutionSummaryWidget").remove()
|
|
@@ -11,9 +11,8 @@ from pub_analyzer.models.report import AuthorReport, InstitutionReport, WorkRepo
|
|
|
11
11
|
from pub_analyzer.models.work import Work
|
|
12
12
|
from pub_analyzer.widgets.common import Card
|
|
13
13
|
|
|
14
|
-
# Works pane cards.
|
|
15
|
-
|
|
16
14
|
|
|
15
|
+
# Works pane cards.
|
|
17
16
|
class ReportCitationMetricsCard(Card):
|
|
18
17
|
"""Citation metrics for this report."""
|
|
19
18
|
|
|
@@ -26,8 +25,8 @@ class ReportCitationMetricsCard(Card):
|
|
|
26
25
|
yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
|
|
27
26
|
|
|
28
27
|
with Vertical(classes="card-container"):
|
|
29
|
-
type_a_count = self.report.
|
|
30
|
-
type_b_count = self.report.
|
|
28
|
+
type_a_count = self.report.citation_summary.type_a_count
|
|
29
|
+
type_b_count = self.report.citation_summary.type_b_count
|
|
31
30
|
cited_by_count = type_a_count + type_b_count
|
|
32
31
|
|
|
33
32
|
yield Label(f"[bold]Count:[/bold] {cited_by_count}")
|
|
@@ -35,8 +34,8 @@ class ReportCitationMetricsCard(Card):
|
|
|
35
34
|
yield Label(f"[bold]Type B:[/bold] {type_b_count}")
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
class
|
|
39
|
-
"""Works Type Counters
|
|
37
|
+
class WorksTypeSummaryCard(Card):
|
|
38
|
+
"""Works Type Counters Summary Card."""
|
|
40
39
|
|
|
41
40
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
42
41
|
self.report = report
|
|
@@ -47,11 +46,11 @@ class WorksTypeResumeCard(Card):
|
|
|
47
46
|
yield Label("[italic]Work Type[/italic]", classes="card-title")
|
|
48
47
|
|
|
49
48
|
with VerticalScroll(classes="card-container"):
|
|
50
|
-
for work_type_counter in self.report.
|
|
49
|
+
for work_type_counter in self.report.works_type_summary:
|
|
51
50
|
yield Label(f"[bold]{work_type_counter.type_name}:[/bold] {work_type_counter.count}")
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
class
|
|
53
|
+
class OpenAccessSummaryCard(Card):
|
|
55
54
|
"""Open Access counts for this report."""
|
|
56
55
|
|
|
57
56
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
@@ -63,7 +62,7 @@ class OpenAccessResumeCard(Card):
|
|
|
63
62
|
yield Label("[italic]Open Access[/italic]", classes="card-title")
|
|
64
63
|
|
|
65
64
|
with VerticalScroll(classes="card-container"):
|
|
66
|
-
for status, count in self.report.
|
|
65
|
+
for status, count in self.report.open_access_summary.model_dump().items():
|
|
67
66
|
yield Label(f"[bold]{status}:[/bold] {count}")
|
|
68
67
|
|
|
69
68
|
|
|
@@ -120,8 +119,8 @@ class CitationMetricsCard(Card):
|
|
|
120
119
|
|
|
121
120
|
def compose(self) -> ComposeResult:
|
|
122
121
|
"""Compose card."""
|
|
123
|
-
type_a_count = self.work_report.
|
|
124
|
-
type_b_count = self.work_report.
|
|
122
|
+
type_a_count = self.work_report.citation_summary.type_a_count
|
|
123
|
+
type_b_count = self.work_report.citation_summary.type_b_count
|
|
125
124
|
cited_by_count = type_a_count + type_b_count
|
|
126
125
|
|
|
127
126
|
yield Label("[italic]Citation[/italic]", classes="card-title")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Concepts Widgets."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.models.concept import DehydratedConcept
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConceptsTable(Static):
|
|
14
|
+
"""All Concepts from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
ConceptsTable .concepts-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, concepts_list: list[DehydratedConcept]) -> None:
|
|
24
|
+
self.concepts_list = concepts_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
concepts_table = Table(title="Concepts", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
32
|
+
concepts_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
concepts_table.add_column("Name", ratio=5)
|
|
34
|
+
concepts_table.add_column("Score", ratio=1)
|
|
35
|
+
concepts_table.add_column("Level", ratio=1)
|
|
36
|
+
|
|
37
|
+
for idx, concept in enumerate(self.concepts_list):
|
|
38
|
+
name = f"""[@click=app.open_link('{quote(str(concept.wikidata))}')][u]{concept.display_name}[/u][/]"""
|
|
39
|
+
|
|
40
|
+
concepts_table.add_row(
|
|
41
|
+
str(idx),
|
|
42
|
+
Text.from_markup(name, overflow="ellipsis"),
|
|
43
|
+
Text.from_markup(f"{concept.score:.2f}"),
|
|
44
|
+
Text.from_markup(f"{concept.level:.1f}"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
yield Static(concepts_table, classes="concepts-table")
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
import datetime
|
|
4
4
|
import pathlib
|
|
5
5
|
from enum import Enum
|
|
6
|
+
from typing import ClassVar
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
from pydantic import TypeAdapter, ValidationError
|
|
9
10
|
from textual import on
|
|
10
11
|
from textual.app import ComposeResult
|
|
12
|
+
from textual.binding import Binding, BindingType
|
|
11
13
|
from textual.containers import Container, Horizontal
|
|
14
|
+
from textual.reactive import reactive
|
|
12
15
|
from textual.widget import Widget
|
|
13
16
|
from textual.widgets import Button, LoadingIndicator, Static, TabbedContent, TabPane
|
|
14
17
|
|
|
@@ -28,6 +31,17 @@ from .work import WorkReportPane
|
|
|
28
31
|
class ReportWidget(Static):
|
|
29
32
|
"""Base report widget."""
|
|
30
33
|
|
|
34
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
35
|
+
Binding(key="ctrl+y", action="toggle_works", description="Toggle empty works"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
show_empty_works: reactive[bool] = reactive(True)
|
|
39
|
+
|
|
40
|
+
async def action_toggle_works(self) -> None:
|
|
41
|
+
"""Toggle show empty works attribute."""
|
|
42
|
+
self.show_empty_works = not self.show_empty_works
|
|
43
|
+
await self.query_one(WorkReportPane).toggle_empty_works()
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
class AuthorReportWidget(ReportWidget):
|
|
33
47
|
"""Author report generator view."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Grants Widgets."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.models.work import Grant
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GrantsTable(Static):
|
|
14
|
+
"""All Grants from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
GrantsTable .grants-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, grants_list: list[Grant]) -> None:
|
|
24
|
+
self.grants_list = grants_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
grants_table = Table(title="Grants", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
32
|
+
grants_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
grants_table.add_column("Name", ratio=3)
|
|
34
|
+
grants_table.add_column("Award ID", ratio=2)
|
|
35
|
+
|
|
36
|
+
for idx, grant in enumerate(self.grants_list):
|
|
37
|
+
name = f"""[@click=app.open_link('{quote(str(grant.funder))}')][u]{grant.funder_display_name}[/u][/]"""
|
|
38
|
+
award_id = grant.award_id or "-"
|
|
39
|
+
|
|
40
|
+
grants_table.add_row(
|
|
41
|
+
str(idx),
|
|
42
|
+
Text.from_markup(name, overflow="ellipsis"),
|
|
43
|
+
Text.from_markup(award_id),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
yield Static(grants_table, classes="grants-table")
|