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.

@@ -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
- async def _export_report(self, file_type: ExportFileType, file_path: pathlib.Path) -> None:
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
- 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)
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
- from urllib.parse import quote
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.widgets.common import Modal
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
- yield Label(self.work_report.work.abstract, classes="abstract")
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):
@@ -4,8 +4,8 @@ from .core import FinderWidget, SearchBar
4
4
  from .results import AuthorResultWidget, InstitutionResultWidget
5
5
 
6
6
  __all__ = [
7
- "FinderWidget",
8
- "SearchBar",
9
7
  "AuthorResultWidget",
8
+ "FinderWidget",
10
9
  "InstitutionResultWidget",
10
+ "SearchBar",
11
11
  ]
@@ -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
1
+ Metadata-Version: 2.3
2
2
  Name: pub-analyzer
3
- Version: 0.4.3
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.2)
27
- Requires-Dist: jinja2 (==3.1.4)
28
- Requires-Dist: pydantic (==2.9.2)
29
- Requires-Dist: textual (==0.70.0)
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=H4lHe3yepGLRlDtQkXAx_ONCwCupacglt6j3KWQOf2Q,2027
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/render.py,sha256=gq5dScWs507tne3glJbTQ-PekmALvtcZPB1p_mVAkE0,2144
16
- pub_analyzer/internal/report.py,sha256=7T4TTe6ASQnOku0F9YVOGn3G3HmXnad0CVXvGEYQnSA,16322
17
- pub_analyzer/internal/templates/author/author_summary.typ,sha256=nIECVqTcW_ZjHX6G8Jets_Wrg8Z1Tmm7Kz5RLD46wlQ,2836
18
- pub_analyzer/internal/templates/author/report.typ,sha256=rlAa5hf2Ms6V3pnlVsu3aosmCwtGq3qtRjbPY9qczNA,2614
19
- pub_analyzer/internal/templates/author/sources.typ,sha256=lQUorqZRMNI4v8P4xLvtqWy89-fbjciUturI3HrfBPo,798
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=62T9raQYXlEk0wlAj2OJgBy_LnOF7_LUyX8iGywgpOs,3133
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=6ZL8B0X0rCpyu_QoAEH0NVvAtTVbydXte3vQIiDui1k,1807
36
- pub_analyzer/widgets/body.py,sha256=wN9cMcm1MaRTjuHYt8RWrG8D_ngg5cn-hVllvmzPX_o,972
37
- pub_analyzer/widgets/common/__init__.py,sha256=Fx5Gl17Rd_wueZjNElBtI1kCn-4DMSbC3lEA8u7PSto,287
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=V4fwBHnSqkUzC1da8sGYqzsPKQRofA7GqpEhbEXsk4s,11436
54
- pub_analyzer/widgets/report/export.py,sha256=EQzF5fMZgHtLv3f5hITDgf9WW2XytRX_foeLWwcIHkM,4762
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=t1Z2oFB5gUIuCdUqp_vkTV9--oUfnOX-yhlCDVayla4,9592
61
- pub_analyzer/widgets/search/__init__.py,sha256=8C3IQtFkiIL8hlQbhJ_fAHM59-TAoe29wBAM2ptozhw,239
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=veZP5FxWFKKArH6wvxtvdu07BEYGlT7qwbRjDpgXxtU,3931
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.4.3.dist-info/LICENSE,sha256=OPopoEowTMKqIea8Kbxk3TKdCQ97YkLvIknjTHE5oCI,1080
66
- pub_analyzer-0.4.3.dist-info/METADATA,sha256=kmOal3d94yzhnYLF3ZVZHqKEyXgZROwWszNZuEhR5pk,4559
67
- pub_analyzer-0.4.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
68
- pub_analyzer-0.4.3.dist-info/entry_points.txt,sha256=mVb_gUNX_-aVWHlNKLjcMAS8YLgNnSq9JLRXVJGIF2c,54
69
- pub_analyzer-0.4.3.dist-info/RECORD,,
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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()