pub-analyzer 0.5.6__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.
Files changed (70) hide show
  1. pub_analyzer/__init__.py +1 -0
  2. pub_analyzer/__main__.py +7 -0
  3. pub_analyzer/css/body.tcss +87 -0
  4. pub_analyzer/css/buttons.tcss +24 -0
  5. pub_analyzer/css/checkbox.tcss +29 -0
  6. pub_analyzer/css/collapsible.tcss +31 -0
  7. pub_analyzer/css/datatable.tcss +50 -0
  8. pub_analyzer/css/editor.tcss +60 -0
  9. pub_analyzer/css/main.tcss +50 -0
  10. pub_analyzer/css/report.tcss +131 -0
  11. pub_analyzer/css/search.tcss +81 -0
  12. pub_analyzer/css/summary.tcss +75 -0
  13. pub_analyzer/css/tabs.tcss +18 -0
  14. pub_analyzer/css/tree.tcss +44 -0
  15. pub_analyzer/internal/__init__.py +1 -0
  16. pub_analyzer/internal/identifier.py +106 -0
  17. pub_analyzer/internal/limiter.py +34 -0
  18. pub_analyzer/internal/render.py +41 -0
  19. pub_analyzer/internal/report.py +497 -0
  20. pub_analyzer/internal/templates/author_report.typ +591 -0
  21. pub_analyzer/main.py +81 -0
  22. pub_analyzer/models/__init__.py +1 -0
  23. pub_analyzer/models/author.py +87 -0
  24. pub_analyzer/models/concept.py +19 -0
  25. pub_analyzer/models/institution.py +138 -0
  26. pub_analyzer/models/report.py +111 -0
  27. pub_analyzer/models/source.py +77 -0
  28. pub_analyzer/models/topic.py +59 -0
  29. pub_analyzer/models/work.py +158 -0
  30. pub_analyzer/widgets/__init__.py +1 -0
  31. pub_analyzer/widgets/author/__init__.py +1 -0
  32. pub_analyzer/widgets/author/cards.py +65 -0
  33. pub_analyzer/widgets/author/core.py +122 -0
  34. pub_analyzer/widgets/author/tables.py +50 -0
  35. pub_analyzer/widgets/body.py +55 -0
  36. pub_analyzer/widgets/common/__init__.py +18 -0
  37. pub_analyzer/widgets/common/card.py +29 -0
  38. pub_analyzer/widgets/common/filesystem.py +203 -0
  39. pub_analyzer/widgets/common/filters.py +111 -0
  40. pub_analyzer/widgets/common/input.py +97 -0
  41. pub_analyzer/widgets/common/label.py +36 -0
  42. pub_analyzer/widgets/common/modal.py +43 -0
  43. pub_analyzer/widgets/common/selector.py +66 -0
  44. pub_analyzer/widgets/common/summary.py +7 -0
  45. pub_analyzer/widgets/institution/__init__.py +1 -0
  46. pub_analyzer/widgets/institution/cards.py +78 -0
  47. pub_analyzer/widgets/institution/core.py +122 -0
  48. pub_analyzer/widgets/institution/tables.py +24 -0
  49. pub_analyzer/widgets/report/__init__.py +1 -0
  50. pub_analyzer/widgets/report/author.py +43 -0
  51. pub_analyzer/widgets/report/cards.py +130 -0
  52. pub_analyzer/widgets/report/concept.py +47 -0
  53. pub_analyzer/widgets/report/core.py +308 -0
  54. pub_analyzer/widgets/report/editor.py +80 -0
  55. pub_analyzer/widgets/report/export.py +112 -0
  56. pub_analyzer/widgets/report/grants.py +85 -0
  57. pub_analyzer/widgets/report/institution.py +39 -0
  58. pub_analyzer/widgets/report/locations.py +75 -0
  59. pub_analyzer/widgets/report/source.py +90 -0
  60. pub_analyzer/widgets/report/topic.py +55 -0
  61. pub_analyzer/widgets/report/work.py +391 -0
  62. pub_analyzer/widgets/search/__init__.py +11 -0
  63. pub_analyzer/widgets/search/core.py +96 -0
  64. pub_analyzer/widgets/search/results.py +82 -0
  65. pub_analyzer/widgets/sidebar.py +70 -0
  66. pub_analyzer-0.5.6.dist-info/METADATA +102 -0
  67. pub_analyzer-0.5.6.dist-info/RECORD +70 -0
  68. pub_analyzer-0.5.6.dist-info/WHEEL +4 -0
  69. pub_analyzer-0.5.6.dist-info/entry_points.txt +3 -0
  70. pub_analyzer-0.5.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,97 @@
