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.

Files changed (40) hide show
  1. pub_analyzer/css/body.tcss +48 -35
  2. pub_analyzer/css/buttons.tcss +0 -4
  3. pub_analyzer/css/main.tcss +1 -1
  4. pub_analyzer/css/summary.tcss +75 -0
  5. pub_analyzer/internal/identifier.py +26 -0
  6. pub_analyzer/internal/report.py +73 -31
  7. pub_analyzer/internal/templates/author/{author_resume.typ → author_summary.typ} +4 -3
  8. pub_analyzer/internal/templates/author/report.typ +4 -3
  9. pub_analyzer/internal/templates/author/sources.typ +7 -5
  10. pub_analyzer/internal/templates/author/works.typ +12 -12
  11. pub_analyzer/internal/templates/author/works_extended.typ +4 -4
  12. pub_analyzer/main.py +3 -2
  13. pub_analyzer/models/author.py +9 -25
  14. pub_analyzer/models/concept.py +19 -0
  15. pub_analyzer/models/institution.py +11 -1
  16. pub_analyzer/models/report.py +14 -14
  17. pub_analyzer/models/source.py +59 -3
  18. pub_analyzer/models/topic.py +59 -0
  19. pub_analyzer/models/work.py +23 -0
  20. pub_analyzer/widgets/author/cards.py +6 -5
  21. pub_analyzer/widgets/author/core.py +11 -10
  22. pub_analyzer/widgets/common/summary.py +7 -0
  23. pub_analyzer/widgets/institution/core.py +10 -9
  24. pub_analyzer/widgets/report/cards.py +10 -11
  25. pub_analyzer/widgets/report/concept.py +47 -0
  26. pub_analyzer/widgets/report/core.py +14 -0
  27. pub_analyzer/widgets/report/grants.py +46 -0
  28. pub_analyzer/widgets/report/source.py +11 -4
  29. pub_analyzer/widgets/report/topic.py +55 -0
  30. pub_analyzer/widgets/report/work.py +45 -9
  31. pub_analyzer/widgets/search/results.py +8 -8
  32. pub_analyzer/widgets/sidebar.py +11 -2
  33. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/METADATA +8 -7
  34. pub_analyzer-0.3.0.dist-info/RECORD +69 -0
  35. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/WHEEL +1 -1
  36. pub_analyzer/css/author.tcss +0 -82
  37. pub_analyzer/css/institution.tcss +0 -82
  38. pub_analyzer-0.2.0.dist-info/RECORD +0 -64
  39. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/LICENSE +0 -0
  40. {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]
@@ -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 DehydratedSource
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 CitationResume(BaseModel):
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 OpenAccessResume(BaseModel):
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
- citation_resume: CitationResume
78
+ citation_summary: CitationSummary
79
79
 
80
80
 
81
- class SourcesResume(BaseModel):
81
+ class SourcesSummary(BaseModel):
82
82
  """Sources model with stats."""
83
83
 
84
- sources: list[DehydratedSource]
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
- citation_resume: CitationResume
94
- open_access_resume: OpenAccessResume
95
- works_type_resume: list[WorkTypeCounter]
96
- sources_resume: SourcesResume
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
- citation_resume: CitationResume
106
- open_access_resume: OpenAccessResume
107
- works_type_resume: list[WorkTypeCounter]
108
- sources_resume: SourcesResume
105
+ citation_summary: CitationSummary
106
+ open_access_summary: OpenAccessSummary
107
+ works_type_summary: list[WorkTypeCounter]
108
+ sources_summary: SourcesSummary
@@ -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 | None = False
16
- is_in_doaj: bool | None = False
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."""
@@ -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.last_known_institution:
58
- ror = self.author.last_known_institution.ror
59
- institution_name = self.author.last_known_institution.display_name
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] {self.author.last_known_institution.country_code}")
64
- yield Label(f"[bold]Type:[/bold] {self.author.last_known_institution.type.value}")
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, VerticalScroll
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 _AuthorResumeWidget(Static):
21
- """Author info resume."""
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="block-container button-container"):
66
+ with Vertical(classes="button-container"):
66
67
  yield Button("Make Report", variant="primary", id="make-report-button")
67
68
 
68
69
 
69
- class AuthorResumeWidget(VerticalScroll):
70
- """Author info resume container."""
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(_AuthorResumeWidget(author=self.author))
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("_AuthorResumeWidget Filter").results(Filter) if not filter.filter_disabled]
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("_AuthorResumeWidget #make-report-button", Button).disabled = not all_filters_valid
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("AuthorResumeWidget").remove()
122
+ await self.app.query_one("AuthorSummaryWidget").remove()
@@ -0,0 +1,7 @@
1
+ """Common Summary format Widget."""
2
+
3
+ from textual.containers import VerticalScroll
4
+
5
+
6
+ class SummaryWidget(VerticalScroll):
7
+ """Common format summary container."""
@@ -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, VerticalScroll
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 _InstitutionResumeWidget(Static):
21
- """Institution info resume."""
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 InstitutionResumeWidget(VerticalScroll):
70
- """Institution info resume container."""
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(_InstitutionResumeWidget(institution=self.institution))
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("_InstitutionResumeWidget Filter").results(Filter) if not filter.filter_disabled]
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("_InstitutionResumeWidget #make-report-button", Button).disabled = not all_filters_valid
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("InstitutionResumeWidget").remove()
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.citation_resume.type_a_count
30
- type_b_count = self.report.citation_resume.type_b_count
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 WorksTypeResumeCard(Card):
39
- """Works Type Counters Resume Card."""
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.works_type_resume:
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 OpenAccessResumeCard(Card):
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.open_access_resume.model_dump().items():
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.citation_resume.type_a_count
124
- type_b_count = self.work_report.citation_resume.type_b_count
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")