pub-analyzer 0.2.0__py3-none-any.whl → 0.3.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.

Files changed (40) hide show
  1. pub_analyzer/css/body.tcss +48 -35
  2. pub_analyzer/css/buttons.tcss +0 -4
  3. pub_analyzer/css/main.tcss +1 -1
  4. pub_analyzer/css/summary.tcss +75 -0
  5. pub_analyzer/internal/identifier.py +26 -0
  6. pub_analyzer/internal/report.py +73 -31
  7. pub_analyzer/internal/templates/author/{author_resume.typ → author_summary.typ} +4 -3
  8. pub_analyzer/internal/templates/author/report.typ +4 -3
  9. pub_analyzer/internal/templates/author/sources.typ +7 -5
  10. pub_analyzer/internal/templates/author/works.typ +12 -12
  11. pub_analyzer/internal/templates/author/works_extended.typ +4 -4
  12. pub_analyzer/main.py +3 -2
  13. pub_analyzer/models/author.py +9 -25
  14. pub_analyzer/models/concept.py +19 -0
  15. pub_analyzer/models/institution.py +11 -1
  16. pub_analyzer/models/report.py +14 -14
  17. pub_analyzer/models/source.py +59 -3
  18. pub_analyzer/models/topic.py +59 -0
  19. pub_analyzer/models/work.py +23 -0
  20. pub_analyzer/widgets/author/cards.py +6 -5
  21. pub_analyzer/widgets/author/core.py +11 -10
  22. pub_analyzer/widgets/common/summary.py +7 -0
  23. pub_analyzer/widgets/institution/core.py +10 -9
  24. pub_analyzer/widgets/report/cards.py +10 -11
  25. pub_analyzer/widgets/report/concept.py +47 -0
  26. pub_analyzer/widgets/report/core.py +14 -0
  27. pub_analyzer/widgets/report/grants.py +46 -0
  28. pub_analyzer/widgets/report/source.py +11 -4
  29. pub_analyzer/widgets/report/topic.py +55 -0
  30. pub_analyzer/widgets/report/work.py +45 -9
  31. pub_analyzer/widgets/search/results.py +8 -8
  32. pub_analyzer/widgets/sidebar.py +11 -2
  33. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/METADATA +8 -7
  34. pub_analyzer-0.3.0.dist-info/RECORD +69 -0
  35. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/WHEEL +1 -1
  36. pub_analyzer/css/author.tcss +0 -82
  37. pub_analyzer/css/institution.tcss +0 -82
  38. pub_analyzer-0.2.0.dist-info/RECORD +0 -64
  39. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/LICENSE +0 -0
  40. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -14,38 +14,51 @@ SideBar {
14
14
 
15
15
  background: $bg-secondary-color;
16
16
  color: $text-primary-color;
17
- transition: width 250ms in_out_cubic;
18
- }
19
17
 
20
- .-dark-mode SideBar {
21
- background: $bg-secondary-color-darken;
22
- color: $text-primary-color-darken;
23
- }
18
+ Button {
19
+ text-align: start;
20
+ }
24
21
 
25
- SideBar.-hidden {
26
- width: 0;
27
- }
22
+ #sidebar-title {
23
+ width: 100%;
24
+ content-align: left middle;
25
+ text-style: bold;
26
+ border-bottom: heavy $text-primary-color;
27
+ }
28
28
 
29
- SideBar .sidebar-options-column {
30
- margin: 1;
31
- padding: 1 1;
32
- }
29
+ .sidebar-options-column {
30
+ margin: 1;
31
+ padding: 1 1;
32
+ }
33
33
 
34
- SideBar #sidebar-title {
35
- width: 100%;
36
- content-align: left middle;
37
- text-style: bold;
38
- border-bottom: heavy $text-primary-color;
39
- }
34
+ .sidebar-buttons-column {
35
+ height: 1fr;
36
+
37
+ .sidebar-option {
38
+ content-align: left middle;
39
+ margin: 1 0;
40
+ width: 100%;
41
+ }
42
+ }
40
43
 
41
- .-dark-mode SideBar #sidebar-title {
42
- border-bottom: heavy $text-primary-color-darken;
44
+ #module-version-label {
45
+ color: $text-primary-color 25%;
46
+ text-align: center;
47
+ width: 100%;
48
+ }
43
49
  }
