pub-analyzer 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0 → pub_analyzer-0.1.2}/PKG-INFO +2 -2
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/report.tcss +10 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/report.py +35 -4
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/report.typ +1 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/works.typ +1 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/works_extended.typ +9 -1
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/work.py +4 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/author/core.py +3 -3
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/cards.py +7 -3
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/core.py +16 -2
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/export.py +25 -19
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/work.py +14 -7
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/search/core.py +3 -1
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pyproject.toml +10 -10
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/LICENSE +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/README.md +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/author.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/body.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/buttons.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/checkbox.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/datatable.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/institution.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/main.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/search.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/tabs.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/css/tree.tcss +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/identifier.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/render.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/author_resume.typ +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/sources.typ +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/main.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/author.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/institution.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/report.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/models/source.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/author/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/author/cards.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/author/tables.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/body.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/card.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/filesystem.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/input.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/modal.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/common/selector.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/institution/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/institution/cards.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/institution/core.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/institution/tables.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/author.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/institution.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/locations.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/report/source.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/search/__init__.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/search/results.py +0 -0
- {pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/widgets/sidebar.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pub-analyzer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A text user interface, written in python, which automates the generation of scientific production reports using OpenAlex
|
|
5
5
|
Home-page: https://github.com/alejandrgaspar/pub-analyzer
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Classifier: Typing :: Typed
|
|
|
24
24
|
Requires-Dist: httpx (==0.24.1)
|
|
25
25
|
Requires-Dist: jinja2 (==3.1.2)
|
|
26
26
|
Requires-Dist: pydantic (==2.3.0)
|
|
27
|
-
Requires-Dist: textual (==0.
|
|
27
|
+
Requires-Dist: textual (==0.35.1)
|
|
28
28
|
Requires-Dist: typst (==0.7.0)
|
|
29
29
|
Project-URL: Documentation, https://pub-analyzer.com/
|
|
30
30
|
Project-URL: Repository, https://github.com/alejandrgaspar/pub-analyzer
|
|
@@ -71,6 +71,10 @@ LoadReportWidget .button-container {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/* Export Report Pane */
|
|
74
|
+
ExportReportPane #export-form {
|
|
75
|
+
height: auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
74
78
|
ExportReportPane .export-form-input-container {
|
|
75
79
|
height: auto;
|
|
76
80
|
margin-bottom: 2;
|
|
@@ -107,6 +111,12 @@ WorkModal #dialog .cards-container {
|
|
|
107
111
|
grid-gutter: 1 2;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
WorkModal #dialog .abstract {
|
|
115
|
+
height: auto;
|
|
116
|
+
width: 100%;
|
|
117
|
+
padding: 1 2;
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
WorkModal #dialog #tables-container {
|
|
111
121
|
margin: 1 0;
|
|
112
122
|
}
|
|
@@ -61,6 +61,24 @@ def _get_citation_type(original_work_authors: list[str], cited_work_authors: lis
|
|
|
61
61
|
|
|
62
62
|
return CitationType.TypeA if not original_set.intersection(cited_set) else CitationType.TypeB
|
|
63
63
|
|
|
64
|
+
|
|
65
|
+
def _add_work_abstract(work: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
"""Get work abtract from abstract_inverted_index and insert new key `abstract`.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
work: Raw work.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Work with new key `abstract`.
|
|
73
|
+
"""
|
|
74
|
+
abstract_inverted_index = work.get("abstract_inverted_index")
|
|
75
|
+
if abstract_inverted_index:
|
|
76
|
+
work["abstract"] = " ".join(abstract_inverted_index)
|
|
77
|
+
else:
|
|
78
|
+
work["abstract"] = None
|
|
79
|
+
return work
|
|
80
|
+
|
|
81
|
+
|
|
64
82
|
def _get_valid_works(works: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
65
83
|
"""Skip works that do not contain enough data.
|
|
66
84
|
|
|
@@ -75,7 +93,8 @@ def _get_valid_works(works: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
75
93
|
In response, we have chosen to exclude such works at this stage, thus avoiding
|
|
76
94
|
the need to handle exceptions within the Model validators.
|
|
77
95
|
"""
|
|
78
|
-
return [work for work in works if work['title'] is not None]
|
|
96
|
+
return [_add_work_abstract(work) for work in works if work['title'] is not None]
|
|
97
|
+
|
|
79
98
|
|
|
80
99
|
async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
|
|
81
100
|
"""Get all works given a URL.
|
|
@@ -88,12 +107,18 @@ async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
|
|
|
88
107
|
|
|
89
108
|
Returns:
|
|
90
109
|
List of Works Models.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
|
|
91
113
|
"""
|
|
92
|
-
response =
|
|
93
|
-
|
|
114
|
+
response = await client.get(url=url)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
|
|
117
|
+
json_response = response.json()
|
|
118
|
+
meta_info = json_response["meta"]
|
|
94
119
|
page_count = math.ceil(meta_info["count"] / meta_info["per_page"])
|
|
95
120
|
|
|
96
|
-
works_data = list(_get_valid_works(
|
|
121
|
+
works_data = list(_get_valid_works(json_response["results"]),)
|
|
97
122
|
|
|
98
123
|
for page_number in range(1, page_count):
|
|
99
124
|
page_result = (await client.get(url + f"&page={page_number + 1}")).json()
|
|
@@ -112,6 +137,9 @@ async def make_author_report(author: Author, from_date: datetime.date | None = N
|
|
|
112
137
|
|
|
113
138
|
Returns:
|
|
114
139
|
Author's scientific production report Model.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
|
|
115
143
|
"""
|
|
116
144
|
author_id = identifier.get_author_id(author)
|
|
117
145
|
|
|
@@ -188,6 +216,9 @@ async def make_institution_report(
|
|
|
188
216
|
|
|
189
217
|
Returns:
|
|
190
218
|
Institution's scientific production report Model.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
|
|
191
222
|
"""
|
|
192
223
|
institution_id = identifier.get_institution_id(institution)
|
|
193
224
|
|
{pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/works_extended.typ
RENAMED
|
@@ -6,8 +6,16 @@
|
|
|
6
6
|
{% endif %}
|
|
7
7
|
|
|
8
8
|
=== #text()[#"{{ work.work.title.replace('"', '\\"') }}"] <work_{{ loop.index }}>
|
|
9
|
+
|
|
10
|
+
#linebreak()
|
|
11
|
+
|
|
12
|
+
{% if work.work.abstract %}
|
|
13
|
+
#text()[#"{{ work.work.abstract.replace('"', '\\"') }}"]
|
|
14
|
+
|
|
9
15
|
#linebreak()
|
|
10
16
|
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
11
19
|
// Cards
|
|
12
20
|
#grid(
|
|
13
21
|
columns: (1fr, 1fr, 1fr),
|
|
@@ -16,7 +24,7 @@
|
|
|
16
24
|
#align(center)[_Authorships_]
|
|
17
25
|
#parbreak()
|
|
18
26
|
{% for authorship in work.work.authorships[:10] %}
|
|
19
|
-
- *{{ authorship.author_position }}:* #text({% if authorship.author.display_name == report.author.display_name %}rgb("909d63"){% endif %})[{{ authorship.author.display_name }}]
|
|
27
|
+
- *{{ authorship.author_position }}:* #underline([#link("{{ authorship.author.orcid or authorship.author.id }}")[#text({% if authorship.author.display_name == report.author.display_name %}rgb("909d63"){% endif %})[{{ authorship.author.display_name }}]]])
|
|
20
28
|
{% endfor %}
|
|
21
29
|
{% if work.work.authorships|length > 10 %}
|
|
22
30
|
- *...*
|
|
@@ -79,6 +79,7 @@ class Work(BaseModel):
|
|
|
79
79
|
ids: WorkIDs
|
|
80
80
|
|
|
81
81
|
title: str
|
|
82
|
+
abstract: str | None = None
|
|
82
83
|
publication_year: int
|
|
83
84
|
publication_date: str
|
|
84
85
|
language: str | None = None
|
|
@@ -92,6 +93,9 @@ class Work(BaseModel):
|
|
|
92
93
|
authorships: list[Authorship]
|
|
93
94
|
|
|
94
95
|
cited_by_count: int
|
|
96
|
+
"""This number comes from the OpenAlex API, represents ALL citations to this work, and may not always be correct.
|
|
97
|
+
To use a verified number that respects the applied filters use [WorkReport][pub_analyzer.models.report.WorkReport].
|
|
98
|
+
"""
|
|
95
99
|
|
|
96
100
|
referenced_works: list[HttpUrl]
|
|
97
101
|
cited_by_api_url: HttpUrl
|
|
@@ -67,10 +67,10 @@ class AuthorResumeWidget(Static):
|
|
|
67
67
|
from_input = self.query_one("#from-date", DateInput)
|
|
68
68
|
to_input = self.query_one("#to-date", DateInput)
|
|
69
69
|
|
|
70
|
-
if checkbox.value and from_input.value
|
|
70
|
+
if checkbox.value and (from_input.value or to_input.value):
|
|
71
71
|
date_format = "%Y-%m-%d"
|
|
72
|
-
from_date = datetime.datetime.strptime(from_input.value, date_format)
|
|
73
|
-
to_date = datetime.datetime.strptime(to_input.value, date_format)
|
|
72
|
+
from_date = datetime.datetime.strptime(from_input.value, date_format) if from_input.value else None
|
|
73
|
+
to_date = datetime.datetime.strptime(to_input.value, date_format) if to_input.value else None
|
|
74
74
|
|
|
75
75
|
report_widget = CreateAuthorReportWidget(author=self.author, from_date=from_date, to_date=to_date)
|
|
76
76
|
else:
|
|
@@ -117,8 +117,12 @@ class CitationMetricsCard(Card):
|
|
|
117
117
|
|
|
118
118
|
def compose(self) -> ComposeResult:
|
|
119
119
|
"""Compose card."""
|
|
120
|
+
type_a_count = self.work_report.citation_resume.type_a_count
|
|
121
|
+
type_b_count = self.work_report.citation_resume.type_b_count
|
|
122
|
+
cited_by_count = type_a_count + type_b_count
|
|
123
|
+
|
|
120
124
|
yield Label('[italic]Citation[/italic]', classes='card-title')
|
|
121
125
|
|
|
122
|
-
yield Label(f'[bold]Count:[/bold] {
|
|
123
|
-
yield Label(f'[bold]Type A:[/bold] {
|
|
124
|
-
yield Label(f'[bold]Type B:[/bold] {
|
|
126
|
+
yield Label(f'[bold]Count:[/bold] {cited_by_count}')
|
|
127
|
+
yield Label(f'[bold]Type A:[/bold] {type_a_count}')
|
|
128
|
+
yield Label(f'[bold]Type B:[/bold] {type_b_count}')
|
|
@@ -4,6 +4,7 @@ import datetime
|
|
|
4
4
|
import pathlib
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
|
+
import httpx
|
|
7
8
|
from pydantic import TypeAdapter, ValidationError
|
|
8
9
|
from textual import on
|
|
9
10
|
from textual.app import ComposeResult
|
|
@@ -89,7 +90,19 @@ class CreateReportWidget(Static):
|
|
|
89
90
|
|
|
90
91
|
async def mount_report(self) -> None:
|
|
91
92
|
"""Mount report."""
|
|
92
|
-
|
|
93
|
+
try:
|
|
94
|
+
report_widget = await self.make_report()
|
|
95
|
+
except httpx.HTTPStatusError as exc:
|
|
96
|
+
self.query_one(LoadingIndicator).display = False
|
|
97
|
+
status_error = f"HTTP Exception for url: {exc.request.url}. Status code: {exc.response.status_code}"
|
|
98
|
+
self.app.notify(
|
|
99
|
+
title="Error making report!",
|
|
100
|
+
message=f"The report could not be generated due to a problem with the OpenAlex API. {status_error}",
|
|
101
|
+
severity="error",
|
|
102
|
+
timeout=20.0
|
|
103
|
+
)
|
|
104
|
+
return None
|
|
105
|
+
|
|
93
106
|
container = self.query_one(Container)
|
|
94
107
|
await container.mount(report_widget)
|
|
95
108
|
|
|
@@ -198,9 +211,10 @@ class LoadReportWidget(Static):
|
|
|
198
211
|
match event.value:
|
|
199
212
|
case self.EntityType.AUTHOR:
|
|
200
213
|
self.entity_handler = self.EntityType.AUTHOR
|
|
201
|
-
|
|
202
214
|
case self.EntityType.INSTITUTION:
|
|
203
215
|
self.entity_handler = self.EntityType.INSTITUTION
|
|
216
|
+
case _:
|
|
217
|
+
raise NotImplementedError
|
|
204
218
|
|
|
205
219
|
def compose(self) -> ComposeResult:
|
|
206
220
|
"""Compose load report widget."""
|
|
@@ -4,7 +4,7 @@ import pathlib
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
|
-
from textual import on
|
|
7
|
+
from textual import on, work
|
|
8
8
|
from textual.app import ComposeResult
|
|
9
9
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
10
10
|
from textual.widgets import Button, Label
|
|
@@ -46,9 +46,10 @@ class ExportReportPane(VerticalScroll):
|
|
|
46
46
|
match event.value:
|
|
47
47
|
case self.ExportFileType.JSON:
|
|
48
48
|
file_name_input.value = f"{self.suggest_prefix}-{datetime.now().strftime('%m-%d-%Y')}.json"
|
|
49
|
-
|
|
50
49
|
case self.ExportFileType.PDF:
|
|
51
50
|
file_name_input.value = f"{self.suggest_prefix}-{datetime.now().strftime('%m-%d-%Y')}.pdf"
|
|
51
|
+
case _:
|
|
52
|
+
file_name_input.value = f"{self.suggest_prefix}-{datetime.now().strftime('%m-%d-%Y')}"
|
|
52
53
|
|
|
53
54
|
@on(FileSystemSelector.FileSelected)
|
|
54
55
|
def enable_button(self, event: FileSystemSelector.FileSelected) -> None:
|
|
@@ -58,31 +59,36 @@ class ExportReportPane(VerticalScroll):
|
|
|
58
59
|
else:
|
|
59
60
|
self.query_one(Button).disabled = True
|
|
60
61
|
|
|
62
|
+
@work(exclusive=True, thread=True)
|
|
63
|
+
async def _export_report(self, file_type: ExportFileType, file_path: pathlib.Path) -> None:
|
|
64
|
+
"""Export report."""
|
|
65
|
+
match file_type:
|
|
66
|
+
case self.ExportFileType.JSON:
|
|
67
|
+
with open(file_path, mode="w", encoding="utf-8") as file:
|
|
68
|
+
file.write(self.report.model_dump_json(indent=2, by_alias=True))
|
|
69
|
+
case self.ExportFileType.PDF:
|
|
70
|
+
report_bytes = await render_report(report=self.report, file_path=file_path)
|
|
71
|
+
with open(file_path, mode="wb") as file:
|
|
72
|
+
file.write(report_bytes)
|
|
73
|
+
|
|
74
|
+
self.app.call_from_thread(
|
|
75
|
+
self.app.notify,
|
|
76
|
+
title="Report exported successfully!",
|
|
77
|
+
message=f"The report was exported correctly. You can go see it at [i]{file_path}[/]",
|
|
78
|
+
timeout=20.0
|
|
79
|
+
)
|
|
80
|
+
|
|
61
81
|
@on(Button.Pressed, "#export-report-button")
|
|
62
82
|
async def export_report(self) -> None:
|
|
63
|
-
"""
|
|
83
|
+
"""Handle export report button."""
|
|
64
84
|
export_path = self.query_one(FileSystemSelector).path_selected
|
|
65
85
|
file_name = self.query_one(Input).value
|
|
66
86
|
file_type = self.query_one(self.ExportTypeSelector).value
|
|
67
87
|
|
|
68
|
-
if export_path and file_name:
|
|
88
|
+
if export_path and file_name and file_type:
|
|
69
89
|
file_path = export_path.joinpath(file_name)
|
|
70
|
-
|
|
71
|
-
match file_type:
|
|
72
|
-
case self.ExportFileType.JSON:
|
|
73
|
-
with open(file_path, mode="w", encoding="utf-8") as file:
|
|
74
|
-
file.write(self.report.model_dump_json(indent=2, by_alias=True))
|
|
75
|
-
case self.ExportFileType.PDF:
|
|
76
|
-
report_bytes = await render_report(report=self.report, file_path=file_path)
|
|
77
|
-
with open(file_path, mode="wb") as file:
|
|
78
|
-
file.write(report_bytes)
|
|
79
|
-
|
|
90
|
+
self._export_report(file_type=file_type, file_path=file_path)
|
|
80
91
|
self.query_one(Button).disabled = True
|
|
81
|
-
self.app.notify(
|
|
82
|
-
title="Report exported successfully!",
|
|
83
|
-
message=f"The report was exported correctly. You can go see it at [i]{file_path}[/]",
|
|
84
|
-
timeout=20.0
|
|
85
|
-
)
|
|
86
92
|
|
|
87
93
|
def compose(self) -> ComposeResult:
|
|
88
94
|
"""Compose content pane."""
|
|
@@ -107,7 +107,6 @@ class WorkModal(Modal[None]):
|
|
|
107
107
|
# Citation Metrics
|
|
108
108
|
yield CitationMetricsCard(work_report=self.work_report)
|
|
109
109
|
|
|
110
|
-
|
|
111
110
|
with TabbedContent(id="tables-container"):
|
|
112
111
|
# Citations Table
|
|
113
112
|
with TabPane("Cited By Works"):
|
|
@@ -121,6 +120,10 @@ class WorkModal(Modal[None]):
|
|
|
121
120
|
yield LocationsTable(self.work_report.work.locations)
|
|
122
121
|
else:
|
|
123
122
|
yield Label("No sources found.")
|
|
123
|
+
# Abtract if exists
|
|
124
|
+
if self.work_report.work.abstract:
|
|
125
|
+
with TabPane("Abstract"):
|
|
126
|
+
yield Label(self.work_report.work.abstract, classes="abstract")
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
class WorksTable(Static):
|
|
@@ -154,11 +157,14 @@ class WorksTable(Static):
|
|
|
154
157
|
|
|
155
158
|
def compose(self) -> ComposeResult:
|
|
156
159
|
"""Generate Table."""
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
if self.report.works:
|
|
161
|
+
first_pub_year = self.report.works[0].work.publication_year
|
|
162
|
+
last_pub_year = self.report.works[-1].work.publication_year
|
|
163
|
+
title = f"Works from {first_pub_year} to {last_pub_year}"
|
|
164
|
+
else:
|
|
165
|
+
title = "Works"
|
|
166
|
+
|
|
167
|
+
work_table = Table(title=title, expand=True, show_lines=True)
|
|
162
168
|
work_table.add_column('', justify='center', vertical='middle')
|
|
163
169
|
work_table.add_column('Title', ratio=3)
|
|
164
170
|
work_table.add_column('Type', ratio=2)
|
|
@@ -205,4 +211,5 @@ class WorkReportPane(VerticalScroll):
|
|
|
205
211
|
yield WorksTypeResumeCard(report=self.report)
|
|
206
212
|
yield OpenAccessResumeCard(report=self.report)
|
|
207
213
|
|
|
208
|
-
|
|
214
|
+
if self.report.works:
|
|
215
|
+
yield WorksTable(report=self.report)
|
|
@@ -86,9 +86,11 @@ class FinderWidget(Static):
|
|
|
86
86
|
case self.OpenAlexEndPoint.AUTHOR:
|
|
87
87
|
self.url = self.OpenAlexEndPoint.AUTHOR
|
|
88
88
|
search_bar.placeholder = "Search author"
|
|
89
|
-
|
|
90
89
|
case self.OpenAlexEndPoint.INSTITUTION:
|
|
91
90
|
self.url = self.OpenAlexEndPoint.INSTITUTION
|
|
92
91
|
search_bar.placeholder = "Search institution"
|
|
92
|
+
case _:
|
|
93
|
+
self.url = self.OpenAlexEndPoint.AUTHOR
|
|
94
|
+
search_bar.placeholder = "Search author"
|
|
93
95
|
|
|
94
96
|
search_bar.value = ""
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "pub-analyzer"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A text user interface, written in python, which automates the generation of scientific production reports using OpenAlex"
|
|
9
9
|
|
|
10
10
|
authors = ["Alejandro Gaspar <alejandro@gaspar.land>"]
|
|
@@ -39,7 +39,7 @@ pub-analyzer = "pub_analyzer.main:run"
|
|
|
39
39
|
[tool.poetry.dependencies]
|
|
40
40
|
python = "^3.10"
|
|
41
41
|
|
|
42
|
-
textual = "0.
|
|
42
|
+
textual = "0.35.1"
|
|
43
43
|
httpx = "0.24.1"
|
|
44
44
|
pydantic = "2.3.0"
|
|
45
45
|
|
|
@@ -49,28 +49,28 @@ jinja2 = "3.1.2"
|
|
|
49
49
|
[tool.poetry.group.dev.dependencies]
|
|
50
50
|
textual-dev = "1.1.0"
|
|
51
51
|
|
|
52
|
-
pre-commit = "3.
|
|
52
|
+
pre-commit = "3.4.0"
|
|
53
53
|
mypy = "1.5.1"
|
|
54
|
-
ruff = "0.0.
|
|
54
|
+
ruff = "0.0.287"
|
|
55
55
|
|
|
56
|
-
pytest = "7.4.
|
|
57
|
-
pytest-asyncio = "0.21.
|
|
56
|
+
pytest = "7.4.1"
|
|
57
|
+
pytest-asyncio = "0.21.1"
|
|
58
58
|
respx = "0.20.2"
|
|
59
59
|
vcrpy = "5.1.0"
|
|
60
60
|
pytest-recording = "0.13.0"
|
|
61
61
|
|
|
62
62
|
[tool.poetry.group.docs.dependencies]
|
|
63
63
|
mkdocs = "1.5.2"
|
|
64
|
-
mkdocs-material = "9.2.
|
|
64
|
+
mkdocs-material = "9.2.8"
|
|
65
65
|
|
|
66
|
-
mkdocstrings = {extras = ["python"], version = "0.
|
|
67
|
-
mkdocstrings-python = "1.
|
|
66
|
+
mkdocstrings = {extras = ["python"], version = "0.23.0"}
|
|
67
|
+
mkdocstrings-python = "1.6.1"
|
|
68
68
|
|
|
69
69
|
[tool.mypy]
|
|
70
70
|
strict = true
|
|
71
71
|
|
|
72
72
|
[tool.ruff]
|
|
73
|
-
required-version = "0.0.
|
|
73
|
+
required-version = "0.0.287"
|
|
74
74
|
target-version = "py310"
|
|
75
75
|
|
|
76
76
|
line-length = 140
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/author_resume.typ
RENAMED
|
File without changes
|
{pub_analyzer-0.1.0 → pub_analyzer-0.1.2}/pub_analyzer/internal/templates/author/sources.typ
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|