pub-analyzer 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pub-analyzer might be problematic. Click here for more details.

@@ -0,0 +1,60 @@
1
+ /* COLORS */
2
+ $bg-main-color: white;
3
+ $bg-secondary-color: #e5e7eb;
4
+ $bg-secondary-color-accent: #d1d5db;
5
+ $text-primary-color: black;
6
+
7
+ $bg-main-color-darken: #1e293b;
8
+ $bg-secondary-color-darken: #0f172a;
9
+ $text-primary-color-darken: black;
10
+
11
+ $primary-color: #b91c1c;
12
+ $primary-color-accent: #991b1b;
13
+ $primary-color-highlight: #dc2626;
14
+
15
+ TextEditor {
16
+ #dialog {
17
+ margin: 0 10;
18
+ min-height: 10vh;
19
+ max-height: 60vh;
20
+ }
21
+
22
+ #text-editor-container {
23
+ height: 1fr;
24
+ }
25
+
26
+ TextArea{
27
+ height: auto;
28
+ padding: 1 3;
29
+
30
+ background: $bg-main-color;
31
+ border: none;
32
+
33
+ .text-area--cursor {
34
+ background: $primary-color;
35
+ }
36
+ .text-area--cursor-gutter {
37
+ color: $bg-main-color;
38
+ background: $primary-color-accent;
39
+ }
40
+ .text-area--cursor-line {
41
+ background: $bg-main-color;
42
+ }
43
+ .text-area--matching-bracket {
44
+ background: $primary-color-highlight 30%;
45
+ }
46
+
47
+ }
48
+
49
+ #actions-buttons {
50
+ height: 3;
51
+ margin-top: 1;
52
+ margin-bottom: 2;
53
+
54
+ align: center middle;
55
+
56
+ Button {
57
+ margin: 0 5;
58
+ }
59
+ }
60
+ }
@@ -67,29 +67,29 @@ LoadReportWidget .button-container {
67
67
  }
68
68
 
69
69
  /* Export Report Pane */
70
- ExportReportPane #export-form {
70
+ #export-form {
71
71
  height: auto;
72
72
  }
73
73
 