44
50
 
45
- SideBar .sidebar-option {
46
- content-align: left middle;
47
- margin: 1 0;
48
- width: 100%;
51
+ .-dark-mode SideBar {
52
+ background: $bg-secondary-color-darken;
53
+ color: $text-primary-color-darken;
54
+
55
+ #sidebar-title {
56
+ border-bottom: heavy $text-primary-color-darken;
57
+ }
58
+
59
+ #module-version-label {
60
+ color: $text-primary-color-darken 25%;
61
+ }
49
62
  }
50
63
 
51
64
  /* Main Content CSS */
@@ -55,20 +68,20 @@ MainContent {
55
68
 
56
69
  padding: 2 1 0 1;
57
70
  height: 100%;
71
+
72
+ .title {
73
+ content-align: left middle;
74
+ text-style: bold;
75
+ border-bottom: heavy $text-primary-color;
76
+ width: 100%
77
+ }
58
78
  }
59
79
 
60
80
  .-dark-mode MainContent {
61
81
  background: $bg-main-color-darken;
62
82
  color: $text-primary-color-darken;
63
- }
64
-
65
- MainContent .title {
66
- content-align: left middle;
67
- text-style: bold;
68
- border-bottom: heavy $text-primary-color;
69
- width: 100%
70
- }
71
83
 
72
- .-dark-mode MainContent .title {
73
- border-bottom: heavy $text-primary-color-darken;
84
+ .title {
85
+ border-bottom: heavy $text-primary-color-darken;
86
+ }
74
87
  }
@@ -3,10 +3,6 @@ $primary-color: #b91c1c;
3
3
  $primary-color-accent: #991b1b;
4
4
  $primary-color-highlight: #dc2626;
5
5
 
6
- Button {
7
- text-align: start;
8
- }
9
-
10
6
  /* Primary variant */
