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,556 @@
1
+ // This document was generated using Pub Analyzer.
2
+ //
3
+ // Pub Analyzer is a tool designed to retrieve, process and present in a concise and understandable
4
+ // way the scientific production of a researcher, including detailed information about their articles,
5
+ // citations, collaborations and other relevant metrics.
6
+ //
7
+ // See more here: https://pub-analyzer.com
8
+
9
+ // Packages
10
+ //
11
+ // This document uses the Cetz package to render plots and graphs. For more information
12
+ // on how to edit the plots see: https://typst.app/universe/package/cetz/
13
+
14
+ #import "@preview/cetz:0.3.4"
15
+ #import "@preview/cetz-plot:0.1.1": plot, chart
16
+
17
+ // Colors
18
+ //
19
+ // The following variables control all colors used in the document.
20
+ // You can modify the color codes by specifying the four RGB(A) components or by
21
+ // using the hexadecimal code.
22
+ //
23
+ // See more here: https://typst.app/docs/reference/visualize/color/#definitions-rgb
24
+
25
+ #let SUCCESS = rgb("#909d63")
26
+ #let ERROR = rgb("#bc5653")
27
+
28
+ #let CATEGORY_1 = rgb("#42a2f8")
29
+ #let CATEGORY_2 = rgb("#82d452")
30
+ #let CATEGORY_3 = rgb("#929292")
31
+ #let CATEGORY_4 = rgb("#f0bb40")
32
+ #let CATEGORY_5 = rgb("#eb4025")
33
+ #let CATEGORY_6 = rgb("#c33375")
34
+
35
+ #let PALETTE = (CATEGORY_1, CATEGORY_2, CATEGORY_3, CATEGORY_4, CATEGORY_5, CATEGORY_6)
36
+
37
+ // Get data
38
+ #let report = json(bytes(sys.inputs.report))
39
+ #let version = str(bytes(sys.inputs.version))
40
+ #let author = report.at("author")
41
+ #let works = report.at("works")
42
+ #let citation_summary = report.at("citation_summary")
43
+ #let open_access_summary = report.at("open_access_summary")
44
+ #let works_type_summary = report.at("works_type_summary")
45
+ #let sources_summary = report.at("sources_summary")
46
+
47
+ // Set document metadata.
48
+ #let description = "This document was generated using Pub Analyzer version " + version + "."
49
+ #set document(
50
+ title: "Pub Analyzer",
51
+ description: description,
52
+ )
53
+
54
+ // Page Layout
55
+ #set page("us-letter")
56
+ #set page(flipped: true)
57
+
58
+ #set page(footer: grid(
59
+ columns: (1fr, 1fr),
60
+ align(left)[Made with #link("https://pub-analyzer.com")[_pub-analyzer_] version #version],
61
+ align(right)[#context counter(page).display("1")],
62
+ )
63
+ )
64
+
65
+ // Text config
66
+ #set heading(numbering: "1.")
67
+ #set text(size: 10.5pt)
68
+ #set par(linebreaks: "simple", justify: true)
69
+ #set text(lang: "en", overhang: true, font: "New Computer Modern")
70
+
71
+ // Override reference
72
+ #show ref: it => {
73
+ let el = it.element
74
+ if el != none and el.func() == heading {
75
+ // Override heading references.
76
+ numbering(
77
+ el.numbering,
78
+ ..counter(heading).at(el.location())
79
+ )
80
+ } else {
81
+ // Other references as usual.
82
+ it
83
+ }
84
+ }
85
+
86
+ // Shortcuts
87
+ #let capitalize(input) = {
88
+ return upper(input.first()) + input.slice(1)
89
+ }
90
+
91
+
92
+ // Header
93
+ #grid(
94
+ columns: (1fr),
95
+ row-gutter: 11pt,
96
+ align: center,
97
+
98
+ [
99
+ #text(size: 17pt, weight: "bold")[#author.at("display_name")]
100
+ ],
101
+ if author.at("last_known_institutions").len() >= 1 [
102
+ #let last_known_institution = author.at("last_known_institutions").first()
103
+ #text(size: 15pt, weight: "thin")[#last_known_institution.at("display_name")]
104
+ ]
105
+ )
106
+
107
+ // Author Summary
108
+ = Author.
109
+
110
+ #let summary-card(title: "Title", body) = {
111
+ return block(
112
+ width: 100%, height: 150pt,
113
+ stroke: 1pt, radius: 2pt,
114
+ inset: (top: 20pt),
115
+ fill: rgb("e5e7eb"),
116
+ )[
117
+ #align(center)[#text(size: 12pt)[#title]]
118
+ #v(5pt)
119
+ #block(width: 100%, inset: (x: 20pt))[#body]
120
+ ]
121
+ }
122
+
123
+ // Cards
124
+ #grid(
125
+ columns: (1fr, 1fr, 1fr),
126
+ column-gutter: 15pt,
127
+
128
+ // Last institution.
129
+ [
130
+ #summary-card(title:"Last institution:")[
131
+ #if author.at("last_known_institutions").len() >= 1 [
132
+ #let last_known_institution = author.at("last_known_institutions").first()
133
+ #let institution_type_name = capitalize(last_known_institution.at("type"))
134
+
135
+ #grid(
136
+ rows: auto, row-gutter: 10pt,
137
+
138
+ [*Name:* #last_known_institution.at("display_name")],
139
+ [*Country:* #last_known_institution.at("country_code")],
140
+ [*Type:* #institution_type_name],
141
+ )
142
+ ]
143
+ ]
144
+ ],
145
+
146
+ // Author identifiers.
147
+ [
148
+ #summary-card(title:"Identifiers:")[
149
+ #grid(
150
+ rows: auto, row-gutter: 10pt,
151
+ ..(
152
+ author.at("ids").pairs().filter(id => id.last() != none).map(
153
+ ((k, v)) => grid.cell[
154
+ - #underline( [#link(v)[#k]] )
155
+ ]
156
+ ).flatten()
157
+ )
158
+ )
159
+ ]
160
+ ],
161
+
162
+ // Citation metrics.
163
+ [
164
+ #summary-card(title: "Citation metrics:")[
165
+ #let summary_stats = author.at("summary_stats")
166
+
167
+ #grid(
168
+ rows: auto, row-gutter: 10pt,
169
+
170
+ [*2-year mean:* #calc.round(summary_stats.at("2yr_mean_citedness"), digits: 5)],
171
+ [*h-index:* #summary_stats.at("h_index")],
172
+ [*i10 index:* #summary_stats.at("i10_index")],
173
+ )
174
+ ]
175
+ ]
176
+ )
177
+
178
+ #align(center, text(11pt)[_Counts by year_])
179
+ #grid(
180
+ columns: (1fr, 1fr),
181
+ column-gutter: 15pt,
182
+ align: (auto, horizon),
183
+
184
+
185
+ [
186
+ #table(
187
+ columns: (1fr, 2fr, 2fr),
188
+ inset: 8pt,
189
+ align: horizon,
190
+ // Headers
191
+ [*Year*], [*Works count*], [*Cited by count*],
192
+
193
+ // Content
194
+ ..author.at("counts_by_year").slice(0, calc.min(author.at("counts_by_year").len(), 8)).map(
195
+ ((year, works_count, cited_by_count)) => (
196
+ table.cell([#year]),
197
+ table.cell([#works_count]),
198
+ table.cell([#cited_by_count]),
199
+ )
200
+ ).flatten()
201
+ )
202
+ ],
203
+ grid.cell(
204
+ inset: (x: 10pt, bottom: 10pt, top: 2.5pt),
205
+ stroke: 1pt
206
+ )[
207
+ #align(center, text(10pt)[Cites by year])
208
+ #v(5pt)
209
+ #cetz.canvas(length: 100%, {
210
+ plot.plot(
211
+ size: (0.90, 0.48),
212
+ axis-style: "scientific-auto",
213
+ plot-style: (stroke: (1pt + PALETTE.at(0)),),
214
+ x-min: auto, x-max: auto,
215
+ x-tick-step: 1, y-tick-step: auto,
216
+ x-label: none, y-label: none,
217
+ {
218
+ plot.add((
219
+ ..author.at("counts_by_year").slice(0, calc.min(author.at("counts_by_year").len(), 8)).map(
220
+ ((year, works_count, cited_by_count)) => (
221
+ (year, cited_by_count)
222
+ )
223
+ )
224
+ ))
225
+ })
226
+ })
227
+ ]
228
+ )
229
+
230
+ // Works
231
+ #pagebreak()
232
+ = Works.
233
+
234
+ #let works_metrics_card(title: "Title", graph, body) = {
235
+ grid(
236
+ rows: (18pt, 175pt, 60pt),
237
+ columns: (100%),
238
+
239
+ [
240
+ #block(width: 100%, height: 100%)[
241
+ #align(center + horizon)[#text(style: "italic")[#title]]
242
+ ]
243
+ ],
244
+ [
245
+ #block(width: 100%, height: 100%)[
246
+ #align(center + horizon)[#graph]
247
+ ]
248
+ ],
249
+ [
250
+ #set text(size: 9.5pt)
251
+ #block(width: 100%, height: 100%, inset: (x: 0pt, y: 10pt))[#body]
252
+ ],
253
+ )
254
+ }
255
+
256
+ #let leyend_box(color: rgb) = {
257
+ box(height: 7pt, width: 7pt, fill: color)
258
+ }
259
+
260
+ #grid(
261
+ columns: (1fr, 1fr, 1fr),
262
+ column-gutter: 15pt,
263
+
264
+ [
265
+ #let graph = {
266
+ cetz.canvas(length: 35%, {
267
+ chart.piechart(
268
+ (
269
+ citation_summary.at("type_a_count"), // Type A
270
+ citation_summary.at("type_b_count") // Type B
271
+ ),
272
+ radius: 1,
273
+ slice-style: (PALETTE.at(0), PALETTE.at(1)),
274
+ outer-label: (content: "%", radius: 115%),
275
+ )
276
+ })
277
+ }
278
+
279
+ #works_metrics_card(title: "Citation metrics", graph)[
280
+ #grid(
281
+ rows: auto, row-gutter: 10pt,
282
+ columns: (1fr, 1fr),
283
+
284
+ grid.cell(colspan: 2)[
285
+ *Count:* #citation_summary.values().sum()
286
+ ],
287
+ [
288
+ #leyend_box(color: PALETTE.at(0)) *Type A:* #citation_summary.at("type_a_count")
289
+ ],
290
+ [
291
+ #leyend_box(color: PALETTE.at(1)) *Type B:* #citation_summary.at("type_b_count")
292
+ ],
293
+ )
294
+ ]
295
+ ],
296
+ [
297
+ #let graph = {
298
+ cetz.canvas(length: 35%, {
299
+ chart.columnchart(
300
+ size: (2.45, 2.0),
301
+ y-grid: false,
302
+ bar-style: cetz.palette.new(
303
+ base: (stroke: none, fill: none),
304
+ colors: PALETTE
305
+ ),
306
+ (
307
+ works_type_summary.slice(0, calc.min(4, works_type_summary.len())).map(
308
+ ((type_name, count)) => (
309
+ (capitalize(type_name.slice(0,2)), count)
310
+ )
311
+ )
312
+ )
313
+ )
314
+ })
315
+ }
316
+ #works_metrics_card(title: "Work Type", graph)[
317
+ #grid(
318
+ rows: auto, row-gutter: 10pt,
319
+ columns: (1fr, 1fr),
320
+ column-gutter: 5pt,
321
+
322
+ grid.cell(colspan: 2)[
323
+ *Count:* #open_access_summary.values().sum()
324
+ ],
325
+ ..works_type_summary.slice(0, calc.min(4, works_type_summary.len())).enumerate().map(
326
+ ((idx, work_type)) => (
327
+ grid.cell([#leyend_box(color: PALETTE.at(idx)) *#capitalize(work_type.type_name):* #work_type.count])
328
+ )
329
+ )
330
+ )
331
+ ]
332
+ ],
333
+ [
334
+ #let graph = {
335
+ cetz.canvas(length: 35%, {
336
+ chart.piechart(
337
+ (
338
+ open_access_summary.diamond, // diamond
339
+ open_access_summary.gold, // Gold
340
+ open_access_summary.green, // Green
341
+ open_access_summary.hybrid, // Hybrid
342
+ open_access_summary.bronze, // Bronze
343
+ open_access_summary.closed, // Closed
344
+ ),
345
+ radius: 1,
346
+ inner-radius: .4,
347
+ slice-style: (PALETTE.at(0), PALETTE.at(3), PALETTE.at(1), PALETTE.at(4), PALETTE.at(5), PALETTE.at(2)),
348
+ outer-label: (content: "%", radius: 115%),
349
+ )
350
+ })
351
+ }
352
+
353
+ #works_metrics_card(title: "Open Access", graph)[
354
+ #grid(
355
+ rows: auto, row-gutter: 10pt,
356
+ columns: (1.17fr, 1fr, 1fr),
357
+ column-gutter: 5pt,
358
+
359
+ grid.cell(colspan: 3)[
360
+ *Count:* #open_access_summary.values().sum()
361
+ ],
362
+
363
+ [#leyend_box(color: PALETTE.at(0)) *Diamond:* #open_access_summary.diamond],
364
+ [#leyend_box(color: PALETTE.at(3)) *Gold:* #open_access_summary.gold],
365
+ [#leyend_box(color: PALETTE.at(1)) *Green:* #open_access_summary.green],
366
+ [#leyend_box(color: PALETTE.at(5)) *Bronze:* #open_access_summary.bronze],
367
+ [#leyend_box(color: PALETTE.at(2)) *Closed:* #open_access_summary.closed],
368
+ [#leyend_box(color: PALETTE.at(4)) *Hybrid:* #open_access_summary.hybrid],
369
+ )
370
+ ]
371
+ ]
372
+ )
373
+
374
+ #align(
375
+ center,
376
+ text(11pt)[
377
+ Works from #str(works.first().work.publication_year) to #str(works.last().work.publication_year)
378
+ ]
379
+ )
380
+
381
+ #{
382
+ set text(size: 10pt)
383
+ table(
384
+ columns: (auto, 2fr, auto, auto, auto, auto, auto, auto, auto),
385
+ inset: 8pt,
386
+ align: horizon,
387
+ // Headers
388
+ [], [*Title*], [*Type*], [*DOI*], [*Publication Date*], [*Cited by count*], [*Type A*], [*Type B*], [*OA*],
389
+
390
+ // Content
391
+ ..works.enumerate().map(
392
+ ((idx, work)) => (
393
+ table.cell([#underline[#link(label("work_" + str(idx)))[#ref(label("work_" + str(idx)))]]]),
394
+ table.cell([#work.work.title]),
395
+ table.cell([#work.work.type]),
396
+ table.cell([#if work.work.ids.doi != none [#underline[#link(work.work.ids.doi)[DOI]]] else [#align(center)[-]]]),
397
+ table.cell([#work.work.publication_date]),
398
+ table.cell([#work.citation_summary.values().sum()]),
399
+ table.cell([#work.citation_summary.type_a_count]),
400
+ table.cell([#work.citation_summary.type_b_count]),
401
+ table.cell([#work.work.open_access.oa_status]),
402
+ )
403
+ ).flatten()
404
+ )
405
+ }
406
+
407
+ // Works Extended
408
+ #let work_driven_version = (
409
+ submittedVersion: "submitted",
410
+ acceptedVersion: "accepted",
411
+ publishedVersion: "published"
412
+ )
413
+ #for (idx, work_report) in works.enumerate() [
414
+ #let work = work_report.work
415
+
416
+ #pagebreak()
417
+ #heading(level: 2)[#work.title] #label("work_" + str(idx))
418
+
419
+
420
+ #if work.abstract != none [
421
+ #v(5pt)
422
+ #work.abstract
423
+ ]
424
+
425
+ // Cards
426
+ #v(5pt)
427
+ #grid(
428
+ columns: (1fr, 1fr, 1fr),
429
+ column-gutter: 30pt,
430
+
431
+ [
432
+ #align(center)[_Authorships_]
433
+ #block()[
434
+ #for authorship in work.authorships.slice(0, calc.min(10, work.authorships.len())) [
435
+ #let author_link = if authorship.author.at("orcid") != none {
436
+ authorship.author.orcid
437
+ } else {
438
+ authorship.author.id
439
+ }
440
+ - *#authorship.author_position:* #underline[#link(author_link)[#if authorship.author.display_name == author.display_name [#text(rgb(SUCCESS))[#authorship.author.display_name]] else [#authorship.author.display_name]]]
441
+ ]
442
+ #if work.authorships.len() > 10 [- *...*]
443
+ ]
444
+ ],
445
+ [
446
+ #align(center)[_Open Access_]
447
+
448
+ - *Status:* #capitalize(work.open_access.oa_status)
449
+ #if work.open_access.oa_url != none [- *URL:* #underline[#link(work.open_access.oa_url)[#work.open_access.oa_url.find(regex("^(https?:\/\/[^\/]+\/)"))]]]
450
+ ],
451
+ [
452
+ #align(center)[_Citation_]
453
+
454
+ - *Count:* #work_report.citation_summary.values().sum()
455
+ - *Type A:* #work_report.citation_summary.type_a_count
456
+ - *Type B:* #work_report.citation_summary.type_b_count
457
+ ]
458
+ )
459
+
460
+ // Cited by Table
461
+ #if work_report.cited_by.len() >= 1 [
462
+ #align(center, text(11pt)[_Cited by_])
463
+
464
+ #table(
465
+ columns: (auto, 3fr, 0.8fr, auto, auto, auto, auto),
466
+ inset: 8pt,
467
+ align: horizon,
468
+ // Headers
469
+ [], [*Title*], [*Type*], [*DOI*], [*Cite Type*], [*Publication Date*], [*Cited by count*],
470
+
471
+ // Content
472
+ ..work_report.cited_by.enumerate(start: 1).map(
473
+ ((idx, cited_by)) => (
474
+ table.cell([#idx]),
475
+ table.cell([#cited_by.work.title]),
476
+ table.cell([#cited_by.work.type]),
477
+ table.cell([#if cited_by.work.ids.doi != none [#underline[#link(cited_by.work.ids.doi)[DOI]]] else [#align(center)[-]]]),
478
+ table.cell([#if cited_by.citation_type == 0 [#text(rgb(SUCCESS))[Type A]] else [#text(rgb(ERROR))[Type B]]]),
479
+ table.cell([#cited_by.work.publication_date]),
480
+ table.cell([#cited_by.work.cited_by_count]),
481
+ )
482
+ ).flatten()
483
+ )
484
+ ]
485
+
486
+ // Sources Table
487
+ #if work.locations.len() >= 1 [
488
+ #align(center, text(11pt)[_Sources_])
489
+ #table(
490
+ columns: (auto, 3fr, 2.5fr, 1fr, auto, auto, 1.2fr, auto),
491
+ inset: 8pt,
492
+ align: horizon,
493
+ // Headers
494
+ [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Is OA*], [*License*], [*Version*],
495
+
496
+ // Content
497
+ ..work.locations.enumerate(start: 1).filter((location => location.last().source != none)).map(
498
+ ((idx, location)) => (
499
+ table.cell([#underline[#link(label("source_" + location.source.id.find(regex("S\d+$"))))[#idx]]]),
500
+ table.cell([#location.source.display_name]),
501
+ table.cell([#if location.source.host_organization_name != none [#location.source.host_organization_name] else [-]]),
502
+ table.cell([#location.source.type]),
503
+ table.cell([#if location.source.issn_l != none [#location.source.issn_l] else [-]]),
504
+ table.cell([#if location.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
505
+ table.cell([#if location.license != none [#location.license] else [-]]),
506
+ table.cell([#if location.version != none [#work_driven_version.at(location.version)]]),
507
+ )
508
+ ).flatten(),
509
+ ..work.locations.enumerate(start: 1).filter((location => location.last().source == none)).map(
510
+ ((idx, location)) => (
511
+ table.cell([#idx]),
512
+ table.cell([#underline([#link(location.landing_page_url)[#location.landing_page_url]])]),
513
+ table.cell([-]),
514
+ table.cell([-]),
515
+ table.cell([-]),
516
+ table.cell([#if location.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
517
+ table.cell([#if location.license != none [#location.license] else [-]]),
518
+ table.cell([#if location.version != none [#work_driven_version.at(location.version)]]),
519
+ )
520
+ ).flatten()
521
+ )
522
+ ]
523
+ ]
524
+
525
+
526
+ // Sources
527
+ #pagebreak()
528
+ = Sources.
529
+
530
+ #table(
531
+ columns: (auto, 2.7fr, 2.56fr, 1.2fr, auto, auto, auto, auto),
532
+ inset: 8pt,
533
+ align: horizon,
534
+ // Headers
535
+ [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Impact factor*], [*h-index*], [*Is OA*],
536
+
537
+ // Content
538
+ ..sources_summary.sources.enumerate(start: 1).map(
539
+ ((idx, source)) => (
540
+ table.cell([3.#idx. #label("source_" + source.id.find(regex("S\d+$")))]),
541
+ table.cell([#if source.homepage_url != none [#underline[#link(source.homepage_url)[#source.display_name]]] else [#underline[#link(source.id)[#source.display_name]]]]),
542
+ table.cell([#if source.host_organization_name != none [#source.host_organization_name] else [-]]),
543
+ table.cell([#source.type]),
544
+ table.cell([#if source.issn_l != none [#source.issn_l] else [-]]),
545
+ table.cell([#calc.round(source.summary_stats.at("2yr_mean_citedness"), digits: 3)]),
546
+ table.cell([#source.summary_stats.h_index]),
547
+ table.cell([#if source.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
548
+ )
549
+ ).flatten()
550
+ )
551
+
552
+
553
+ #pagebreak()
554
+ = Bibliography
555
+
556
+ Priem, J., Piwowar, H., & Orr, R. (2022). OpenAlex: A fully-open index of scholarly works, authors, venues, institutions, and concepts. ArXiv. https://arxiv.org/abs/2205.01833
pub_analyzer/main.py CHANGED
@@ -20,6 +20,7 @@ class PubAnalyzerApp(App[DOMNode]):
20
20
  """Pub Analyzer App entrypoint."""
21
21
 
22
22
  TITLE = "Pub Analyzer"
23
+ ENABLE_COMMAND_PALETTE = False
23
24
 
24
25
  CSS_PATH: ClassVar[CSSPathType | None] = [
25
26
  "css/body.tcss",
@@ -27,29 +28,25 @@ class PubAnalyzerApp(App[DOMNode]):
27
28
  "css/checkbox.tcss",
28
29
  "css/collapsible.tcss",
29
30
  "css/datatable.tcss",
31
+ "css/editor.tcss",
30
32
  "css/main.tcss",
31
33
  "css/report.tcss",
32
- "css/summary.tcss",
33
34
  "css/search.tcss",
35
+ "css/summary.tcss",
34
36
  "css/tabs.tcss",
35
37
  "css/tree.tcss",
36
38
  ]
37
39
  BINDINGS: ClassVar[list[BindingType]] = [
38
40
  Binding(key="ctrl+d", action="toggle_dark", description="Dark mode"),
39
41
  Binding(key="ctrl+s", action="toggle_sidebar", description="Sidebar"),
40
- Binding(key="ctrl+p", action="save_screenshot", description="Screenshot"),
41
42
  ]
42
43
 
43
44
  dark: Reactive[bool] = Reactive(False)
44
45
 
45
46
  def compose(self) -> ComposeResult:
46
47
  """Create child widgets for the app."""
47
- footer = Footer()
48
- footer.upper_case_keys = True
49
- footer.ctrl_to_caret = False
50
-
51
48
  yield Body()
52
- yield footer
49
+ yield Footer(show_command_palette=False)
53
50
 
54
51
  def action_toggle_dark(self) -> None:
55
52
  """Toggle dark mode."""
@@ -59,7 +59,7 @@ class InstitutionGeo(BaseModel):
59
59
  geonames_city_id: str
60
60
 
61
61
  region: str | None = None
62
- country_code: str
62
+ country_code: str | None = None
63
63
  country: str
64
64
 
65
65
  latitude: float
@@ -95,7 +95,7 @@ class Institution(BaseModel):
95
95
  ids: InstitutionIDs
96
96
 
97
97
  display_name: str
98
- country_code: str
98
+ country_code: str | None = None
99
99
  type: InstitutionType
100
100
  homepage_url: HttpUrl | None = None
101
101
  image_url: HttpUrl | None = None
@@ -120,7 +120,7 @@ class DehydratedInstitution(BaseModel):
120
120
  id: InstitutionOpenAlexID
121
121
  ror: str
122
122
  display_name: str
123
- country_code: str
123
+ country_code: str | None = None
124
124
  type: InstitutionType
125
125
 
126
126
 
@@ -44,6 +44,7 @@ class AffiliationsTable(Static):
44
44
  else f"{institution.display_name}"
45
45
  )
46
46
  years = ",".join([str(year) for year in affiliation.years])
47
- table.add_row(str(institution_name), str(institution.country_code.upper()), str(institution.type.name), str(years))
47
+ country_code = institution.country_code.upper() if institution.country_code else "-"
48
+ table.add_row(str(institution_name), str(country_code), str(institution.type.name), str(years))
48
49
 
49
50
  yield Static(table)
@@ -1,7 +1,10 @@
1
1
  """Body components."""
2
2
 
3
3
  from rich.console import RenderableType
4
+ from textual import on
4
5
  from textual.app import ComposeResult
6
+ from textual.message import Message
7
+ from textual.widget import Widget
5
8
  from textual.widgets import Label, Static
6
9
 
7
10
  from pub_analyzer.widgets.search import FinderWidget
@@ -13,6 +16,14 @@ class MainContent(Static):
13
16
 
14
17
  DEFAULT_CLASSES = "main-content"
15
18
 
19
+ class UpdateMainContent(Message):
20
+ """New main content required."""
21
+
22
+ def __init__(self, new_widget: Widget, title: str | None) -> None:
23
+ self.widget = new_widget
24
+ self.title = title
25
+ super().__init__()
26
+
16
27
  def __init__(self, title: str = "Title") -> None:
17
28
  self.title = title
18
29
  super().__init__()
@@ -26,6 +37,14 @@ class MainContent(Static):
26
37
  """Update view title."""
27
38
  self.query_one("#page-title", Label).update(title)
28
39
 
40
+ @on(UpdateMainContent)
41
+ async def update_content(self, new_content: UpdateMainContent) -> None:
42
+ """Replace the main content."""
43
+ await self.query_children().exclude("#page-title").remove()
44
+ if new_content.title:
45
+ self.update_title(new_content.title)
46
+ await self.mount(new_content.widget)
47
+
29
48
 
30
49
  class Body(Static):
31
50
  """Body App."""
@@ -3,6 +3,7 @@
3
3
  from .card import Card
4
4
  from .filesystem import FileSystemSelector
5
5
  from .input import DateInput, Input
6
+ from .label import ReactiveLabel
6
7
  from .modal import Modal
7
8
  from .selector import Select
8
9
 
@@ -12,5 +13,6 @@ __all__ = [
12
13
  "FileSystemSelector",
13
14
  "Input",
14
15
  "Modal",
16
+ "ReactiveLabel",
15
17
  "Select",
16
18
  ]