pub-analyzer 0.4.3__py3-none-any.whl → 0.5.1__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/editor.tcss +60 -0
- pub_analyzer/css/report.tcss +15 -6
- pub_analyzer/internal/limiter.py +34 -0
- pub_analyzer/internal/render.py +17 -44
- pub_analyzer/internal/report.py +60 -29
- pub_analyzer/internal/templates/author_report.typ +556 -0
- pub_analyzer/main.py +4 -7
- pub_analyzer/models/institution.py +3 -3
- pub_analyzer/widgets/author/tables.py +2 -1
- pub_analyzer/widgets/body.py +19 -0
- pub_analyzer/widgets/common/__init__.py +2 -0
- pub_analyzer/widgets/common/label.py +36 -0
- pub_analyzer/widgets/report/core.py +10 -0
- pub_analyzer/widgets/report/editor.py +80 -0
- pub_analyzer/widgets/report/export.py +2 -4
- pub_analyzer/widgets/report/work.py +145 -6
- pub_analyzer/widgets/search/__init__.py +2 -2
- pub_analyzer/widgets/search/results.py +2 -12
- {pub_analyzer-0.4.3.dist-info → pub_analyzer-0.5.1.dist-info}/METADATA +7 -8
- {pub_analyzer-0.4.3.dist-info → pub_analyzer-0.5.1.dist-info}/RECORD +23 -23
- {pub_analyzer-0.4.3.dist-info → pub_analyzer-0.5.1.dist-info}/WHEEL +1 -1
- pub_analyzer/internal/templates/author/author_summary.typ +0 -112
- pub_analyzer/internal/templates/author/report.typ +0 -91
- pub_analyzer/internal/templates/author/sources.typ +0 -22
- pub_analyzer/internal/templates/author/works.typ +0 -154
- pub_analyzer/internal/templates/author/works_extended.typ +0 -109
- {pub_analyzer-0.4.3.dist-info → pub_analyzer-0.5.1.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.4.3.dist-info → pub_analyzer-0.5.1.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import datetime
|
|
4
4
|
import pathlib
|
|
5
5
|
from enum import Enum
|
|
6
|
+
from time import time
|
|
6
7
|
from typing import ClassVar
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
@@ -105,7 +106,9 @@ class CreateReportWidget(Static):
|
|
|
105
106
|
async def mount_report(self) -> None:
|
|
106
107
|
"""Mount report."""
|
|
107
108
|
try:
|
|
109
|
+
start = time()
|
|
108
110
|
report_widget = await self.make_report()
|
|
111
|
+
elapsed = time() - start
|
|
109
112
|
except httpx.HTTPStatusError as exc:
|
|
110
113
|
self.query_one(LoadingIndicator).display = False
|
|
111
114
|
status_error = f"HTTP Exception for url: {exc.request.url}. Status code: {exc.response.status_code}"
|
|
@@ -117,6 +120,13 @@ class CreateReportWidget(Static):
|
|
|
117
120
|
)
|
|
118
121
|
return None
|
|
119
122
|
|
|
123
|
+
self.app.notify(
|
|
124
|
+
title="Report created!",
|
|
125
|
+
message=f"Elapsed {elapsed:.2f}s",
|
|
126
|
+
severity="information",
|
|
127
|
+
timeout=20.0,
|
|
128
|
+
)
|
|
129
|
+
|
|
120
130
|
container = self.query_one(Container)
|
|
121
131
|
await container.mount(report_widget)
|
|
122
132
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Text editor widget."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from textual import events, on
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal, VerticalScroll
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
from textual.widgets import Button, Label, Static, TextArea
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.widgets.common import Modal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextEditor(Modal[str | None]):
|
|
14
|
+
"""Text editor widget."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, display_name: str, text: str) -> None:
|
|
17
|
+
self.display_name = display_name
|
|
18
|
+
self.text = text
|
|
19
|
+
super().__init__()
|
|
20
|
+
|
|
21
|
+
@on(events.Key)
|
|
22
|
+
def exit_modal(self, message: events.Key) -> None:
|
|
23
|
+
"""Exit from the modal with esc KEY."""
|
|
24
|
+
if message.key == "escape":
|
|
25
|
+
self.dismiss(None)
|
|
26
|
+
|
|
27
|
+
@on(Button.Pressed, "#save")
|
|
28
|
+
def save(self) -> None:
|
|
29
|
+
"""Return the edited content."""
|
|
30
|
+
self.dismiss(self.query_one(TextArea).text)
|
|
31
|
+
|
|
32
|
+
@on(Button.Pressed, "#cancel")
|
|
33
|
+
def cancel(self) -> None:
|
|
34
|
+
"""Cancel action button handler."""
|
|
35
|
+
self.dismiss(None)
|
|
36
|
+
|
|
37
|
+
def compose(self) -> ComposeResult:
|
|
38
|
+
"""Compose text editor."""
|
|
39
|
+
with VerticalScroll(id="dialog"):
|
|
40
|
+
yield Label(f'Editing the "{self.display_name}" field.', classes="dialog-title")
|
|
41
|
+
|
|
42
|
+
with VerticalScroll(id="text-editor-container"):
|
|
43
|
+
yield TextArea(text=self.text, theme="css", soft_wrap=True, show_line_numbers=True, tab_behavior="indent")
|
|
44
|
+
|
|
45
|
+
with Horizontal(id="actions-buttons"):
|
|
46
|
+
yield Button("Save", variant="primary", id="save")
|
|
47
|
+
yield Button("Cancel", variant="default", id="cancel")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EditWidget(Static):
|
|
51
|
+
"""Ask for edit widget."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, display_name: str, field_name: str, model: BaseModel, widget: Widget | None, widget_field: str | None) -> None:
|
|
54
|
+
self.display_name = display_name
|
|
55
|
+
self.field_name = field_name
|
|
56
|
+
self.model = model
|
|
57
|
+
self.widget = widget
|
|
58
|
+
self.widget_field = widget_field
|
|
59
|
+
super().__init__()
|
|
60
|
+
|
|
61
|
+
@on(Button.Pressed)
|
|
62
|
+
def launch_text_editor(self) -> None:
|
|
63
|
+
"""Lunch Text editor."""
|
|
64
|
+
|
|
65
|
+
def save(new_text: str | None) -> None:
|
|
66
|
+
"""Save changes if save button is pressed."""
|
|
67
|
+
if new_text:
|
|
68
|
+
self.app.log("Value updated.")
|
|
69
|
+
setattr(self.model, self.field_name, new_text)
|
|
70
|
+
|
|
71
|
+
if self.widget and self.widget_field:
|
|
72
|
+
setattr(self.widget, self.widget_field, new_text)
|
|
73
|
+
|
|
74
|
+
text = getattr(self.model, self.field_name)
|
|
75
|
+
self.app.push_screen(TextEditor(self.display_name, text), callback=save)
|
|
76
|
+
|
|
77
|
+
def compose(self) -> ComposeResult:
|
|
78
|
+
"""Compose widget."""
|
|
79
|
+
with Horizontal():
|
|
80
|
+
yield Button("Edit", variant="primary")
|
|
@@ -60,16 +60,14 @@ class ExportReportPane(VerticalScroll):
|
|
|
60
60
|
self.query_one(Button).disabled = True
|
|
61
61
|
|
|
62
62
|
@work(exclusive=True, thread=True)
|
|
63
|
-
|
|
63
|
+
def _export_report(self, file_type: ExportFileType, file_path: pathlib.Path) -> None:
|
|
64
64
|
"""Export report."""
|
|
65
65
|
match file_type:
|
|
66
66
|
case self.ExportFileType.JSON:
|
|
67
67
|
with open(file_path, mode="w", encoding="utf-8") as file:
|
|
68
68
|
file.write(self.report.model_dump_json(indent=2, by_alias=True))
|
|
69
69
|
case self.ExportFileType.PDF:
|
|
70
|
-
|
|
71
|
-
with open(file_path, mode="wb") as file:
|
|
72
|
-
file.write(report_bytes)
|
|
70
|
+
render_report(report=self.report, file_path=file_path)
|
|
73
71
|
|
|
74
72
|
self.app.call_from_thread(
|
|
75
73
|
self.app.notify,
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
"""Works Report Widgets."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import quote, urlparse
|
|
4
6
|
|
|
7
|
+
import httpx
|
|
5
8
|
from rich.console import RenderableType
|
|
6
9
|
from rich.table import Table
|
|
7
10
|
from rich.text import Text
|
|
8
|
-
from textual import events, on
|
|
11
|
+
from textual import events, on, work
|
|
9
12
|
from textual.app import ComposeResult
|
|
10
|
-
from textual.containers import Horizontal, VerticalScroll
|
|
11
|
-
from textual.widgets import Label, Static, TabbedContent, TabPane
|
|
13
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
14
|
+
from textual.widgets import Button, Label, Static, TabbedContent, TabPane
|
|
12
15
|
|
|
13
16
|
from pub_analyzer.models.author import Author
|
|
14
17
|
from pub_analyzer.models.report import AuthorReport, CitationReport, CitationType, InstitutionReport, WorkReport
|
|
15
|
-
from pub_analyzer.
|
|
18
|
+
from pub_analyzer.models.work import Location
|
|
19
|
+
from pub_analyzer.widgets.common import FileSystemSelector, Input, Modal, ReactiveLabel, Select
|
|
16
20
|
from pub_analyzer.widgets.report.cards import (
|
|
17
21
|
AuthorshipCard,
|
|
18
22
|
CitationMetricsCard,
|
|
@@ -21,6 +25,7 @@ from pub_analyzer.widgets.report.cards import (
|
|
|
21
25
|
ReportCitationMetricsCard,
|
|
22
26
|
WorksTypeSummaryCard,
|
|
23
27
|
)
|
|
28
|
+
from pub_analyzer.widgets.report.editor import EditWidget
|
|
24
29
|
|
|
25
30
|
from .concept import ConceptsTable
|
|
26
31
|
from .grants import GrantsTable
|
|
@@ -80,12 +85,133 @@ class CitedByTable(Static):
|
|
|
80
85
|
yield Static(citations_table, classes="citations-table")
|
|
81
86
|
|
|
82
87
|
|
|
88
|
+
class DownloadPane(VerticalScroll):
|
|
89
|
+
"""Download Work pane widget."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, work_report: WorkReport, locations: list[Location]) -> None:
|
|
92
|
+
self.work_report = work_report
|
|
93
|
+
self.locations = locations
|
|
94
|
+
super().__init__()
|
|
95
|
+
|
|
96
|
+
@on(FileSystemSelector.FileSelected)
|
|
97
|
+
def enable_button(self, event: FileSystemSelector.FileSelected) -> None:
|
|
98
|
+
"""Enable button on file select."""
|
|
99
|
+
if event.file_selected:
|
|
100
|
+
self.query_one(Button).disabled = False
|
|
101
|
+
else:
|
|
102
|
+
self.query_one(Button).disabled = True
|
|
103
|
+
|
|
104
|
+
@on(Button.Pressed, "#export-report-button")
|
|
105
|
+
async def export_report(self) -> None:
|
|
106
|
+
"""Handle export report button."""
|
|
107
|
+
export_path = self.query_one(FileSystemSelector).path_selected
|
|
108
|
+
file_name = self.query_one(Input).value
|
|
109
|
+
pdf_url = self.query_one(Select).value
|
|
110
|
+
|
|
111
|
+
if export_path and file_name:
|
|
112
|
+
file_path = export_path.joinpath(file_name)
|
|
113
|
+
self.download_work(file_path=file_path, pdf_url=pdf_url)
|
|
114
|
+
self.query_one(Button).disabled = True
|
|
115
|
+
|
|
116
|
+
@work(exclusive=True)
|
|
117
|
+
async def download_work(self, file_path: pathlib.Path, pdf_url: str) -> None:
|
|
118
|
+
"""Download PDF."""
|
|
119
|
+
async with httpx.AsyncClient() as client:
|
|
120
|
+
try:
|
|
121
|
+
self.log.info(f"Starting downloading: {pdf_url}")
|
|
122
|
+
response = await client.get(url=pdf_url, timeout=300, follow_redirects=True)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
with open(file_path, mode="wb") as f:
|
|
125
|
+
f.write(response.content)
|
|
126
|
+
|
|
127
|
+
self.app.notify(
|
|
128
|
+
title="PDF downloaded successfully!",
|
|
129
|
+
message=f"The file was downloaded successfully. You can go see it at [i]{file_path}[/]",
|
|
130
|
+
timeout=20.0,
|
|
131
|
+
)
|
|
132
|
+
except httpx.RequestError:
|
|
133
|
+
self.app.notify(
|
|
134
|
+
title="Network problems!",
|
|
135
|
+
message="An error occurred while requesting. Please check your connection and try again.",
|
|
136
|
+
severity="error",
|
|
137
|
+
timeout=20.0,
|
|
138
|
+
)
|
|
139
|
+
except httpx.HTTPStatusError as exec:
|
|
140
|
+
status_code = exec.response.status_code
|
|
141
|
+
title = f"HTTP Error! Status {status_code} ({httpx.codes.get_reason_phrase(status_code)})."
|
|
142
|
+
|
|
143
|
+
if status_code == httpx.codes.FORBIDDEN:
|
|
144
|
+
msg = (
|
|
145
|
+
"Sometimes servers forbid robots from accessing their websites."
|
|
146
|
+
+ "Try to download it from your browser using the following link: "
|
|
147
|
+
+ f"""[@click=app.open_link('{quote(pdf_url)}')][u]{pdf_url}[/u][/]"""
|
|
148
|
+
)
|
|
149
|
+
self.app.notify(
|
|
150
|
+
title=title,
|
|
151
|
+
message=msg,
|
|
152
|
+
severity="error",
|
|
153
|
+
timeout=30.0,
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
msg = (
|
|
157
|
+
"Try to download it from your browser using the following link: "
|
|
158
|
+
+ f"""[@click=app.open_link('{quote(pdf_url)}')][u]{pdf_url}[/u][/]"""
|
|
159
|
+
)
|
|
160
|
+
self.app.notify(
|
|
161
|
+
title=title,
|
|
162
|
+
message=msg,
|
|
163
|
+
severity="error",
|
|
164
|
+
timeout=30.0,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def safe_filename(self, title: str) -> str:
|
|
168
|
+
"""Create a safe filename."""
|
|
169
|
+
no_tags = re.sub(r"<[^>]+>", "", title)
|
|
170
|
+
hyphenated = re.sub(r"\s+", "-", no_tags.strip())
|
|
171
|
+
safe = re.sub(r"[^a-zA-Z0-9\-_]", "", hyphenated)
|
|
172
|
+
return safe[:20]
|
|
173
|
+
|
|
174
|
+
def compose(self) -> ComposeResult:
|
|
175
|
+
"""Compose content pane."""
|
|
176
|
+
filename = self.safe_filename(self.work_report.work.title)
|
|
177
|
+
suggest_file_name = f"{filename}.pdf"
|
|
178
|
+
with Vertical(id="export-form"):
|
|
179
|
+
with Vertical(classes="export-form-input-container"):
|
|
180
|
+
yield Label("[b]Name File:[/]", classes="export-form-label")
|
|
181
|
+
|
|
182
|
+
with Horizontal(classes="file-selector-container"):
|
|
183
|
+
options = []
|
|
184
|
+
for location in self.locations:
|
|
185
|
+
if location.source:
|
|
186
|
+
options.append((location.source.display_name, location.pdf_url))
|
|
187
|
+
else:
|
|
188
|
+
hostname = str(urlparse(location.pdf_url).hostname)
|
|
189
|
+
options.append((hostname, location.pdf_url))
|
|
190
|
+
|
|
191
|
+
yield Input(value=suggest_file_name, placeholder="work.pdf", classes="export-form-input")
|
|
192
|
+
yield Select(
|
|
193
|
+
options=options,
|
|
194
|
+
allow_blank=False,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
with Vertical(classes="export-form-input-container"):
|
|
198
|
+
yield Label("[b]Export Directory:[/]", classes="export-form-label")
|
|
199
|
+
yield FileSystemSelector(path=pathlib.Path.home(), only_dir=True)
|
|
200
|
+
|
|
201
|
+
with Horizontal(classes="export-form-buttons"):
|
|
202
|
+
yield Button("Download", variant="primary", disabled=True, id="export-report-button")
|
|
203
|
+
|
|
204
|
+
|
|
83
205
|
class WorkModal(Modal[None]):
|
|
84
206
|
"""Summary of the statistics of a work."""
|
|
85
207
|
|
|
86
208
|
def __init__(self, work_report: WorkReport, author: Author | None) -> None:
|
|
87
209
|
self.work_report = work_report
|
|
88
210
|
self.author = author
|
|
211
|
+
|
|
212
|
+
locations = self.work_report.work.locations
|
|
213
|
+
self.locations_with_pdf_available = [location for location in locations if location.pdf_url]
|
|
214
|
+
|
|
89
215
|
super().__init__()
|
|
90
216
|
|
|
91
217
|
@on(events.Key)
|
|
@@ -114,7 +240,15 @@ class WorkModal(Modal[None]):
|
|
|
114
240
|
# Abstract if exists
|
|
115
241
|
if self.work_report.work.abstract:
|
|
116
242
|
with TabPane("Abstract"):
|
|
117
|
-
|
|
243
|
+
label = ReactiveLabel(self.work_report.work.abstract, classes="abstract")
|
|
244
|
+
yield label
|
|
245
|
+
yield EditWidget(
|
|
246
|
+
display_name="abstract",
|
|
247
|
+
field_name="abstract",
|
|
248
|
+
model=self.work_report.work,
|
|
249
|
+
widget=label,
|
|
250
|
+
widget_field="renderable",
|
|
251
|
+
)
|
|
118
252
|
# Citations Table
|
|
119
253
|
with TabPane("Cited By Works"):
|
|
120
254
|
if len(self.work_report.cited_by):
|
|
@@ -145,6 +279,11 @@ class WorkModal(Modal[None]):
|
|
|
145
279
|
yield TopicsTable(self.work_report.work.topics)
|
|
146
280
|
else:
|
|
147
281
|
yield Label("No Topics found.")
|
|
282
|
+
# Download
|
|
283
|
+
location = self.work_report.work.best_oa_location
|
|
284
|
+
if location and location.pdf_url:
|
|
285
|
+
with TabPane("Download"):
|
|
286
|
+
yield DownloadPane(work_report=self.work_report, locations=self.locations_with_pdf_available)
|
|
148
287
|
|
|
149
288
|
|
|
150
289
|
class WorksTable(Static):
|
|
@@ -49,12 +49,7 @@ class AuthorResultWidget(ResultWidget):
|
|
|
49
49
|
from pub_analyzer.widgets.body import MainContent
|
|
50
50
|
|
|
51
51
|
author_summary_widget = AuthorSummaryWidget(author_result=self.author_result)
|
|
52
|
-
|
|
53
|
-
main_content = self.app.query_one(MainContent)
|
|
54
|
-
main_content.update_title(title=self.author_result.display_name)
|
|
55
|
-
await main_content.mount(author_summary_widget)
|
|
56
|
-
|
|
57
|
-
await self.app.query_one("FinderWidget").remove()
|
|
52
|
+
self.post_message(MainContent.UpdateMainContent(new_widget=author_summary_widget, title=self.author_result.display_name))
|
|
58
53
|
|
|
59
54
|
|
|
60
55
|
class InstitutionResultWidget(ResultWidget):
|
|
@@ -84,9 +79,4 @@ class InstitutionResultWidget(ResultWidget):
|
|
|
84
79
|
from pub_analyzer.widgets.body import MainContent
|
|
85
80
|
|
|
86
81
|
institution_summary_widget = InstitutionSummaryWidget(institution_result=self.institution_result)
|
|
87
|
-
|
|
88
|
-
main_content = self.app.query_one(MainContent)
|
|
89
|
-
main_content.update_title(title=self.institution_result.display_name)
|
|
90
|
-
await main_content.mount(institution_summary_widget)
|
|
91
|
-
|
|
92
|
-
await self.app.query_one("FinderWidget").remove()
|
|
82
|
+
self.post_message(MainContent.UpdateMainContent(new_widget=institution_summary_widget, title=self.institution_result.display_name))
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pub-analyzer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A text user interface, written in python, which automates the generation of scientific production reports using OpenAlex
|
|
5
|
-
Home-page: https://github.com/alejandrgaspar/pub-analyzer
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Alejandro Gaspar
|
|
8
7
|
Author-email: alejandro@gaspar.land
|
|
@@ -23,12 +22,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
24
|
Classifier: Typing :: Typed
|
|
26
|
-
Requires-Dist: httpx (==0.
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist: typst (==0.11.1)
|
|
25
|
+
Requires-Dist: httpx[http2] (==0.28.1)
|
|
26
|
+
Requires-Dist: pydantic (==2.11.7)
|
|
27
|
+
Requires-Dist: textual (==0.85.2)
|
|
28
|
+
Requires-Dist: typst (==0.13.2)
|
|
31
29
|
Project-URL: Documentation, https://pub-analyzer.com/
|
|
30
|
+
Project-URL: Homepage, https://github.com/alejandrgaspar/pub-analyzer
|
|
32
31
|
Project-URL: Repository, https://github.com/alejandrgaspar/pub-analyzer
|
|
33
32
|
Description-Content-Type: text/markdown
|
|
34
33
|
|
|
@@ -4,26 +4,24 @@ pub_analyzer/css/buttons.tcss,sha256=FruJ39dXmKnZm3_y0CxAJByKHrbmt6RQky0T0uM906g
|
|
|
4
4
|
pub_analyzer/css/checkbox.tcss,sha256=FblyIHns-r1K0ikOnSJtoTMz57C6iDEcscdFAsJ7s48,506
|
|
5
5
|
pub_analyzer/css/collapsible.tcss,sha256=Rh-L5PcIMhnZ7RhY1udd_BcYC1mfCMew2m6o6ty3juE,605
|
|
6
6
|
pub_analyzer/css/datatable.tcss,sha256=JgdMUPc4fYmZlXi_FxbuD88pegK6Pi4FgDHIfA_TKxo,994
|
|
7
|
+
pub_analyzer/css/editor.tcss,sha256=1IzL92r7sJlnzIJbrTuEGyB16jFCCcAZ9eDDECCZGLc,1198
|
|
7
8
|
pub_analyzer/css/main.tcss,sha256=rtHLKCVZaBuJ83dAg840cetzVtEy8Rjbw1UKxgafeE8,1027
|
|
8
|
-
pub_analyzer/css/report.tcss,sha256=
|
|
9
|
+
pub_analyzer/css/report.tcss,sha256=y3-N0mgfyGp4X02LRwFwhxMXMuY7oDsoA9tLDEaUNm8,2047
|
|
9
10
|
pub_analyzer/css/search.tcss,sha256=rovbWjp4pYfCF_OyAC_QrV_0WdMUlsYoQ3vbs9pGw7g,1326
|
|
10
11
|
pub_analyzer/css/summary.tcss,sha256=i4ixICwoQFj2BToW9NVmJGUIYk5upbukbTCnDgT40ds,1350
|
|
11
12
|
pub_analyzer/css/tabs.tcss,sha256=dS7y6ZZmo1Vw7Wqpx66-O-oE7zeqPE9reWqIhQ1KcZs,311
|
|
12
13
|
pub_analyzer/css/tree.tcss,sha256=5BSabX9ZmRL3VTz0Gya2RRJnWrwdIF9cTf6dXj2R4kE,818
|
|
13
14
|
pub_analyzer/internal/__init__.py,sha256=9aqrBJDedUiBO5kEO81kSAuPbOSFoaDZZK8w5NydPhs,22
|
|
14
15
|
pub_analyzer/internal/identifier.py,sha256=LDYew25TLuwqJHmLg9iRNTURWynN27ZbTxTVGbuOUD0,2939
|
|
15
|
-
pub_analyzer/internal/
|
|
16
|
-
pub_analyzer/internal/
|
|
17
|
-
pub_analyzer/internal/
|
|
18
|
-
pub_analyzer/internal/templates/
|
|
19
|
-
pub_analyzer/
|
|
20
|
-
pub_analyzer/internal/templates/author/works.typ,sha256=ZhJ3JhyDc4Z5JE3kLmyFJzfSNZdQi-RVlA57SxDBKIg,5132
|
|
21
|
-
pub_analyzer/internal/templates/author/works_extended.typ,sha256=Eh_1BONK3BArWvlo3MNQJ8kueOpW_N8PlxBTa-Guhkg,3510
|
|
22
|
-
pub_analyzer/main.py,sha256=tvGLwYHW-5hysM9sfaU0Il563p9zYcl6Kege--sP7hk,2427
|
|
16
|
+
pub_analyzer/internal/limiter.py,sha256=1YaVBSSG7IfFg0nhD_up21NNL_H2Q4qaIQTvZS674Vo,1002
|
|
17
|
+
pub_analyzer/internal/render.py,sha256=uF1LsY39UkTpkTJgU4hyYnVv6b1MCQayubrPwrGW2DI,1271
|
|
18
|
+
pub_analyzer/internal/report.py,sha256=RnX3EELW33ABwEu1W506_0q7gWQyF6Rcds-YAbXYlow,18046
|
|
19
|
+
pub_analyzer/internal/templates/author_report.typ,sha256=XdqPmBptlC46vDORDFs-YaILehWh7lDuCo0cbyMPGHo,16927
|
|
20
|
+
pub_analyzer/main.py,sha256=0iNj4cggG-HJ8FMODwZ67Yp3-GaFPw-gUEcSCCzwMcc,2332
|
|
23
21
|
pub_analyzer/models/__init__.py,sha256=hvR6m379slQw7gSwnl_OFY21Ytv90mmmOe7bp8vZYkk,59
|
|
24
22
|
pub_analyzer/models/author.py,sha256=NvFmvSsmnchz2lo7m69NE3kjLYP0CXICRAolnvcznW8,2118
|
|
25
23
|
pub_analyzer/models/concept.py,sha256=yNvajKWTn6uBalNoJmlobitvbFBOjF80jlZnjKjwDRw,677
|
|
26
|
-
pub_analyzer/models/institution.py,sha256=
|
|
24
|
+
pub_analyzer/models/institution.py,sha256=Ur9Prf4ljNEo4dVdhedMjKv4qak7LiDbJ4tD7CEPvVQ,3175
|
|
27
25
|
pub_analyzer/models/report.py,sha256=yma70xpD6I8hv_kk5ylMhylLu60eOpQwNEJa-Dd-XQI,2672
|
|
28
26
|
pub_analyzer/models/source.py,sha256=o3ich4iDYB_PH_cVbrZtVRFVLQlPS3W5ajgBQQGzYqM,2730
|
|
29
27
|
pub_analyzer/models/topic.py,sha256=3MBQV-njnjfmOVvgmFZxy8fFU7sMj5yxUW8EHFAjlD4,1825
|
|
@@ -32,13 +30,14 @@ pub_analyzer/widgets/__init__.py,sha256=JALs1yGE06XYwjoY_0AG-Wt_pMknI1WEWNYK3atQ
|
|
|
32
30
|
pub_analyzer/widgets/author/__init__.py,sha256=oiJibt7YiuGpovOnFIAlC9YwLO-0LN3SDgPWFL-LVPQ,22
|
|
33
31
|
pub_analyzer/widgets/author/cards.py,sha256=JWZxYy4Oen5fToiSBgvfEgmBJlrIVXCWpT-XjkLbxY4,2445
|
|
34
32
|
pub_analyzer/widgets/author/core.py,sha256=9i3U7jSeyQdAdWAslQUMd0_juw3yy9s4mEgl-nJ1LVg,5068
|
|
35
|
-
pub_analyzer/widgets/author/tables.py,sha256=
|
|
36
|
-
pub_analyzer/widgets/body.py,sha256=
|
|
37
|
-
pub_analyzer/widgets/common/__init__.py,sha256=
|
|
33
|
+
pub_analyzer/widgets/author/tables.py,sha256=LmICDSd-qdQvpqx5GVRGxeYte3usfQENIRrP7RWQMCY,1884
|
|
34
|
+
pub_analyzer/widgets/body.py,sha256=BvNDe63FE0-KLl89SlhDXtALlLZX4cRqedEG55urTrM,1656
|
|
35
|
+
pub_analyzer/widgets/common/__init__.py,sha256=a65SpyN6RPKA8caSEyDm8mIzW_r4e0LdQGjJaLk07v8,341
|
|
38
36
|
pub_analyzer/widgets/common/card.py,sha256=GGSaeuZt6AqY7kAvcVnWNMrhNPzr7do66YRQOYNSYvU,595
|
|
39
37
|
pub_analyzer/widgets/common/filesystem.py,sha256=i0S3D6JJzPkF1Sqm83SSQlmYFKRf82SnoFgKVE6BdYI,6460
|
|
40
38
|
pub_analyzer/widgets/common/filters.py,sha256=bdtWaxahbFksaZZf6l0Yhgi9opH_RygFHXQV4_CYYj0,3372
|
|
41
39
|
pub_analyzer/widgets/common/input.py,sha256=tK_UCtLDGHlI_NKpKjGkVu4gWiwMAIHixT9Im--Un4c,2649
|
|
40
|
+
pub_analyzer/widgets/common/label.py,sha256=03tl0RdayTeSixdRu4Fyt-jU1cDrI99iQww0hEXahCA,1091
|
|
42
41
|
pub_analyzer/widgets/common/modal.py,sha256=otLQZotdTRTlSeTBknIxqRyduVY6lRZ5yW5u20SLcwI,882
|
|
43
42
|
pub_analyzer/widgets/common/selector.py,sha256=Jh5bsn-zYmHGfEE3eO9XL6BsgKpLMGfg8FJur4gQmH0,1493
|
|
44
43
|
pub_analyzer/widgets/common/summary.py,sha256=Qj-FRfAVgJmCaVUJI-jQrHX2sGKHTP2b75KukuJWlog,165
|
|
@@ -50,20 +49,21 @@ pub_analyzer/widgets/report/__init__.py,sha256=oolRVss3JKaQHaQVDncjtxbLINRJ5Rd1u
|
|
|
50
49
|
pub_analyzer/widgets/report/author.py,sha256=IEfRDfsA8jcmFwQQk1O-iuh8MKr4DbzBPpjoE8xECZA,1459
|
|
51
50
|
pub_analyzer/widgets/report/cards.py,sha256=2jf9cpfzVFZO0I9b29bkNaVhENMnfL26etEpUG-NMk0,4854
|
|
52
51
|
pub_analyzer/widgets/report/concept.py,sha256=xiGXy_RXO_XmdqnlePkOozYPmQrsDdqKPMRXHsZbDP0,1485
|
|
53
|
-
pub_analyzer/widgets/report/core.py,sha256=
|
|
54
|
-
pub_analyzer/widgets/report/
|
|
52
|
+
pub_analyzer/widgets/report/core.py,sha256=Bgy_fK-IwGjoIidcr687xXsHzN3LEml-A3ykyXNeVW8,11704
|
|
53
|
+
pub_analyzer/widgets/report/editor.py,sha256=WlhjNQCrqeot2rvV1266Vr8yDYJQLL1lJ1XY040UoJI,2768
|
|
54
|
+
pub_analyzer/widgets/report/export.py,sha256=as2yM2FXsqgvMnF4KVWVuxboULXqJ62v7wzMYek23s4,4633
|
|
55
55
|
pub_analyzer/widgets/report/grants.py,sha256=m183W6djVhucAuYs-EhjkHuA9heqpGwsW_iRouVQsns,1347
|
|
56
56
|
pub_analyzer/widgets/report/institution.py,sha256=PDPE9fK18l9kKKch5sJrbnHHDss0kJ6bgVhM4hTyrAo,1297
|
|
57
57
|
pub_analyzer/widgets/report/locations.py,sha256=s6O5v_jX_oPsKOf2fEujtDxLHQRVsqrIcgN4rZkRKkg,2892
|
|
58
58
|
pub_analyzer/widgets/report/source.py,sha256=WJhJc0_sZOcAtkmh9-VjbgugoArZgxKoXlITqVaBYK0,3045
|
|
59
59
|
pub_analyzer/widgets/report/topic.py,sha256=SI3STTBFlpR-VJcsNhJyu6vc9uyytU_ASKuWXb-qr60,1969
|
|
60
|
-
pub_analyzer/widgets/report/work.py,sha256=
|
|
61
|
-
pub_analyzer/widgets/search/__init__.py,sha256=
|
|
60
|
+
pub_analyzer/widgets/report/work.py,sha256=fKhnkbT9GhKqz31qYTtLtOiNbi9aCDRC8NiQtuWxS7M,15874
|
|
61
|
+
pub_analyzer/widgets/search/__init__.py,sha256=90L9IghqXD2jAWBKWK6-UeHLSVlci7D3_OGjFSSRgEs,239
|
|
62
62
|
pub_analyzer/widgets/search/core.py,sha256=4NvowtBcrH1fmob9kuF7v9Tq3Nd99jzB2S7xaD8OYeI,3861
|
|
63
|
-
pub_analyzer/widgets/search/results.py,sha256=
|
|
63
|
+
pub_analyzer/widgets/search/results.py,sha256=3ko7zcToGp9MV-mzz_9uTJxSec7IozlIWDZe7QeRmj0,3703
|
|
64
64
|
pub_analyzer/widgets/sidebar.py,sha256=XlIshlCVW5Bb3MXFPnU9is0qQrUrGdT6xlkKiYNEcAM,2704
|
|
65
|
-
pub_analyzer-0.
|
|
66
|
-
pub_analyzer-0.
|
|
67
|
-
pub_analyzer-0.
|
|
68
|
-
pub_analyzer-0.
|
|
69
|
-
pub_analyzer-0.
|
|
65
|
+
pub_analyzer-0.5.1.dist-info/LICENSE,sha256=OPopoEowTMKqIea8Kbxk3TKdCQ97YkLvIknjTHE5oCI,1080
|
|
66
|
+
pub_analyzer-0.5.1.dist-info/METADATA,sha256=ebqDbZ41qqzCXNSHqmxabArEXoEcWsmx9BNOML_r8Mk,4547
|
|
67
|
+
pub_analyzer-0.5.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
68
|
+
pub_analyzer-0.5.1.dist-info/entry_points.txt,sha256=mVb_gUNX_-aVWHlNKLjcMAS8YLgNnSq9JLRXVJGIF2c,54
|
|
69
|
+
pub_analyzer-0.5.1.dist-info/RECORD,,
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Author Summary
|
|
2
|
-
= Author.
|
|
3
|
-
|
|
4
|
-
#let summary-card(title: "Title", body) = {
|
|
5
|
-
return block(
|
|
6
|
-
width: 100%,
|
|
7
|
-
height: 150pt,
|
|
8
|
-
fill: rgb("e5e7eb"),
|
|
9
|
-
stroke: 1pt,
|
|
10
|
-
radius: 2pt,
|
|
11
|
-
)[
|
|
12
|
-
#v(20pt)
|
|
13
|
-
#align(center)[#text(size: 12pt)[#title]]
|
|
14
|
-
#v(5pt)
|
|
15
|
-
#block(width: 100%, inset: (x: 20pt))[#body]
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Cards
|
|
20
|
-
#grid(
|
|
21
|
-
columns: (1fr, 1fr, 1fr),
|
|
22
|
-
column-gutter: 15pt,
|
|
23
|
-
[
|
|
24
|
-
// Last institution.
|
|
25
|
-
#summary-card(title:"Last institution:")[
|
|
26
|
-
{% if report.author.last_known_institutions%}
|
|
27
|
-
{% set last_known_institution = report.author.last_known_institutions[0] %}
|
|
28
|
-
#grid(
|
|
29
|
-
rows: auto, row-gutter: 10pt,
|
|
30
|
-
|
|
31
|
-
[*Name:* {{ last_known_institution.display_name }}],
|
|
32
|
-
[*Country:* {{ last_known_institution.country_code }}],
|
|
33
|
-
[*Type:* {{ last_known_institution.type.value|capitalize }}],
|
|
34
|
-
)
|
|
35
|
-
{% endif %}
|
|
36
|
-
]
|
|
37
|
-
],
|
|
38
|
-
[
|
|
39
|
-
// Author identifiers.
|
|
40
|
-
#summary-card(title:"Identifiers:")[
|
|
41
|
-
#grid(
|
|
42
|
-
rows: auto, row-gutter: 10pt,
|
|
43
|
-
{% for key, value in report.author.ids.model_dump().items() %}
|
|
44
|
-
{% if value %}
|
|
45
|
-
[- #underline( [#link("{{ value }}")[{{ key }}]] )],
|
|
46
|
-
{% endif %}
|
|
47
|
-
{% endfor %}
|
|
48
|
-
)
|
|
49
|
-
]
|
|
50
|
-
],
|
|
51
|
-
[
|
|
52
|
-
// Citation metrics.
|
|
53
|
-
#summary-card(title: "Citation metrics:")[
|
|
54
|
-
#grid(
|
|
55
|
-
rows: auto, row-gutter: 10pt,
|
|
56
|
-
|
|
57
|
-
[*2-year mean:* {{ report.author.summary_stats.two_yr_mean_citedness|round(5) }}],
|
|
58
|
-
[*h-index:* {{ report.author.summary_stats.h_index }}],
|
|
59
|
-
[*i10 index:* {{ report.author.summary_stats.i10_index }}]
|
|
60
|
-
)
|
|
61
|
-
]
|
|
62
|
-
],
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
#v(10pt)
|
|
66
|
-
#align(center, text(11pt)[_Counts by year_])
|
|
67
|
-
#grid(
|
|
68
|
-
columns: (1fr, 1fr),
|
|
69
|
-
column-gutter: 15pt,
|
|
70
|
-
align: (auto, horizon),
|
|
71
|
-
|
|
72
|
-
[
|
|
73
|
-
#table(
|
|
74
|
-
columns: (1fr, 2fr, 2fr),
|
|
75
|
-
inset: 8pt,
|
|
76
|
-
align: horizon,
|
|
77
|
-
// Headers
|
|
78
|
-
[*Year*], [*Works count*], [*Cited by count*],
|
|
79
|
-
|
|
80
|
-
// Content
|
|
81
|
-
{% set max_year_count = 0 %}
|
|
82
|
-
{% for year_count in report.author.counts_by_year[:8] %}
|
|
83
|
-
[{{ year_count.year }}], [{{ year_count.works_count }}], [{{ year_count.cited_by_count }}],
|
|
84
|
-
{% set max_year_count = year_count %}
|
|
85
|
-
{% endfor %}
|
|
86
|
-
)
|
|
87
|
-
],
|
|
88
|
-
grid.cell(
|
|
89
|
-
inset: (x: 10pt, bottom: 10pt, top: 2.5pt),
|
|
90
|
-
stroke: 1pt
|
|
91
|
-
)[
|
|
92
|
-
#align(center, text(10pt)[Cites by year])
|
|
93
|
-
#v(5pt)
|
|
94
|
-
#canvas(length: 100%, {
|
|
95
|
-
plot.plot(
|
|
96
|
-
size: (0.90, 0.48),
|
|
97
|
-
axis-style: "scientific-auto",
|
|
98
|
-
plot-style: (stroke: (1pt + PALETTE.at(0)),),
|
|
99
|
-
x-min: auto, x-max: auto,
|
|
100
|
-
x-tick-step: 1, y-tick-step: auto,
|
|
101
|
-
x-label: none, y-label: none,
|
|
102
|
-
{
|
|
103
|
-
plot.add((
|
|
104
|
-
{% for year_count in report.author.counts_by_year[:8] %}
|
|
105
|
-
({{ year_count.year }}, {{ year_count.cited_by_count }}),
|
|
106
|
-
{% endfor %}
|
|
107
|
-
))
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
]
|
|
111
|
-
)
|
|
112
|
-
#pagebreak()
|