74
- ExportReportPane .export-form-input-container {
74
+ .export-form-input-container {
75
75
  height: auto;
76
76
  margin-bottom: 2;
77
77
  }
78
78
 
79
- ExportReportPane .export-form-label {
79
+ .export-form-label {
80
80
  width: 25vw;
81
81
  border-bottom: solid $text-primary-color;
82
82
  }
83
83
 
84
- ExportReportPane .file-selector-container {
84
+ .file-selector-container {
85
85
  height: 3;
86
86
  }
87
87
 
88
- ExportReportPane .export-form-input {
88
+ .export-form-input {
89
89
  width: 50vw;
90
90
  }
91
91
 
92
- ExportReportPane .export-form-buttons {
92
+ .export-form-buttons {
93
93
  align: center middle;
94
94
  height: 3;
95
95
  }
@@ -113,6 +113,15 @@ WorkModal #dialog .abstract {
113
113
  padding: 1 2;
114
114
  }
115
115
 
116
+ WorkModal TabPane EditWidget {
117
+ height: 3;
118
+ margin-top: 1;
119
+
120
+ Horizontal {
121
+ align: center middle;
122
+ }
123
+ }
124
+
116
125
  WorkModal #dialog #tables-container {
117
126
  margin: 1 0;
118
127
  }
@@ -1,68 +1,41 @@
1
1
  """Render reports."""
2
2
 
3
3
  import pathlib
4
+ import time
4
5
  from importlib.metadata import version
5
6
 
6
7
  import typst
7
- from jinja2 import Environment, FileSystemLoader
8
+ from textual import log
8
9
 
9
10
  from pub_analyzer.models.report import AuthorReport, InstitutionReport
10
11
 
11
12
 
12
- async def render_template_report(report: AuthorReport | InstitutionReport) -> str:
13
- """Render report template.
14
-
15
- Render the report to typst format using the templates.
13
+ def render_report(report: AuthorReport | InstitutionReport, file_path: pathlib.Path | None) -> bytes | None:
14
+ """Render report to PDF.
16
15
 
17
16
  Args:
18
17
  report: Report Model.
18
+ file_path: Path to save the compiled file.
19
19
 
20
20
  Returns:
21
- Report in Typst language.
21
+ PDF bytes or None if output file path is defined.
22
22
 
23
23
  Raises:
24
- NotImplementedError: If report is `InstitutionReport` type.
24
+ SyntaxError: If typst compiler syntax error.
25
25
  """
26
26
  if isinstance(report, AuthorReport):
27
- templates_path = pathlib.Path(__file__).parent.resolve().joinpath("templates/author")
27
+ templates_path = pathlib.Path(__file__).parent.resolve().joinpath("templates")
28
+ typst_file = templates_path / "author_report.typ"
28
29
  if isinstance(report, InstitutionReport):
29
30
  raise NotImplementedError
30
31
 
31
- # Render template
32
- env = Environment(loader=FileSystemLoader(searchpath=templates_path), enable_async=True, trim_blocks=True, lstrip_blocks=True)
33
- return await env.get_template("report.typ").render_async(report=report, version=version("pub-analyzer"))
34
-
35
-
36
- async def render_report(report: AuthorReport | InstitutionReport, file_path: pathlib.Path) -> bytes:
37
- """Render report to PDF.
38
-
39
- The specified path is not where the PDF file will be saved. The path is where the typst
40
- file will be created (You can create a temporary path using the `tempfile` package).
41
- This is done in this way because at the moment the typst package can only read the
42
- document to be compiled from a file.
43
-
44
- Args:
45
- report: Report Model.
46
- file_path: Temporary directory for the typst file.
32
+ sys_inputs = {"report": report.model_dump_json(by_alias=True), "version": version("pub-analyzer")}
47
33
 
48
- Returns:
49
- PDF bytes.
50
-
51
- Raises:
52
- SyntaxError: If typst compiler syntax error.
53
- """
54
- template_render = await render_template_report(report=report)
55
-
56
- # Write template to typst file
57
- root = file_path.parent
58
- temp_file = open(root.joinpath(file_path.stem + ".typ"), mode="w", encoding="utf-8")
59
- temp_file.write(template_render)
60
- temp_file.close()
61
-
62
- # Render typst file
63
- pdf_render = typst.compile(temp_file.name)
64
-
65
- if isinstance(pdf_render, bytes):
66
- return pdf_render
34
+ start_time = time.time()
35
+ if file_path:
36
+ result = typst.compile(input=typst_file, output=file_path, sys_inputs=sys_inputs)
67
37
  else:
68
- raise SyntaxError
38
+ result = typst.compile(input=typst_file, sys_inputs=sys_inputs)
39
+
40
+ log.info(f"Typst compile time: {round((time.time() - start_time), 2)} seconds.")
41
+ return result
@@ -6,6 +6,7 @@ from typing import Any, NewType
6
6
 
7
7
  import httpx
8
8
  from pydantic import TypeAdapter
9
+ from textual import log
9
10
 
10
11
  from pub_analyzer.internal import identifier
11
12
  from pub_analyzer.models.author import Author, AuthorOpenAlexKey, AuthorResult, DehydratedAuthor
@@ -138,7 +139,14 @@ def _get_valid_works(works: list[dict[str, Any]]) -> list[dict[str, Any]]:
138
139
  In response, we have chosen to exclude such works at this stage, thus avoiding
139
140
  the need to handle exceptions within the Model validators.
140
141
  """
141
- return [_add_work_abstract(work) for work in works if work["title"] is not None]
142
+ valid_works = []
143
+ for work in works:
144
+ if work["title"] is not None:
145
+ valid_works.append(_add_work_abstract(work))
146
+ else:
147
+ log.warning(f"Discarded work: {work['id']}")
148
+
149
+ return valid_works
142
150
 
143
151
 
144
152
  async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
@@ -156,7 +164,7 @@ async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
156
164
  Raises:
157
165
  httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
158
166
  """
159
- response = await client.get(url=url)
167
+ response = await client.get(url=url, follow_redirects=True)
160
168
  response.raise_for_status()
161
169
 
162
170
  json_response = response.json()
@@ -166,7 +174,7 @@ async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
166
174
  works_data = list(_get_valid_works(json_response["results"]))
167
175
 
168
176
  for page_number in range(1, page_count):
169
- page_result = (await client.get(url + f"&page={page_number + 1}")).json()
177
+ page_result = (await client.get(url + f"&page={page_number + 1}", follow_redirects=True)).json()
170
178
  works_data.extend(_get_valid_works(page_result["results"]))
171
179
 
172
180
  return TypeAdapter(list[Work]).validate_python(works_data)
@@ -185,10 +193,17 @@ async def _get_source(client: httpx.AsyncClient, url: str) -> Source:
185
193
  Raises:
186
194
  httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
187
195
  """
188
- response = await client.get(url=url)
196
+ response = await client.get(url=url, follow_redirects=True)
189
197
  response.raise_for_status()
190
198
 
191
- return Source(**response.json())
199
+ json_response = response.json()
200
+ hp_url = json_response["homepage_url"]
201
+ if isinstance(hp_url, str):
202
+ if not hp_url.startswith(("http", "https")):
203
+ json_response["homepage_url"] = None
204
+ log.warning(f"Discarted source homepage url: {url}")
205
+
206
+ return Source(**json_response)
192
207
 
193
208
 
194
209
  async def make_author_report(
@@ -242,8 +257,11 @@ async def make_author_report(
242
257
  dehydrated_sources: list[DehydratedSource] = []
243
258
 
244
259
  # Getting all works that have cited the author.
245
- for author_work in author_works:
260
+ author_works_count = len(author_works)
261
+ for idx_work, author_work in enumerate(author_works, 1):
246
262
  work_id = identifier.get_work_id(author_work)
263
+ log.info(f"[{work_id}] Work [{idx_work}/{author_works_count}]")
264
+
247
265
  work_authors = _get_authors_list(authorships=author_work.authorships)
248
266
  cited_by_api_url = (
249
267
  f"https://api.openalex.org/works?filter=cites:{work_id}{cited_from_filter}{cited_to_filter}&sort=publication_date"
@@ -281,9 +299,12 @@ async def make_author_report(
281
299
 
282
300
  # Get sources full info.
283
301
  sources: list[Source] = []
284
- for dehydrated_source in dehydrated_sources:
302
+ sources_count = len(dehydrated_sources)
303
+ for idx, dehydrated_source in enumerate(dehydrated_sources, 1):
285
304
  source_id = identifier.get_source_id(dehydrated_source)
286
305
  source_url = f"https://api.openalex.org/sources/{source_id}"
306
+
307
+ log.info(f"Getting Sources... [{idx}/{sources_count}]")
287
308
  sources.append(await _get_source(client, source_url))
288
309
 
289
310
  # Sort sources by h_index
@@ -349,8 +370,11 @@ async def make_institution_report(
349
370
  dehydrated_sources: list[DehydratedSource] = []
350
371
 
351
372
  # Getting all works that have cited a work.
352
- for institution_work in institution_works:
373
+ institution_works_count = len(institution_works)
374
+ for idx_work, institution_work in enumerate(institution_works, 1):
353
375
  work_id = identifier.get_work_id(institution_work)
376
+ log.info(f"[{work_id}] Work [{idx_work}/{institution_works_count}]")
377
+
354
378
  work_authors = _get_authors_list(authorships=institution_work.authorships)
355
379
  cited_by_api_url = (
356
380
  f"https://api.openalex.org/works?filter=cites:{work_id}{cited_from_filter}{cited_to_filter}&sort=publication_date"
@@ -388,9 +412,12 @@ async def make_institution_report(
388
412
 
389
413
  # Get sources full info.
390
414
  sources: list[Source] = []
391
- for dehydrated_source in dehydrated_sources:
415
+ sources_count = len(dehydrated_sources)
416
+ for idx, dehydrated_source in enumerate(dehydrated_sources, 1):
392
417
  source_id = identifier.get_source_id(dehydrated_source)
393
418
  source_url = f"https://api.openalex.org/sources/{source_id}"
419
+
420
+ log.debug(f"[{work_id}] Getting Sources... [{idx}/{sources_count}]")
394
421
  sources.append(await _get_source(client, source_url))
395
422
 
396
423
  # Sort sources by h_index