1
+ """Input widgets."""
2
+
3
+ import re
4
+ from collections.abc import Iterable
5
+
6
+ from rich.highlighter import Highlighter
7
+ from textual import on
8
+ from textual.events import Key
9
+ from textual.suggester import Suggester
10
+ from textual.validation import Regex, Validator
11
+ from textual.widgets import Input as TextualInput
12
+
13
+
14
+ class Input(TextualInput):
15
+ """Input with extra bindings."""
16
+
17
+ DEFAULT_CSS = """
18
+ /* COLORS */
19
+ $primary-color: #b91c1c;
20
+ $primary-color-accent: #991b1b;
21
+ $primary-color-highlight: #dc2626;
22
+
23
+ Input {
24
+ border: tall $background;
25
+ }
26
+
27
+ Input:focus {
28
+ border: tall $primary-color-accent;
29
+ }
30
+ Input.-invalid {
31
+ border: tall orange 40%;
32
+ }
33
+ Input.-invalid:focus {
34
+ border: tall orange 60%;
35
+ }
36
+ """
37
+
38
+ @on(Key)
39
+ def exit_focus(self, event: Key) -> None:
40
+ """Unfocus from the input with esc KEY."""
41
+ if event.key == "escape":
42
+ self.screen.set_focus(None)
43
+
44
+
45
+ class DateSuggester(Suggester):
46
+ """Suggest date format."""
47
+
48
+ async def get_suggestion(self, value: str) -> str | None:
49
+ """Gets a completion as of the current input date."""
50
+ if match := re.match(r"^(?P<year>\d{1,4})(-|$)(?P<month>\d{1,2})?(-|$)(?P<day>\d{1,2})?$", value):
51
+ date_comp = match.groupdict()
52
+
53
+ year = date_comp["year"] or "yyyy"
54
+ month = date_comp["month"] or "mm"
55
+ day = date_comp["day"] or "dd"
56
+
57
+ return f"{year}-{month}-{day}"
58
+ return None
59
+
60
+
61
+ class DateInput(Input):
62
+ """Input with Date validation."""
63
+
64
+ def __init__(
65
+ self,
66
+ value: str | None = None,
67
+ placeholder: str = "",
68
+ highlighter: Highlighter | None = None,
69
+ password: bool = False,
70
+ *,
71
+ validators: Validator | Iterable[Validator] | None = None,
72
+ name: str | None = None,
73
+ id: str | None = None,
74
+ classes: str | None = None,
75
+ disabled: bool = False,
76
+ ) -> None:
77
+ suggester = DateSuggester()
78
+
79
+ super().__init__(
80
+ value,
81
+ placeholder,
82
+ highlighter,
83
+ password,
84
+ suggester=suggester,
85
+ validators=validators,
86
+ name=name,
87
+ id=id,
88
+ classes=classes,
89
+ disabled=disabled,
90
+ )
91
+
92
+ self.validators.append(
93
+ Regex(
94
+ regex=r"^(?P<year>\d{4})-((?P<longMonth>(0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|(?P<shortMonth>(0[469]|11)-(0[1-9]|[12][0-9]|30))|(?P<februaryMonth>(02)-([01][1-9]|[2][0-8])))$",
95
+ failure_description="Input must be formatted as `yyyy-mm-dd`",
96
+ )
97
+ )
@@ -0,0 +1,36 @@
1
+ """Label widget but with reactivity enable."""
2
+
3
+ from rich.console import RenderableType
4
+ from rich.text import Text
5
+ from textual.app import RenderResult
6
+ from textual.reactive import reactive
7
+ from textual.widget import Widget
8
+
9
+
10
+ class ReactiveLabel(Widget):
11
+ """A Label widget but with reactivity enable."""
12
+
13
+ renderable: reactive[RenderableType] = reactive[RenderableType]("", layout=True)
14
+
15
+ def __init__(
16
+ self,
17
+ renderable: RenderableType = "",
18
+ markup: bool = True,
19
+ name: str | None = None,
20
+ id: str | None = None,
21
+ classes: str | None = None,
22
+ disabled: bool = False,
23
+ ):
24
+ super().__init__(name=name, id=id, classes=classes, disabled=disabled)
25
+ self.markup = markup
26
+ self.renderable = renderable
27
+
28
+ def render(self) -> RenderResult:
29
+ """Render widget."""
30
+ if isinstance(self.renderable, str):
31
+ if self.markup:
32
+ return Text.from_markup(self.renderable)
33
+ else:
34
+ return Text(self.renderable)
35
+ else:
36
+ return self.renderable
@@ -0,0 +1,43 @@
1
+ """Modal Screen."""
2
+
3
+ from textual.screen import Screen, ScreenResultType
4
+
5
+
6
+ class Modal(Screen[ScreenResultType]):
7
+ """Base overlay window container."""
8
+
9
+ DEFAULT_CSS = """
10
+ $bg-main-color: white;
11
+ $bg-secondary-color: #e5e7eb;
12
+ $text-primary-color: black;
13
+
14
+ $text-primary-color-darken: black;
15
+
16
+ Modal {
17
+ background: rgba(229, 231, 235, 0.5);
18
+ align: center middle;
19
+ }
20
+
21
+ Modal #dialog {
22
+ background: $bg-main-color;
23
+ height: 100%;
24
+ width: 100%;
25
+
26
+ margin: 3 10;
27
+ border: $bg-secondary-color;
28
+ }
29
+
30
+ .-dark-mode Modal #dialog {
31
+ background: $bg-secondary-color;
32
+ color: $text-primary-color-darken;
33
+ }
34
+
35
+ Modal #dialog .dialog-title {
36
+ height: 3;
37
+ width: 100%;
38
+ margin: 1;
39
+
40
+ text-align: center;
41
+ border-bottom: solid $text-primary-color;
42
+ }
43
+ """
@@ -0,0 +1,66 @@
1
+ """Custom Selector widget."""
2
+
3
+ from typing import TypeVar
4
+
5
+ from textual import on
6
+ from textual.events import Key
7
+ from textual.widgets import Select as TextualSelect
8
+
9
+ SelectType = TypeVar("SelectType")
10
+
11
+
12
+ class Select(TextualSelect[SelectType]):
13
+ """Widget to select from a list of possible options."""
14
+
15
+ DEFAULT_CSS = """
16
+ /* COLORS */
17
+ $primary-color: #b91c1c;
18
+ $primary-color-accent: #991b1b;
19
+ $primary-color-highlight: #dc2626;
20
+
21
+ $bg-secondary-color: #e5e7eb;
22
+
23
+ Select {
24
+ height: 3;
25
+ }
26
+
27
+ Select:focus > SelectCurrent {
28
+ border: tall $primary-color-accent;
29
+ }
30
+
31
+ Select.-expanded > SelectCurrent {
32
+ border: tall $primary-color-accent;
33
+ }
34
+
35
+ SelectOverlay {
36
+ border: tall $bg-secondary-color;
37
+ background: $bg-secondary-color;
38
+ }
39
+
40
+ SelectOverlay:focus {
41
+ border: tall $bg-secondary-color;
42
+ }
43
+
44
+ Select OptionList {
45
+ background: $bg-secondary-color;
46
+ }
47
+
48
+
49
+ Select OptionList:focus > .option-list--option-highlighted {
50
+ background: $primary-color;
51
+ }
52
+
53
+ Select OptionList > .option-list--option-hover-highlighted {
54
+ background: $primary-color-accent;
55
+ }
56
+
57
+ Select OptionList:focus > .option-list--option-hover-highlighted {
58
+ background: $primary-color-accent;
59
+ }
60
+ """
61
+
62
+ @on(Key)
63
+ def exit_focus(self, event: Key) -> None:
64
+ """Unfocus from the input with esc KEY."""
65
+ if event.key == "escape":
66
+ self.screen.set_focus(None)
@@ -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."""
@@ -0,0 +1 @@
1
+ """Institution Widgets."""
@@ -0,0 +1,78 @@
1
+ """Institution Cards Widgets."""
2
+
3
+ from urllib.parse import quote
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Vertical
7
+ from textual.widgets import Label
8
+
9
+ from pub_analyzer.models.institution import Institution
10
+ from pub_analyzer.widgets.common import Card
11
+
12
+
13
+ class CitationMetricsCard(Card):
14
+ """Citation metrics for this institution."""
15
+
16
+ def __init__(self, institution: Institution) -> None:
17
+ self.institution = institution
18
+ super().__init__()
19
+
20
+ def compose(self) -> ComposeResult:
21
+ """Compose card."""
22
+ yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
23
+
24
+ with Vertical(classes="card-container"):
25
+ yield Label(f"[bold]2-year mean:[/bold] {self.institution.summary_stats.two_yr_mean_citedness:.5f}")
26
+ yield Label(f"[bold]h-index:[/bold] {self.institution.summary_stats.h_index}")
27
+ yield Label(f"[bold]i10 index:[/bold] {self.institution.summary_stats.i10_index}")
28
+
29
+
30
+ class IdentifiersCard(Card):
31
+ """Card with external identifiers that we know about this institution."""
32
+
33
+ def __init__(self, institution: Institution) -> None:
34
+ self.institution = institution
35
+ super().__init__()
36
+
37
+ def compose(self) -> ComposeResult:
38
+ """Compose card."""
39
+ yield Label("[italic]Identifiers:[/italic]", classes="card-title")
40
+
41
+ for platform, platform_url in self.institution.ids.model_dump().items():
42
+ if platform_url:
43
+ yield Label(f"""- [@click=app.open_link('{quote(str(platform_url))}')]{platform}[/]""")
44
+
45
+ if self.institution.homepage_url:
46
+ yield Label(f"""- [@click=app.open_link('{quote(str(self.institution.homepage_url))}')]Homepage[/]""")
47
+
48
+
49
+ class GeoCard(Card):
50
+ """Card with location info of this institution."""
51
+
52
+ def __init__(self, institution: Institution) -> None:
53
+ self.institution = institution
54
+ super().__init__()
55
+
56
+ def compose(self) -> ComposeResult:
57
+ """Compose card."""
58
+ yield Label("[italic]Geo:[/italic]", classes="card-title")
59
+
60
+ with Vertical(classes="card-container"):
61
+ yield Label(f"[bold]City:[/bold] {self.institution.geo.city}")
62
+ yield Label(f"[bold]Country:[/bold] {self.institution.geo.country}")
63
+
64
+
65
+ class RolesCard(Card):
66
+ """Card with roles info of this institution."""
67
+
68
+ def __init__(self, institution: Institution) -> None:
69
+ self.institution = institution
70
+ super().__init__()
71
+
72
+ def compose(self) -> ComposeResult:
73
+ """Compose card."""
74
+ yield Label("[italic]Works by roles:[/italic]", classes="card-title")
75
+
76
+ with Vertical(classes="card-container"):
77
+ for role in self.institution.roles:
78
+ yield Label(f"""[@click=app.open_link('{quote(str(role.id))}')]{role.role.value.title()}[/]: {role.works_count}""")
@@ -0,0 +1,122 @@
1
+ """Module with Widgets that allows to display the complete information of Institution using OpenAlex."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from textual import on
7
+ from textual.app import ComposeResult
8
+ from textual.containers import Container, Horizontal, Vertical
9
+ from textual.widgets import Button, Collapsible, Label, Static
10
+
11
+ from pub_analyzer.internal.identifier import get_institution_id
12
+ from pub_analyzer.models.institution import Institution, InstitutionResult
13
+ from pub_analyzer.widgets.common.filters import DateRangeFilter, Filter
14
+ from pub_analyzer.widgets.common.summary import SummaryWidget
15
+ from pub_analyzer.widgets.report.core import CreateInstitutionReportWidget
16
+
17
+ from .cards import CitationMetricsCard, IdentifiersCard, RolesCard
18
+ from .tables import InstitutionWorksByYearTable
19
+
20
+
21
+ class _InstitutionSummaryWidget(Static):
22
+ """Institution info summary."""
23
+
24
+ def __init__(self, institution: Institution) -> None:
25
+ self.institution = institution
26
+ super().__init__()
27
+
28
+ def compose(self) -> ComposeResult:
29
+ """Compose institution info."""
30
+ is_report_not_available = self.institution.works_count < 1
31
+
32
+ # Compose Cards
33
+ with Vertical(classes="block-container"):
34
+ yield Label("[bold]Institution info:[/bold]", classes="block-title")
35
+
36
+ with Horizontal(classes="cards-container"):
37
+ yield RolesCard(institution=self.institution)
38
+ yield IdentifiersCard(institution=self.institution)
39
+ yield CitationMetricsCard(institution=self.institution)
40
+
41
+ # Work related info
42
+ with Vertical(classes="block-container"):
43
+ yield Label("[bold]Work Info:[/bold]", classes="block-title")
44
+
45
+ with Horizontal(classes="info-container"):
46
+ yield Label(f"[bold]Cited by count:[/bold] {self.institution.cited_by_count}")
47
+ yield Label(f"[bold]Works count:[/bold] {self.institution.works_count}")
48
+
49
+ # Count by year table section
50
+ with Container(classes="table-container"):
51
+ yield InstitutionWorksByYearTable(institution=self.institution)
52
+
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")
56
+
57
+ # Filters
58
+ with Collapsible(title="Report filters.", classes="filter-collapsible"):
59
+ # Institution publication Date Range
60
+ yield DateRangeFilter(checkbox_label="Publication date range:", id="institution-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="block-container button-container"):
67
+ yield Button("Make Report", variant="primary", id="make-report-button")
68
+
69
+
70
+ class InstitutionSummaryWidget(SummaryWidget):
71
+ """Institution info summary container."""
72
+
73
+ def __init__(self, institution_result: InstitutionResult) -> None:
74
+ self.institution_result = institution_result
75
+ self.institution: Institution
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)
82
+
83
+ async def _get_info(self) -> None:
84
+ """Query OpenAlex API."""
85
+ institution_id = get_institution_id(self.institution_result)
86
+ url = f"https://api.openalex.org/institutions/{institution_id}"
87
+
88
+ async with httpx.AsyncClient() as client:
89
+ results = (await client.get(url)).json()
90
+ self.institution = Institution(**results)
91
+
92
+ async def load_data(self) -> None:
93
+ """Query OpenAlex API and composing the widget."""
94
+ await self._get_info()
95
+ await self.mount(_InstitutionSummaryWidget(institution=self.institution))
96
+
97
+ self.loading = False
98
+
99
+ @on(Filter.Changed)
100
+ def filter_change(self) -> None:
101
+ """Handle filter changes."""
102
+ filters = [filter for filter in self.query("_InstitutionSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
103
+ all_filters_valid = all(filter.validation_state for filter in filters)
104
+
105
+ self.query_one("_InstitutionSummaryWidget #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("#institution-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 = CreateInstitutionReportWidget(institution=self.institution, **filters)
121
+ await self.app.query_one("MainContent").mount(report_widget)
122
+ await self.app.query_one("InstitutionSummaryWidget").remove()
@@ -0,0 +1,24 @@
1
+ """Institution Tables Widgets."""
2
+
3
+ from rich.table import Table
4
+ from textual.app import ComposeResult
5
+ from textual.widgets import Static
6
+
7
+ from pub_analyzer.models.institution import Institution
8
+
9
+
10
+ class InstitutionWorksByYearTable(Static):
11
+ """Table with Work count and cited by count of the last 10 years."""
12
+
13
+ def __init__(self, institution: Institution) -> None:
14
+ self.institution = institution
15
+ super().__init__()
16
+
17
+ def compose(self) -> ComposeResult:
18
+ """Compose Table."""
19
+ table = Table("Year", "Works Count", "Cited by Count", title="Counts by Year", expand=True)
20
+ for row in self.institution.counts_by_year:
21
+ year, works_count, cited_by_count = row.model_dump().values()
22
+ table.add_row(str(year), str(works_count), str(cited_by_count))
23
+
24
+ yield Static(table)
@@ -0,0 +1 @@
1
+ """Widgets for summarizing and generating reports."""
@@ -0,0 +1,43 @@
1
+ """Author Report Widgets."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal, VerticalScroll
5
+ from textual.widgets import TabbedContent, TabPane
6
+
7
+ from pub_analyzer.models.report import AuthorReport
8
+ from pub_analyzer.widgets.author.cards import CitationMetricsCard, IdentifiersCard, LastInstitutionCard
9
+ from pub_analyzer.widgets.author.tables import AffiliationsTable, AuthorWorksByYearTable
10
+
11
+
12
+ class AuthorReportPane(VerticalScroll):
13
+ """Work report Pane Widget."""
14
+
15
+ DEFAULT_CSS = """
16
+ AuthorReportPane {
17
+ layout: vertical;
18
+ overflow-x: hidden;
19
+ overflow-y: auto;
20
+
21
+ .author-tables-container {
22
+ margin: 1 0 0 0 ;
23
+ height: auto;
24
+ }
25
+ }
26
+ """
27
+
28
+ def __init__(self, report: AuthorReport) -> None:
29
+ self.report = report
30
+ super().__init__()
31
+
32
+ def compose(self) -> ComposeResult:
33
+ """Compose content pane."""
34
+ with Horizontal(classes="cards-container"):
35
+ yield LastInstitutionCard(author=self.report.author)
36
+ yield IdentifiersCard(author=self.report.author)
37
+ yield CitationMetricsCard(author=self.report.author)
38
+
39
+ with TabbedContent(id="author-tables-container"):
40
+ with TabPane("Citation Metrics"):
41
+ yield AuthorWorksByYearTable(author=self.report.author)
42
+ with TabPane("Institutions"):
43
+ yield AffiliationsTable(author=self.report.author)
@@ -0,0 +1,130 @@
1
+ """Report Cards Widgets."""
2
+
3
+ from urllib.parse import quote
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Vertical, VerticalScroll
7
+ from textual.widgets import Label
8
+
9
+ from pub_analyzer.models.author import Author
10
+ from pub_analyzer.models.report import AuthorReport, InstitutionReport, WorkReport
11
+ from pub_analyzer.models.work import Work
12
+ from pub_analyzer.widgets.common import Card
13
+
14
+
15
+ # Works pane cards.
16
+ class ReportCitationMetricsCard(Card):
17
+ """Citation metrics for this report."""
18
+
19
+ def __init__(self, report: AuthorReport | InstitutionReport) -> None:
20
+ self.report = report
21
+ super().__init__()
22
+
23
+ def compose(self) -> ComposeResult:
24
+ """Compose card."""
25
+ yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
26
+
27
+ with Vertical(classes="card-container"):
28
+ type_a_count = self.report.citation_summary.type_a_count
29
+ type_b_count = self.report.citation_summary.type_b_count
30
+ cited_by_count = type_a_count + type_b_count
31
+
32
+ yield Label(f"[bold]Count:[/bold] {cited_by_count}")
33
+ yield Label(f"[bold]Type A:[/bold] {type_a_count}")
34
+ yield Label(f"[bold]Type B:[/bold] {type_b_count}")
35
+
36
+
37
+ class WorksTypeSummaryCard(Card):
38
+ """Works Type Counters Summary Card."""
39
+
40
+ def __init__(self, report: AuthorReport | InstitutionReport) -> None:
41
+ self.report = report
42
+ super().__init__()
43
+
44
+ def compose(self) -> ComposeResult:
45
+ """Compose card."""
46
+ yield Label("[italic]Work Type[/italic]", classes="card-title")
47
+
48
+ with VerticalScroll(classes="card-container"):
49
+ for work_type_counter in self.report.works_type_summary:
50
+ yield Label(f"[bold]{work_type_counter.type_name}:[/bold] {work_type_counter.count}")
51
+
52
+
53
+ class OpenAccessSummaryCard(Card):
54
+ """Open Access counts for this report."""
55
+
56
+ def __init__(self, report: AuthorReport | InstitutionReport) -> None:
57
+ self.report = report
58
+ super().__init__()
59
+
60
+ def compose(self) -> ComposeResult:
61
+ """Compose card."""
62
+ yield Label("[italic]Open Access[/italic]", classes="card-title")
63
+
64
+ with VerticalScroll(classes="card-container"):
65
+ for status, count in self.report.open_access_summary.model_dump().items():
66
+ yield Label(f"[bold]{status.capitalize()}:[/bold] {count}")
67
+
68
+
69
+ # Work Info cards.
70
+ class AuthorshipCard(Card):
71
+ """Card that enumerate the authorship's of a work."""
72
+
73
+ def __init__(self, work: Work, author: Author | None) -> None:
74
+ self.work = work
75
+ self.author = author
76
+ super().__init__()
77
+
78
+ def compose(self) -> ComposeResult:
79
+ """Compose card."""
80
+ yield Label("[italic]Authorship's[/italic]", classes="card-title")
81
+
82
+ with VerticalScroll(classes="card-container"):
83
+ for authorship in self.work.authorships:
84
+ # If the author was provided, highlight
85
+ if self.author and authorship.author.display_name == self.author.display_name:
86
+ author_name_formated = f"[b #909d63]{authorship.author.display_name}[/]"
87
+ else:
88
+ author_name_formated = str(authorship.author.display_name)
89
+
90
+ external_id = authorship.author.orcid or authorship.author.id
91
+ yield Label(
92
+ f"""- [b]{authorship.author_position}:[/b] [@click=app.open_link('{quote(str(external_id))}')]{author_name_formated}[/]""" # noqa: E501
93
+ )
94
+
95
+
96
+ class OpenAccessCard(Card):
97
+ """Card that show OpenAccess status of a work."""
98
+
99
+ def __init__(self, work: Work) -> None:
100
+ self.work = work
101
+ super().__init__()
102
+
103
+ def compose(self) -> ComposeResult:
104
+ """Compose card."""
105
+ work_url = self.work.open_access.oa_url
106
+
107
+ yield Label("[italic]Open Access[/italic]", classes="card-title")
108
+ yield Label(f"[bold]Status:[/bold] {self.work.open_access.oa_status.value.capitalize()}")
109
+ if work_url:
110
+ yield Label(f"""[bold]URL:[/bold] [@click=app.open_link('{quote(str(work_url))}')]{work_url}[/]""")
111
+
112
+
113
+ class CitationMetricsCard(Card):
114
+ """Card that show Citation metrics of a work."""
115
+
116
+ def __init__(self, work_report: WorkReport) -> None:
117
+ self.work_report = work_report
118
+ super().__init__()
119
+
120
+ def compose(self) -> ComposeResult:
121
+ """Compose card."""
122
+ type_a_count = self.work_report.citation_summary.type_a_count
123
+ type_b_count = self.work_report.citation_summary.type_b_count
124
+ cited_by_count = type_a_count + type_b_count
125
+
126
+ yield Label("[italic]Citation[/italic]", classes="card-title")
127
+
128
+ yield Label(f"[bold]Count:[/bold] {cited_by_count}")
129
+ yield Label(f"[bold]Type A:[/bold] {type_a_count}")
130
+ yield Label(f"[bold]Type B:[/bold] {type_b_count}")