pub-analyzer 0.5.6__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.
Files changed (70) hide show
  1. pub_analyzer/__init__.py +1 -0
  2. pub_analyzer/__main__.py +7 -0
  3. pub_analyzer/css/body.tcss +87 -0
  4. pub_analyzer/css/buttons.tcss +24 -0
  5. pub_analyzer/css/checkbox.tcss +29 -0
  6. pub_analyzer/css/collapsible.tcss +31 -0
  7. pub_analyzer/css/datatable.tcss +50 -0
  8. pub_analyzer/css/editor.tcss +60 -0
  9. pub_analyzer/css/main.tcss +50 -0
  10. pub_analyzer/css/report.tcss +131 -0
  11. pub_analyzer/css/search.tcss +81 -0
  12. pub_analyzer/css/summary.tcss +75 -0
  13. pub_analyzer/css/tabs.tcss +18 -0
  14. pub_analyzer/css/tree.tcss +44 -0
  15. pub_analyzer/internal/__init__.py +1 -0
  16. pub_analyzer/internal/identifier.py +106 -0
  17. pub_analyzer/internal/limiter.py +34 -0
  18. pub_analyzer/internal/render.py +41 -0
  19. pub_analyzer/internal/report.py +497 -0
  20. pub_analyzer/internal/templates/author_report.typ +591 -0
  21. pub_analyzer/main.py +81 -0
  22. pub_analyzer/models/__init__.py +1 -0
  23. pub_analyzer/models/author.py +87 -0
  24. pub_analyzer/models/concept.py +19 -0
  25. pub_analyzer/models/institution.py +138 -0
  26. pub_analyzer/models/report.py +111 -0
  27. pub_analyzer/models/source.py +77 -0
  28. pub_analyzer/models/topic.py +59 -0
  29. pub_analyzer/models/work.py +158 -0
  30. pub_analyzer/widgets/__init__.py +1 -0
  31. pub_analyzer/widgets/author/__init__.py +1 -0
  32. pub_analyzer/widgets/author/cards.py +65 -0
  33. pub_analyzer/widgets/author/core.py +122 -0
  34. pub_analyzer/widgets/author/tables.py +50 -0
  35. pub_analyzer/widgets/body.py +55 -0
  36. pub_analyzer/widgets/common/__init__.py +18 -0
  37. pub_analyzer/widgets/common/card.py +29 -0
  38. pub_analyzer/widgets/common/filesystem.py +203 -0
  39. pub_analyzer/widgets/common/filters.py +111 -0
  40. pub_analyzer/widgets/common/input.py +97 -0
  41. pub_analyzer/widgets/common/label.py +36 -0
  42. pub_analyzer/widgets/common/modal.py +43 -0
  43. pub_analyzer/widgets/common/selector.py +66 -0
  44. pub_analyzer/widgets/common/summary.py +7 -0
  45. pub_analyzer/widgets/institution/__init__.py +1 -0
  46. pub_analyzer/widgets/institution/cards.py +78 -0
  47. pub_analyzer/widgets/institution/core.py +122 -0
  48. pub_analyzer/widgets/institution/tables.py +24 -0
  49. pub_analyzer/widgets/report/__init__.py +1 -0
  50. pub_analyzer/widgets/report/author.py +43 -0
  51. pub_analyzer/widgets/report/cards.py +130 -0
  52. pub_analyzer/widgets/report/concept.py +47 -0
  53. pub_analyzer/widgets/report/core.py +308 -0
  54. pub_analyzer/widgets/report/editor.py +80 -0
  55. pub_analyzer/widgets/report/export.py +112 -0
  56. pub_analyzer/widgets/report/grants.py +85 -0
  57. pub_analyzer/widgets/report/institution.py +39 -0
  58. pub_analyzer/widgets/report/locations.py +75 -0
  59. pub_analyzer/widgets/report/source.py +90 -0
  60. pub_analyzer/widgets/report/topic.py +55 -0
  61. pub_analyzer/widgets/report/work.py +391 -0
  62. pub_analyzer/widgets/search/__init__.py +11 -0
  63. pub_analyzer/widgets/search/core.py +96 -0
  64. pub_analyzer/widgets/search/results.py +82 -0
  65. pub_analyzer/widgets/sidebar.py +70 -0
  66. pub_analyzer-0.5.6.dist-info/METADATA +102 -0
  67. pub_analyzer-0.5.6.dist-info/RECORD +70 -0
  68. pub_analyzer-0.5.6.dist-info/WHEEL +4 -0
  69. pub_analyzer-0.5.6.dist-info/entry_points.txt +3 -0
  70. pub_analyzer-0.5.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,87 @@