11
7
  Button.-primary {
12
8
  background: $primary-color;
@@ -39,6 +39,6 @@ LoadingIndicator {
39
39
  color: $primary-color-accent;
40
40
  }
41
41
 
42
- LoadingIndicator.-overlay {
42
+ LoadingIndicator.-textual-loading-indicator {
43
43
  background: transparent;
44
44
  }
@@ -0,0 +1,75 @@
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
+ SummaryWidget {
12
+ height: 1fr;
13
+ margin: 1 2;
14
+
15
+ /* Titles */
16
+ .block-title {
17
+ text-align: center;
18
+ width: 100%;
19
+ border-bottom: solid $text-primary-color;
20
+ }
21
+
22
+ /* Block Container */
23
+ .block-container {
24
+ padding: 1;
25
+ height: auto;
26
+ }
27
+
28
+ /* Cards */
29
+ .cards-container {
30
+ height: auto;
31
+ margin: 1 0 0 0 ;
32
+
33
+ layout: grid;
34
+ grid-size: 3 1;
35
+ grid-rows: 14;
36
+ grid-columns: 1fr;
37
+ grid-gutter: 1 2;
38
+ }
39
+
40
+ /* Info Container */
41
+ .info-container {
42
+ height: auto;
43
+
44
+ Label {
45
+ text-align: center;
46
+ width: 1fr;
47
+ }
48
+ }
49
+
50
+ /* Table */
51
+ .table-container {
52
+ height: auto;
53
+ margin: 1 0 0 0;
54
+ }
55
+
56
+ /* Filter Container */
57
+ .filter-collapsible {
58
+ margin-top: 1;
59
+
60
+ DateRangeFilter {
61
+ margin-top: 1;
62
+ }
63
+ }
64
+
65
+ /* Buttons */
66
+ .button-container {
67
+ align: center middle;
68
+ height: 5;
69
+ }
70
+ }
71
+
72
+ .-dark-mode SummaryWidget {
73
+ color: $text-primary-color-darken;
74
+ background: $bg-secondary-color;
75
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  from pub_analyzer.models.author import Author, AuthorOpenAlexKey, AuthorResult, DehydratedAuthor
4
4
  from pub_analyzer.models.institution import DehydratedInstitution, Institution, InstitutionOpenAlexKey, InstitutionResult
5
+ from pub_analyzer.models.source import DehydratedSource, Source
5
6
  from pub_analyzer.models.work import Work
6
7
 
7
8
 
@@ -78,3 +79,28 @@ def get_work_id(work: Work) -> str:
78
79
  return work.id.path.rpartition("/")[2]
79
80
  else:
80
81
  return ""
82
+
83
+
84
+ def get_source_id(source: DehydratedSource | Source) -> str:
85
+ """Extract OpenAlex ID from Source Model.
86
+
87
+ Args:
88
+ source: Source model instance.
89
+
90
+ Returns:
91
+ Source OpenAlex ID.
92
+
93
+ Example:
94
+ ```python
95
+ from pub_analyzer.internal.identifier import get_source_id
96
+ from pub_analyzer.models.source import Source
97
+
98
+ source = Source(id="https://openalex.org/S000000000", **kwargs)
99
+ print(get_source_id(source))
100
+ # 'S000000000'
101
+ ```
102
+ """
103
+ if source.id.path:
104
+ return source.id.path.rpartition("/")[2]
105
+ else:
106
+ return ""
@@ -13,14 +13,15 @@ from pub_analyzer.models.institution import DehydratedInstitution, Institution,
13
13
  from pub_analyzer.models.report import (
14
14
  AuthorReport,
15
15
  CitationReport,
16
- CitationResume,
16
+ CitationSummary,
17
17
  CitationType,
18
18
  InstitutionReport,
19
- OpenAccessResume,
20
- SourcesResume,
19
+ OpenAccessSummary,
20
+ SourcesSummary,
21
21
  WorkReport,
22
22
  WorkTypeCounter,
23
23
  )
24
+ from pub_analyzer.models.source import DehydratedSource, Source
24
25
  from pub_analyzer.models.work import Authorship, Work
25
26
 
26
27
  FromDate = NewType("FromDate", datetime.datetime)
@@ -171,6 +172,25 @@ async def _get_works(client: httpx.AsyncClient, url: str) -> list[Work]:
171
172
  return TypeAdapter(list[Work]).validate_python(works_data)
172
173
 
173
174
 
175
+ async def _get_source(client: httpx.AsyncClient, url: str) -> Source:
176
+ """Get source given a URL.
177
+
178
+ Args:
179
+ client: HTTPX asynchronous client to be used to make the requests.
180
+ url: URL of works with all filters.
181
+
182
+ Returns:
183
+ Source Model.
184
+
185
+ Raises:
186
+ httpx.HTTPStatusError: One response from OpenAlex API had an error HTTP status of 4xx or 5xx.
187
+ """
188
+ response = await client.get(url=url)
189
+ response.raise_for_status()
190
+
191
+ return Source(**response.json())
192
+
193
+
174
194
  async def make_author_report(
175
195
  author: Author,
176
196
  extra_profiles: list[Author | AuthorResult | DehydratedAuthor] | None = None,
@@ -216,10 +236,10 @@ async def make_author_report(
216
236
 
217
237
  # Report fields.
218
238
  works: list[WorkReport] = []
219
- report_citation_resume = CitationResume()
220
- open_access_resume = OpenAccessResume()
239
+ report_citation_summary = CitationSummary()
240
+ open_access_summary = OpenAccessSummary()
221
241
  works_type_counter: list[WorkTypeCounter] = []
222
- sources_resume = SourcesResume(sources=[])
242
+ dehydrated_sources: list[DehydratedSource] = []
223
243
 
224
244
  # Getting all works that have cited the author.
225
245
  for author_work in author_works:
@@ -230,7 +250,7 @@ async def make_author_report(
230
250
  )
231
251
 
232
252
  # Adding the type of OpenAccess in the counter.
233
- open_access_resume.add_oa_type(author_work.open_access.oa_status)
253
+ open_access_summary.add_oa_type(author_work.open_access.oa_status)
234
254
 
235
255
  # Adding the work type to works type counter.
236
256
  work_type = next((work_type for work_type in works_type_counter if work_type.type_name == author_work.type), None)
@@ -241,31 +261,42 @@ async def make_author_report(
241
261
 
242
262
  # Add Sources to global list.
243
263
  for location in author_work.locations:
244
- if location.source and not any(source.display_name == location.source.display_name for source in sources_resume.sources):
245
- sources_resume.sources.append(location.source)
264
+ if location.source and not any(source.id == location.source.id for source in dehydrated_sources):
265
+ dehydrated_sources.append(location.source)
246
266
 
247
267
  cited_by_works = await _get_works(client, cited_by_api_url)
248
268
  cited_by: list[CitationReport] = []
249
- work_citation_resume = CitationResume()
269
+ work_citation_summary = CitationSummary()
250
270
  for cited_by_work in cited_by_works:
251
271
  cited_authors = _get_authors_list(authorships=cited_by_work.authorships)
252
272
  citation_type = _get_citation_type(work_authors, cited_authors)
253
273
 
254
274
  # Adding the type of cites in the counters.
255
- report_citation_resume.add_cite_type(citation_type)
256
- work_citation_resume.add_cite_type(citation_type)
275
+ report_citation_summary.add_cite_type(citation_type)
276
+ work_citation_summary.add_cite_type(citation_type)
257
277
 
258
278
  cited_by.append(CitationReport(work=cited_by_work, citation_type=citation_type))
259
279
 
260
- works.append(WorkReport(work=author_work, cited_by=cited_by, citation_resume=work_citation_resume))
280
+ works.append(WorkReport(work=author_work, cited_by=cited_by, citation_summary=work_citation_summary))
281
+
282
+ # Get sources full info.
283
+ sources: list[Source] = []
284
+ for dehydrated_source in dehydrated_sources:
285
+ source_id = identifier.get_source_id(dehydrated_source)
286
+ source_url = f"https://api.openalex.org/sources/{source_id}"
287
+ sources.append(await _get_source(client, source_url))
288
+
289
+ # Sort sources by h_index
290
+ sources_sorted = sorted(sources, key=lambda source: source.summary_stats.two_yr_mean_citedness, reverse=True)
291
+ sources_summary = SourcesSummary(sources=sources_sorted)
261
292
 
262
293
  return AuthorReport(
263
294
  author=author,
264
295
  works=works,
265
- citation_resume=report_citation_resume,
266
- open_access_resume=open_access_resume,
267
- works_type_resume=works_type_counter,
268
- sources_resume=sources_resume,
296
+ citation_summary=report_citation_summary,
297
+ open_access_summary=open_access_summary,
298
+ works_type_summary=works_type_counter,
299
+ sources_summary=sources_summary,
269
300
  )
270
301
 
271
302
 
@@ -312,10 +343,10 @@ async def make_institution_report(
312
343
 
313
344
  # Report fields.
314
345
  works: list[WorkReport] = []
315
- report_citation_resume = CitationResume()
316
- open_access_resume = OpenAccessResume()
346
+ report_citation_summary = CitationSummary()
347
+ open_access_summary = OpenAccessSummary()
317
348
  works_type_counter: list[WorkTypeCounter] = []
318
- sources_resume = SourcesResume(sources=[])
349
+ dehydrated_sources: list[DehydratedSource] = []
319
350
 
320
351
  # Getting all works that have cited a work.
321
352
  for institution_work in institution_works:
@@ -326,7 +357,7 @@ async def make_institution_report(
326
357
  )
327
358
 
328
359
  # Adding the type of OpenAccess in the counter.
329
- open_access_resume.add_oa_type(institution_work.open_access.oa_status)
360
+ open_access_summary.add_oa_type(institution_work.open_access.oa_status)
330
361
 
331
362
  # Adding the work type to works type counter.
332
363
  work_type = next((work_type for work_type in works_type_counter if work_type.type_name == institution_work.type), None)
@@ -337,29 +368,40 @@ async def make_institution_report(
337
368
 
338
369
  # Add Sources to global list.
339
370
  for location in institution_work.locations:
340
- if location.source and not any(source.display_name == location.source.display_name for source in sources_resume.sources):
341
- sources_resume.sources.append(location.source)
371
+ if location.source and not any(source.id == location.source.id for source in dehydrated_sources):
372
+ dehydrated_sources.append(location.source)
342
373
 
343
374
  cited_by_works = await _get_works(client, cited_by_api_url)
344
375
  cited_by: list[CitationReport] = []
345
- work_citation_resume = CitationResume()
376
+ work_citation_summary = CitationSummary()
346
377
  for cited_by_work in cited_by_works:
347
378
  cited_authors = _get_authors_list(authorships=cited_by_work.authorships)
348
379
  citation_type = _get_citation_type(work_authors, cited_authors)
349
380
 
350
381
  # Adding the type of cites in the counters.
351
- report_citation_resume.add_cite_type(citation_type)
352
- work_citation_resume.add_cite_type(citation_type)
382
+ report_citation_summary.add_cite_type(citation_type)
383
+ work_citation_summary.add_cite_type(citation_type)
353
384
 
354
385
  cited_by.append(CitationReport(work=cited_by_work, citation_type=citation_type))
355
386
 
356
- works.append(WorkReport(work=institution_work, cited_by=cited_by, citation_resume=work_citation_resume))
387
+ works.append(WorkReport(work=institution_work, cited_by=cited_by, citation_summary=work_citation_summary))
388
+
389
+ # Get sources full info.
390
+ sources: list[Source] = []
391
+ for dehydrated_source in dehydrated_sources:
392
+ source_id = identifier.get_source_id(dehydrated_source)
393
+ source_url = f"https://api.openalex.org/sources/{source_id}"
394
+ sources.append(await _get_source(client, source_url))
395
+
396
+ # Sort sources by h_index
397
+ sources_sorted = sorted(sources, key=lambda source: source.summary_stats.two_yr_mean_citedness, reverse=True)
398
+ sources_summary = SourcesSummary(sources=sources_sorted)
357
399
 
358
400
  return InstitutionReport(
359
401
  institution=institution,
360
402
  works=works,
361
- citation_resume=report_citation_resume,
362
- open_access_resume=open_access_resume,
363
- works_type_resume=works_type_counter,
364
- sources_resume=sources_resume,
403
+ citation_summary=report_citation_summary,
404
+ open_access_summary=open_access_summary,
405
+ works_type_summary=works_type_counter,
406
+ sources_summary=sources_summary,
365
407
  )
@@ -1,4 +1,4 @@
1
- // Author Resume
1
+ // Author Summary
2
2
  = Author.
3
3
 
4
4
  // Cards
@@ -22,8 +22,9 @@
22
22
  [#align(center)[#text(size: 12pt)[Last institution:]]],
23
23
 
24
24
  // Card content
25
- {% if report.author.last_known_institution %}
26
- [#align(left)[#text(size: 10pt)[- *Name:* {{ report.author.last_known_institution.display_name }}]]],
25
+ {% if report.author.last_known_institutions%}
26
+ {% set last_known_institution = report.author.last_known_institutions[0] %}
27
+ [#align(left)[#text(size: 10pt)[- *Name:* {{ last_known_institution.display_name }}]]],
27
28
  [#align(left)[#text(size: 10pt)[- *Country:* MX]]],
28
29
  [#align(left)[#text(size: 10pt)[- *Type:* education]]],
29
30
  {% endif %}
@@ -39,12 +39,13 @@
39
39
  columns: (1fr),
40
40
  row-gutter: 11pt,
41
41
  [#align(center, text(size: 17pt, weight: "bold")[{{ report.author.display_name }}])],
42
- {% if report.author.last_known_institution %}
43
- [#align(center, text(size: 15pt, weight: "thin")[{{ report.author.last_known_institution.display_name }}])],
42
+ {% if report.author.last_known_institutions %}
43
+ {% set last_known_institution = report.author.last_known_institutions[0] %}
44
+ [#align(center, text(size: 15pt, weight: "thin")[{{ last_known_institution.display_name }}])],
44
45
  {% endif %}
45
46
  )
46
47
 
47
- {% include 'author_resume.typ' %}
48
+ {% include 'author_summary.typ' %}
48
49
 
49
50
  {% include 'works.typ' %}
50
51
 
@@ -2,19 +2,21 @@
2
2
  = Sources.
3
3
 
4
4
  #table(
5
- columns: (auto, 3fr, 2fr, auto, auto, auto),
5
+ columns: (auto, 3fr, 2fr, auto, auto, auto, auto, auto),
6
6
  inset: 8pt,
7
7
  align: horizon,
8
8
  // Headers
9
- [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Is Open Access*],
9
+ [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Impact factor*], [*h-index*], [*Is OA*],
10
10
 
11
11
  // Content
12
- {% for source in report.sources_resume.sources %}
13
- [{{ loop.index }}],
14
- [#underline([#link("{{ source.id }}")[#"{{ source.display_name }}"]])],
12
+ {% for source in report.sources_summary.sources %}
13
+ [#underline[3.{{ loop.index }}. #label("source_{{ source.id.path.rpartition("/")[2] }}")]],
14
+ [#underline([#link("{{ source.homepage_url }}")[#"{{ source.display_name }}"]])],
15
15
  [{{ source.host_organization_name or "-" }}],
16
16
  [{{source.type }}],
17
17
  [{{ source.issn_l or "-" }}],
18
+ [{{ source.summary_stats.two_yr_mean_citedness|round(3) }}],
19
+ [{{ source.summary_stats.h_index }}],
18
20
  [{% if source.is_oa %}#text(rgb("909d63"))[True]{% else %}#text(rgb("bc5653"))[False]{% endif %}],
19
21
  {% endfor %}
20
22
  )
@@ -9,14 +9,14 @@
9
9
  [
10
10
  #align(center)[_Citation metrics_]
11
11
  #parbreak()
12
- - *Count:* {{ report.citation_resume.type_a_count + report.citation_resume.type_b_count }}
13
- - *Type A:* {{ report.citation_resume.type_a_count }}
14
- - *Type B:* {{ report.citation_resume.type_b_count }}
12
+ - *Count:* {{ report.citation_summary.type_a_count + report.citation_summary.type_b_count }}
13
+ - *Type A:* {{ report.citation_summary.type_a_count }}
14
+ - *Type B:* {{ report.citation_summary.type_b_count }}
15
15
  ],
16
16
  [
17
17
  #align(center)[_Work Type_]
18
18
  #parbreak()
19
- {% for work_type in report.works_type_resume %}
19
+ {% for work_type in report.works_type_summary %}
20
20
  - *{{ work_type.type_name }}:* {{ work_type.count }}
21
21
  {% endfor %}
22
22
  ],
@@ -27,13 +27,13 @@
27
27
  columns: (1fr, 1fr),
28
28
  column-gutter: 15pt,
29
29
  [
30
- - *gold:* {{report.open_access_resume.gold}}
31
- - *green:* {{report.open_access_resume.green}}
32
- - *hybrid:* {{report.open_access_resume.hybrid}}
30
+ - *gold:* {{report.open_access_summary.gold}}
31
+ - *green:* {{report.open_access_summary.green}}
32
+ - *hybrid:* {{report.open_access_summary.hybrid}}
33
33
  ],
34
34
  [
35
- - *bronze:* {{report.open_access_resume.bronze}}
36
- - *closed:* {{report.open_access_resume.closed}}
35
+ - *bronze:* {{report.open_access_summary.bronze}}
36
+ - *closed:* {{report.open_access_summary.closed}}
37
37
  ],
38
38
  )
39
39
  ],
@@ -56,9 +56,9 @@
56
56
  [{{ work.work.type }}],
57
57
  [{% if work.work.ids.doi %}#underline([#link("{{ work.work.ids.doi }}")[DOI]]){% else %}-{% endif %}],
58
58
  [{{ work.work.publication_date }}],
59
- [{{ work.citation_resume.type_a_count + work.citation_resume.type_b_count }}],
60
- [{{ work.citation_resume.type_a_count }}],
61
- [{{ work.citation_resume.type_b_count }}],
59
+ [{{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}],
60
+ [{{ work.citation_summary.type_a_count }}],
61
+ [{{ work.citation_summary.type_b_count }}],
62
62
  [{{ work.work.open_access.oa_status.value }}],
63
63
  {% endfor %}
64
64
  )
@@ -39,9 +39,9 @@
39
39
  [
40
40
  #align(center)[_Citation_]
41
41
  #parbreak()
42
- - *Count:* {{ work.citation_resume.type_a_count + work.citation_resume.type_b_count }}
43
- - *Type A:* {{ work.citation_resume.type_a_count }}
44
- - *Type B:* {{ work.citation_resume.type_b_count }}
42
+ - *Count:* {{ work.citation_summary.type_a_count + work.citation_summary.type_b_count }}
43
+ - *Type A:* {{ work.citation_summary.type_a_count }}
44
+ - *Type B:* {{ work.citation_summary.type_b_count }}
45
45
  ],
46
46
  )
47
47
 
@@ -83,7 +83,7 @@
83
83
  // Content
84
84
  {% for location in work.work.locations %}
85
85
  {% if location.source %}
86
- [{{ loop.index }}],
86
+ [#underline([#link(label("source_{{ location.source.id.path.rpartition("/")[2] }}"))[{{ loop.index }}]])],
87
87
  [#underline([#link("{{ location.landing_page_url }}")[#"{{ location.source.display_name }}"]])],
88
88
  [{{ location.source.host_organization_name or "-" }}],
89
89
  [{{ location.source.type }}],
pub_analyzer/main.py CHANGED
@@ -18,16 +18,17 @@ from pub_analyzer.widgets.sidebar import SideBar
18
18
  class PubAnalyzerApp(App[DOMNode]):
19
19
  """Pub Analyzer App entrypoint."""
20
20
 
21
+ TITLE = "Pub Analyzer"
22
+
21
23
  CSS_PATH: ClassVar[CSSPathType | None] = [
22
- "css/author.tcss",
23
24
  "css/body.tcss",
24
25
  "css/buttons.tcss",
25
26
  "css/checkbox.tcss",
26
27
  "css/collapsible.tcss",
27
28
  "css/datatable.tcss",
28
- "css/institution.tcss",
29
29
  "css/main.tcss",
30
30
  "css/report.tcss",
31
+ "css/summary.tcss",
31
32
  "css/search.tcss",
32
33
  "css/tabs.tcss",
33
34
  "css/tree.tcss",
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import TypeAlias
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
5
+ from pydantic import BaseModel, Field, HttpUrl
6
6
 
7
7
  from pub_analyzer.models.institution import DehydratedInstitution
8
8
 
@@ -16,19 +16,11 @@ AuthorOpenAlexKey: TypeAlias = str
16
16
  class AuthorIDs(BaseModel):
17
17
  """IDs from an Author."""
18
18
 
19
- openalex: str
20
- orcid: str | None = ""
21
- scopus: str | None = ""
22
- twitter: str | None = ""
23
- wikipedia: str | None = ""
24
-
25
- # Allowing a value to be assigned during validation.
26
- model_config = ConfigDict(validate_assignment=True)
27
-
28
- @field_validator("scopus", "twitter", "wikipedia")
29
- def set_default(cls, value: str) -> str:
30
- """Define a default text."""
31
- return value or ""
19
+ openalex: AuthorOpenAlexID
20
+ orcid: HttpUrl | None = None
21
+ scopus: HttpUrl | None = None
22
+ twitter: HttpUrl | None = None
23
+ wikipedia: HttpUrl | None = None
32
24
 
33
25
 
34
26
  class AuthorYearCount(BaseModel):
@@ -60,7 +52,7 @@ class Author(BaseModel):
60
52
  works_count: int
61
53
  cited_by_count: int
62
54
 
63
- last_known_institution: DehydratedInstitution | None
55
+ last_known_institutions: list[DehydratedInstitution]
64
56
  counts_by_year: list[AuthorYearCount]
65
57
 
66
58
  summary_stats: AuthorSummaryStats
@@ -81,16 +73,8 @@ class AuthorResult(BaseModel):
81
73
 
82
74
  id: AuthorOpenAlexID
83
75
  display_name: str
84
- hint: str | None = ""
76
+ hint: str | None = None
85
77
  cited_by_count: int
86
78
  works_count: int
87
79
  entity_type: str
88
- external_id: str | None = ""
89
-
90
- # Allowing a value to be assigned during validation.
91
- model_config = ConfigDict(validate_assignment=True)
92
-
93
- @field_validator("hint", "external_id")
94
- def set_default(cls, value: str) -> str:
95
- """Define a default text."""
96
- return value or ""
80
+ external_id: str | None = None
@@ -0,0 +1,19 @@
1
+ """Concept model from OpenAlex API Schema definition."""
2
+
3
+ from pydantic import BaseModel, HttpUrl
4
+
5
+
6
+ class DehydratedConcept(BaseModel):
7
+ """Stripped-down Concept Model."""
8
+
9
+ id: HttpUrl
10
+ """The OpenAlex ID for this concept."""
11
+ display_name: str
12
+ """The English-language label of the concept."""
13
+
14
+ wikidata: HttpUrl
15
+ """The Wikidata ID for this concept. All OpenAlex concepts are also Wikidata concepts."""
16
+ level: int
17
+ """The level in the concept. Lower-level concepts are more general, and higher-level concepts are more specific."""
18
+ score: float
19
+ """The strength of the connection between the work and this concept (higher is stronger)."""