1
+ """Authors models from OpenAlex API Schema definition."""
2
+
3
+ from typing import TypeAlias
4
+
5
+ from pydantic import BaseModel, Field, HttpUrl
6
+
7
+ from pub_analyzer.models.institution import DehydratedInstitution
8
+
9
+ AuthorOpenAlexID: TypeAlias = HttpUrl
10
+ """OpenAlex ID for Author Objects with the format `https://openalex.org/A000000000`"""
11
+
12
+ AuthorOpenAlexKey: TypeAlias = str
13
+ """OpenAlex author entity Key with the format `A000000000`"""
14
+
15
+
16
+ class AuthorIDs(BaseModel):
17
+ """IDs from an Author."""
18
+
19
+ openalex: AuthorOpenAlexID
20
+ orcid: HttpUrl | None = None
21
+ scopus: HttpUrl | None = None
22
+ twitter: HttpUrl | None = None
23
+ wikipedia: HttpUrl | None = None
24
+
25
+
26
+ class AuthorYearCount(BaseModel):
27
+ """Summary of published papers and number of citations in a year."""
28
+
29
+ year: int
30
+ works_count: int
31
+ cited_by_count: int
32
+
33
+
34
+ class AuthorSummaryStats(BaseModel):
35
+ """Citation metrics for this author."""
36
+
37
+ two_yr_mean_citedness: float = Field(..., alias="2yr_mean_citedness")
38
+ h_index: int
39
+ i10_index: int
40
+
41
+
42
+ class AuthorAffiliation(BaseModel):
43
+ """List of affiliations this author has claimed in their publications."""
44
+
45
+ institution: DehydratedInstitution
46
+ years: list[int]
47
+
48
+
49
+ class Author(BaseModel):
50
+ """Author Model Object from OpenAlex API definition."""
51
+
52
+ id: AuthorOpenAlexID
53
+ ids: AuthorIDs
54
+ orcid: str | None = ""
55
+
56
+ display_name: str
57
+ display_name_alternatives: list[str]
58
+ last_known_institutions: list[DehydratedInstitution] | None = Field(default_factory=list)
59
+ affiliations: list[AuthorAffiliation]
60
+
61
+ works_count: int
62
+ cited_by_count: int
63
+
64
+ counts_by_year: list[AuthorYearCount]
65
+ summary_stats: AuthorSummaryStats
66
+
67
+ works_api_url: str
68
+
69
+
70
+ class DehydratedAuthor(BaseModel):
71
+ """Stripped-down Author Model."""
72
+
73
+ id: AuthorOpenAlexID
74
+ display_name: str | None = None
75
+ orcid: HttpUrl | None = None
76
+
77
+
78
+ class AuthorResult(BaseModel):
79
+ """Author result Model resulting from a search in OpenAlex."""
80
+
81
+ id: AuthorOpenAlexID
82
+ display_name: str
83
+ hint: str | None = None
84
+ cited_by_count: int
85
+ works_count: int
86
+ entity_type: str
87
+ 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)."""
@@ -0,0 +1,138 @@
1
+ """Institutions models from OpenAlex API Schema definition."""
2
+
3
+ from enum import Enum
4
+ from typing import TypeAlias
5
+
6
+ from pydantic import BaseModel, Field, HttpUrl
7
+
8
+ InstitutionOpenAlexID: TypeAlias = HttpUrl
9
+ """OpenAlex ID for Institution Objects with the format `https://openalex.org/I000000000`"""
10
+
11
+ InstitutionOpenAlexKey: TypeAlias = str
12
+ """OpenAlex Institution entity Key with the format `I000000000`"""
13
+
14
+
15
+ class InstitutionIDs(BaseModel):
16
+ """IDs from an Institution."""
17
+
18
+ openalex: InstitutionOpenAlexID
19
+ grid: str | None = None
20
+ ror: HttpUrl | None = None
21
+ wikipedia: HttpUrl | None = None
22
+ wikidata: HttpUrl | None = None
23
+
24
+
25
+ class InstitutionType(str, Enum):
26
+ """The institution's primary type, using the ROR "type" controlled vocabulary."""
27
+
28
+ Education = "education"
29
+ Healthcare = "healthcare"
30
+ Company = "company"
31
+ Archive = "archive"
32
+ Nonprofit = "nonprofit"
33
+ Government = "government"
34
+ Facility = "facility"
35
+ Funder = "funder"
36
+ Other = "other"
37
+
38
+
39
+ class InstitutionSummaryStats(BaseModel):
40
+ """Citation metrics for this Institution."""
41
+
42
+ two_yr_mean_citedness: float = Field(..., alias="2yr_mean_citedness")
43
+ h_index: int
44
+ i10_index: int
45
+
46
+
47
+ class InstitutionYearCount(BaseModel):
48
+ """Summary of published papers and number of citations in a year."""
49
+
50
+ year: int
51
+ works_count: int
52
+ cited_by_count: int
53
+
54
+
55
+ class InstitutionGeo(BaseModel):
56
+ """Location of the institution."""
57
+
58
+ city: str
59
+ geonames_city_id: str
60
+
61
+ region: str | None = None
62
+ country_code: str | None = None
63
+ country: str
64
+
65
+ latitude: float
66
+ longitude: float
67
+
68
+
69
+ class InstitutionRoleType(str, Enum):
70
+ """Possible institution roles."""
71
+
72
+ funder = "funder"
73
+ publisher = "publisher"
74
+ institution = "institution"
75
+
76
+
77
+ class InstitutionRole(BaseModel):
78
+ """Institution role."""
79
+
80
+ role: InstitutionRoleType
81
+ id: HttpUrl
82
+ works_count: int
83
+
84
+
85
+ class International(BaseModel):
86
+ """The institution's display name in different languages."""
87
+
88
+ display_name: dict[str, str] | None = None
89
+
90
+
91
+ class Institution(BaseModel):
92
+ """Universities and other organizations to which authors claim affiliations."""
93
+
94
+ id: InstitutionOpenAlexID
95
+ ids: InstitutionIDs
96
+
97
+ display_name: str
98
+ country_code: str | None = None
99
+ type: InstitutionType
100
+ homepage_url: HttpUrl | None = None
101
+ image_url: HttpUrl | None = None
102
+
103
+ display_name_acronyms: list[str]
104
+ international: International
105
+
106
+ works_count: int
107
+ cited_by_count: int
108
+ summary_stats: InstitutionSummaryStats
109
+ counts_by_year: list[InstitutionYearCount]
110
+
111
+ geo: InstitutionGeo
112
+ roles: list[InstitutionRole]
113
+
114
+ works_api_url: str
115
+
116
+
117
+ class DehydratedInstitution(BaseModel):
118
+ """Stripped-down Institution Model."""
119
+
120
+ id: InstitutionOpenAlexID
121
+ ror: str
122
+ display_name: str
123
+ country_code: str | None = None
124
+ type: InstitutionType
125
+
126
+
127
+ class InstitutionResult(BaseModel):
128
+ """Institution result Model resulting from a search in OpenAlex."""
129
+
130
+ id: InstitutionOpenAlexID
131
+ display_name: str
132
+ hint: str | None = None
133
+
134
+ cited_by_count: int
135
+ works_count: int
136
+
137
+ entity_type: str
138
+ external_id: str | None = None
@@ -0,0 +1,111 @@
1
+ """Reports Structure Objects."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from .author import Author
8
+ from .institution import Institution
9
+ from .source import Source
10
+ from .work import OpenAccessStatus, Work
11
+
12
+
13
+ class CitationType(Enum):
14
+ """Citation type Work."""
15
+
16
+ TypeA = 0
17
+ TypeB = 1
18
+
19
+
20
+ class CitationReport(BaseModel):
21
+ """Cited by Works with stats."""
22
+
23
+ work: Work
24
+ citation_type: CitationType
25
+
26
+
27
+ class CitationSummary(BaseModel):
28
+ """Summary of citation information in all works."""
29
+
30
+ type_a_count: int = 0
31
+ type_b_count: int = 0
32
+
33
+ def add_cite_type(self, cite_type: CitationType) -> None:
34
+ """Add the type of cite in the corresponding counter."""
35
+ if cite_type.value == CitationType.TypeA.value:
36
+ self.type_a_count += 1
37
+ elif cite_type.value == CitationType.TypeB.value:
38
+ self.type_b_count += 1
39
+
40
+
41
+ class OpenAccessSummary(BaseModel):
42
+ """Open Access Type counter."""
43
+
44
+ diamond: int = 0
45
+ gold: int = 0
46
+ green: int = 0
47
+ hybrid: int = 0
48
+ bronze: int = 0
49
+ closed: int = 0
50
+
51
+ def add_oa_type(self, open_access_type: OpenAccessStatus) -> None:
52
+ """Add the type of Open Access in the corresponding counter."""
53
+ match open_access_type:
54
+ case OpenAccessStatus.diamond:
55
+ self.diamond += 1
56
+ case OpenAccessStatus.gold:
57
+ self.gold += 1
58
+ case OpenAccessStatus.green:
59
+ self.green += 1
60
+ case OpenAccessStatus.hybrid:
61
+ self.hybrid += 1
62
+ case OpenAccessStatus.bronze:
63
+ self.bronze += 1
64
+ case OpenAccessStatus.closed:
65
+ self.closed += 1
66
+
67
+
68
+ class WorkTypeCounter(BaseModel):
69
+ """Work Type Counter."""
70
+
71
+ type_name: str
72
+ count: int
73
+
74
+
75
+ class WorkReport(BaseModel):
76
+ """Work model with stats."""
77
+
78
+ work: Work
79
+ cited_by: list[CitationReport]
80
+
81
+ citation_summary: CitationSummary
82
+
83
+
84
+ class SourcesSummary(BaseModel):
85
+ """Sources model with stats."""
86
+
87
+ sources: list[Source]
88
+
89
+
90
+ class AuthorReport(BaseModel):
91
+ """Report of scientific production of an author."""
92
+
93
+ author: Author
94
+ works: list[WorkReport]
95
+
96
+ citation_summary: CitationSummary
97
+ open_access_summary: OpenAccessSummary
98
+ works_type_summary: list[WorkTypeCounter]
99
+ sources_summary: SourcesSummary
100
+
101
+
102
+ class InstitutionReport(BaseModel):
103
+ """Scientific production report of the Institution."""
104
+
105
+ institution: Institution
106
+ works: list[WorkReport]
107
+
108
+ citation_summary: CitationSummary
109
+ open_access_summary: OpenAccessSummary
110
+ works_type_summary: list[WorkTypeCounter]
111
+ sources_summary: SourcesSummary
@@ -0,0 +1,77 @@
1
+ """Sources models from OpenAlex API Schema definition."""
2
+
3
+ from pydantic import BaseModel, Field, HttpUrl
4
+
5
+
6
+ class SourceSummaryStats(BaseModel):
7
+ """Citation metrics for this Source."""
8
+
9
+ two_yr_mean_citedness: float = Field(..., alias="2yr_mean_citedness")
10
+ """The 2-year mean citedness for this source. Also known as impact factor."""
11
+ h_index: int
12
+ """The h-index for this source."""
13
+ i10_index: int
14
+ """The i-10 index for this source."""
15
+
16
+
17
+ class SourceYearCount(BaseModel):
18
+ """Summary of published papers and number of citations in a year."""
19
+
20
+ year: int
21
+ """Year."""
22
+ works_count: int
23
+ """The number of Works this source hosts in this year."""
24
+ cited_by_count: int
25
+ """The total number of Works that cite a Work hosted in this source in this year."""
26
+
27
+
28
+ class DehydratedSource(BaseModel):
29
+ """Stripped-down Source Model."""
30
+
31
+ id: HttpUrl
32
+ """The OpenAlex ID for this source."""
33
+ display_name: str
34
+ """The name of the source."""
35
+
36
+ issn_l: str | None = None
37
+ """The ISSN-L identifying this source. The ISSN-L designating a single canonical ISSN
38
+ for all media versions of the title. It's usually the same as the print ISSN.
39
+ """
40
+ issn: list[str] | None = None
41
+ """The ISSNs used by this source. An ISSN identifies all continuing resources, irrespective
42
+ of their medium (print or electronic). [More info](https://www.issn.org/){target=_blank}.
43
+ """
44
+
45
+ is_oa: bool
46
+ """Whether this is currently fully-open-access source."""
47
+ is_in_doaj: bool
48
+ """Whether this is a journal listed in the [Directory of Open Access Journals](https://doaj.org){target=_blank} (DOAJ)."""
49
+
50
+ host_organization: HttpUrl | None = None
51
+ """The host organization for this source as an OpenAlex ID. This will be an
52
+ [Institution.id][pub_analyzer.models.institution.Institution.id] if the source is a repository,
53
+ and a Publisher.id if the source is a journal, conference, or eBook platform
54
+ """
55
+ host_organization_name: str | None = None
56
+ """The display_name from the host_organization."""
57
+
58
+ type: str | None = None
59
+ """The type of source, which will be one of: `journal`, `repository`, `conference`,
60
+ `ebook platform`, or `book series`.
61
+ """
62
+
63
+
64
+ class Source(DehydratedSource):
65
+ """Where works are hosted."""
66
+
67
+ homepage_url: HttpUrl | None = None
68
+ """The homepage for this source's website."""
69
+
70
+ is_in_doaj: bool
71
+ """Whether this is a journal listed in the Directory of Open Access Journals (DOAJ)."""
72
+
73
+ summary_stats: SourceSummaryStats
74
+ """Citation metrics for this source."""
75
+
76
+ counts_by_year: list[SourceYearCount]
77
+ """works_count and cited_by_count for each of the last ten years, binned by year."""
@@ -0,0 +1,59 @@
1
+ """Topics models from OpenAlex API Schema definition."""
2
+
3
+ from pydantic import BaseModel, HttpUrl
4
+
5
+
6
+ class TopicIDs(BaseModel):
7
+ """External identifiers for a Topic."""
8
+
9
+ openalex: HttpUrl
10
+ """The OpenAlex ID for this Topic."""
11
+ wikipedia: HttpUrl | None = None
12
+ """This topic's Wikipedia page URL."""
13
+
14
+
15
+ class TopicLevel(BaseModel):
16
+ """Topic level information."""
17
+
18
+ id: HttpUrl
19
+ """ID for the topic level. For more info, consult the
20
+ [OpenAlex topic mapping table](https://docs.google.com/spreadsheets/d/1v-MAq64x4YjhO7RWcB-yrKV5D_2vOOsxl4u6GBKEXY8/){target=_blank}.
21
+ """
22
+ display_name: str
23
+ """The English-language label of the level."""
24
+
25
+
26
+ class DehydratedTopic(BaseModel):
27
+ """Stripped-down Topic Model."""
28
+
29
+ id: HttpUrl
30
+ """The OpenAlex ID for this Topic."""
31
+ display_name: str
32
+ """The English-language label of the topic."""
33
+ score: float
34
+ """The strength of the connection between the work and this topic (higher is stronger)."""
35
+
36
+ domain: TopicLevel
37
+ """The highest level in the Topics structure."""
38
+ field: TopicLevel
39
+ """The second-highest level in the Topics structure."""
40
+ subfield: TopicLevel
41
+ """The third-highest level in the Topics structure."""
42
+
43
+
44
+ class Topic(DehydratedTopic):
45
+ """Labels which can be used to describe what a paper is about."""
46
+
47
+ ids: TopicIDs
48
+ """All the external identifiers for a Topic."""
49
+ description: str
50
+ """A description of this topic, generated by AI."""
51
+ keywords: list[str]
52
+ """Keywords consisting of one or several words each, meant to represent
53
+ the content of the papers in the topic.
54
+ """
55
+
56
+ works_count: int
57
+ """The number of works tagged with this topic."""
58
+ cited_by_count: int
59
+ """The number of citations to works that have been tagged with this topic."""
@@ -0,0 +1,158 @@
1
+ """Works models from OpenAlex API Schema definition."""
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, HttpUrl, field_validator
7
+
8
+ from .author import DehydratedAuthor
9
+ from .concept import DehydratedConcept
10
+ from .source import DehydratedSource
11
+ from .topic import DehydratedTopic
12
+
13
+
14
+ class WorkIDs(BaseModel):
15
+ """IDs from a Work."""
16
+
17
+ openalex: HttpUrl
18
+ doi: HttpUrl | None = None
19
+ pmid: str | None = None
20
+ pmcid: str | None = None
21
+
22
+
23
+ class WorkDrivenVersion(str, Enum):
24
+ """The version of the work, based on the Driver Guidelines versioning scheme."""
25
+
26
+ submitted = "submittedVersion"
27
+ accepted = "acceptedVersion"
28
+ published = "publishedVersion"
29
+
30
+
31
+ class Location(BaseModel):
32
+ """Describes the location of a given work."""
33
+
34
+ is_oa: bool
35
+ landing_page_url: str
36
+ license: str | None
37
+ pdf_url: str | None
38
+ version: WorkDrivenVersion | None
39
+ source: DehydratedSource | None = None
40
+
41
+
42
+ class OpenAccessStatus(str, Enum):
43
+ """The Open Access (OA) status of this work."""
44
+
45
+ diamond = "diamond"
46
+ gold = "gold"
47
+ green = "green"
48
+ hybrid = "hybrid"
49
+ bronze = "bronze"
50
+ closed = "closed"
51
+
52
+
53
+ class WorkAccessInfo(BaseModel):
54
+ """Information about the access status of this work."""
55
+
56
+ is_oa: bool
57
+ oa_status: OpenAccessStatus
58
+ oa_url: str | None = None
59
+ any_repository_has_fulltext: bool
60
+
61
+
62
+ class Authorship(BaseModel):
63
+ """Information of author and her institutional affiliations in the context of work."""
64
+
65
+ author_position: str
66
+ author: DehydratedAuthor
67
+
68
+
69
+ class ArticleProcessingCharge(BaseModel):
70
+ """Information about the paid APC for this work."""
71
+
72
+ value: int
73
+ currency: str
74
+ provenance: str | None = None
75
+ value_usd: int | None
76
+
77
+
78
+ class Grant(BaseModel):
79
+ """Grant Model Object from OpenAlex API definition."""
80
+
81
+ funder: HttpUrl
82
+ funder_display_name: str
83
+ award_id: str | None = None
84
+
85
+
86
+ class Award(BaseModel):
87
+ """Award work details."""
88
+
89
+ id: HttpUrl
90
+ display_name: str | None = None
91
+ funder_award_id: str | None = None
92
+ funder_id: HttpUrl
93
+ funder_display_name: str
94
+ doi: str | None = None
95
+
96
+
97
+ class Keyword(BaseModel):
98
+ """Keyword extracted from the work's title and confidence score."""
99
+
100
+ id: HttpUrl
101
+ display_name: str
102
+ score: float
103
+
104
+
105
+ class Work(BaseModel):
106
+ """Work Model Object from OpenAlex API definition."""
107
+
108
+ id: HttpUrl
109
+ ids: WorkIDs
110
+
111
+ title: str
112
+ abstract: str | None = None
113
+ publication_year: int | None = None
114
+ publication_date: str | None = None
115
+ language: str | None = None
116
+ type: str
117
+
118
+ primary_location: Location | None = None
119
+ best_oa_location: Location | None = None
120
+ locations: list[Location]
121
+
122
+ open_access: WorkAccessInfo
123
+ authorships: list[Authorship]
124
+
125
+ cited_by_count: int
126
+ """This number comes from the OpenAlex API, represents ALL citations to this work, and may not always be correct.
127
+ To use a verified number that respects the applied filters use [WorkReport][pub_analyzer.models.report.WorkReport].
128
+ """
129
+
130
+ awards: list[Award]
131
+ keywords: list[Keyword]
132
+ concepts: list[DehydratedConcept]
133
+ topics: list[DehydratedTopic]
134
+
135
+ referenced_works: list[HttpUrl]
136
+
137
+ apc_list: ArticleProcessingCharge | None = None
138
+ """The price as listed by the journal's publisher."""
139
+ apc_paid: ArticleProcessingCharge | None = None
140
+ """APC actually paid by authors."""
141
+
142
+ @field_validator("locations", mode="before")
143
+ def valid_locations(cls, locations: list[dict[str, Any]]) -> list[dict[str, Any]]:
144
+ """Skip locations that do not contain enough data."""
145
+ return [location for location in locations if location["landing_page_url"] is not None]
146
+
147
+ @field_validator("primary_location", "best_oa_location", mode="before")
148
+ def valid_location(cls, location: dict[str, Any]) -> dict[str, Any] | None:
149
+ """Skip location that do not contain enough data."""
150
+ if location and location["landing_page_url"] is None:
151
+ return None
152
+ else:
153
+ return location
154
+
155
+ @field_validator("authorships", mode="before")
156
+ def valid_authorships(cls, authorships: list[dict[str, Any]]) -> list[dict[str, Any]]:
157
+ """Skip authorship's that do not contain enough data."""
158
+ return [authorship for authorship in authorships if authorship["author"].get("id") is not None]
@@ -0,0 +1 @@
1
+ """TUI module."""
@@ -0,0 +1 @@
1
+ """Author Widgets."""