Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: astral-sh/setup-uv@v2
with:
enable-cache: true
version: "0.6.0"
version: "0.8.13"

- name: Install dependencies
run: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
name: CI for ${{ matrix.python-version }}
runs-on: ubuntu-latest
env:
USING_COVERAGE: "3.9,3.13"
USING_COVERAGE: "3.11,3.13"

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -30,7 +30,7 @@ jobs:
uses: astral-sh/setup-uv@v2
with:
enable-cache: true
version: "0.6.0"
version: "0.8.13"

- name: Run tests for ${{ matrix.python-version }}
run: |
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ dependencies = [
"bibtexparser~=1.4.0",
"networkx~=3.0",
"pydantic~=2.11.7",
"requests~=2.32.3",
"typer~=0.16.0",
"requests~=2.32.5",
"typer~=0.17.3",
]
requires-python = ">=3.9"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
Expand All @@ -39,8 +39,8 @@ dev = [
"pre-commit~=4.3.0",
"ruff~=0.12.11",
"mypy~=1.17.1",
"types-requests>=2.32.0.20241016",
"ipython>=8.18.1",
"types-requests~=2.32.4.20250809",
"ipython~=9.5.0",
]

[project.scripts]
Expand Down Expand Up @@ -116,7 +116,7 @@ path = "src/bibx/__init__.py"

[tool.tox]
requires = ["tox>=4.24.2"]
env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"]
env_list = ["3.11", "3.12", "3.13"]

[tool.tox.env_run_base]
deps = [
Expand Down
26 changes: 13 additions & 13 deletions src/bibx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from typing import TextIO

from bibx.algorithms.sap import Sap
from bibx.article import Article
from bibx.builders.openalex import EnrichReferences, OpenAlexCollectionBuilder
from bibx.builders.scopus_bib import ScopusBibCollectionBuilder
from bibx.builders.scopus_csv import ScopusCsvCollectionBuilder
from bibx.builders.scopus_ris import ScopusRisCollectionBuilder
from bibx.builders.wos import WosCollectionBuilder
from bibx.collection import Collection
from bibx.exceptions import BibXError
from bibx.models.article import Article
from bibx.models.collection import Collection
from bibx.sources.openalex import EnrichReferences, OpenAlexSource
from bibx.sources.scopus_bib import ScopusBibSource
from bibx.sources.scopus_csv import ScopusCsvSource
from bibx.sources.scopus_ris import ScopusRisSource
from bibx.sources.wos import WosSource

logger = logging.getLogger(__name__)

Expand All @@ -28,7 +28,7 @@
"read_wos",
]

__version__ = "0.7.2"
__version__ = "0.8.0"


def query_openalex(
Expand All @@ -37,7 +37,7 @@ def query_openalex(
enrich: EnrichReferences = EnrichReferences.BASIC,
) -> Collection:
"""Query OpenAlex and return a collection."""
return OpenAlexCollectionBuilder(query, limit, enrich=enrich).build()
return OpenAlexSource(query, limit, enrich=enrich).build()


def read_scopus_bib(*files: TextIO) -> Collection:
Expand All @@ -46,7 +46,7 @@ def read_scopus_bib(*files: TextIO) -> Collection:
:param files: Scopus bib files open.
:return: the collection
"""
return ScopusBibCollectionBuilder(*files).build()
return ScopusBibSource(*files).build()


def read_scopus_ris(*files: TextIO) -> Collection:
Expand All @@ -55,7 +55,7 @@ def read_scopus_ris(*files: TextIO) -> Collection:
:param files: Scopus bib files open.
:return: the collection
"""
return ScopusRisCollectionBuilder(*files).build()
return ScopusRisSource(*files).build()


def read_scopus_csv(*files: TextIO) -> Collection:
Expand All @@ -64,7 +64,7 @@ def read_scopus_csv(*files: TextIO) -> Collection:
:param files: Scopus csv files open.
:return: the collection
"""
return ScopusCsvCollectionBuilder(*files).build()
return ScopusCsvSource(*files).build()


def read_wos(*files: TextIO) -> Collection:
Expand All @@ -73,7 +73,7 @@ def read_wos(*files: TextIO) -> Collection:
:param files: WoS files open.
:return: the collection
"""
return WosCollectionBuilder(*files).build()
return WosSource(*files).build()


def read_any(file: TextIO) -> Collection:
Expand Down
4 changes: 2 additions & 2 deletions src/bibx/algorithms/sap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import networkx as nx
from networkx.algorithms.community.louvain import louvain_communities

from bibx.article import Article
from bibx.collection import Collection
from bibx.models.article import Article
from bibx.models.collection import Collection

YEAR = "year"
LEAF = "leaf"
Expand Down
2 changes: 1 addition & 1 deletion src/bibx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
read_wos,
)
from bibx.algorithms.sap import Sap
from bibx.builders.openalex import EnrichReferences
from bibx.sources.openalex import EnrichReferences

app = typer.Typer()

Expand Down
29 changes: 14 additions & 15 deletions src/bibx/clients/openalex.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed, wait
from enum import Enum
from typing import Optional, Union

import requests
from pydantic import BaseModel, ValidationError
Expand Down Expand Up @@ -29,7 +28,7 @@ class Author(BaseModel):

id: str
display_name: str
orcid: Optional[str] = None
orcid: str | None = None


class WorkAuthorship(BaseModel):
Expand All @@ -51,10 +50,10 @@ class WorkKeyword(BaseModel):
class WorkBiblio(BaseModel):
"""Work bibliographic information from the openalex API."""

volume: Optional[str] = None
issue: Optional[str] = None
first_page: Optional[str] = None
last_page: Optional[str] = None
volume: str | None = None
issue: str | None = None
first_page: str | None = None
last_page: str | None = None


class WorkLocationSource(BaseModel):
Expand All @@ -69,25 +68,25 @@ class WorkLoacation(BaseModel):
"""Location of the work from the openalex API."""

is_oa: bool
landing_page_url: Optional[str] = None
pdf_url: Optional[str] = None
source: Optional[WorkLocationSource]
landing_page_url: str | None = None
pdf_url: str | None = None
source: WorkLocationSource | None


class Work(BaseModel):
"""A work from the openalex API."""

id: str
ids: dict[str, str]
doi: Optional[str] = None
title: Optional[str] = None
doi: str | None = None
title: str | None = None
publication_year: int
authorships: list[WorkAuthorship]
cited_by_count: int
keywords: list[WorkKeyword]
referenced_works: list[str]
biblio: WorkBiblio
primary_location: Optional[WorkLoacation] = None
primary_location: WorkLoacation | None = None


class ResponseMeta(BaseModel):
Expand All @@ -110,8 +109,8 @@ class OpenAlexClient:

def __init__(
self,
base_url: Optional[str] = None,
email: Optional[str] = None,
base_url: str | None = None,
email: str | None = None,
) -> None:
self.base_url = base_url or "https://api.openalex.org"
self.session = requests.Session()
Expand All @@ -124,7 +123,7 @@ def __init__(
}
)

def _fetch_works(self, params: dict[str, Union[str, int]]) -> WorkResponse:
def _fetch_works(self, params: dict[str, str | int]) -> WorkResponse:
response = self.session.get(
f"{self.base_url}/works",
params=params,
Expand Down
1 change: 1 addition & 0 deletions src/bibx/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Models for the bibx package."""
28 changes: 14 additions & 14 deletions src/bibx/article.py → src/bibx/models/article.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Optional, TypeVar, Union
from typing import TypeVar

T = TypeVar("T")

Expand All @@ -20,15 +20,15 @@ class Article:
label: str
ids: set[str]
authors: list[str] = field(default_factory=list)
year: Optional[int] = None
title: Optional[str] = None
journal: Optional[str] = None
volume: Optional[str] = None
issue: Optional[str] = None
page: Optional[str] = None
doi: Optional[str] = None
_permalink: Optional[str] = None
times_cited: Optional[int] = None
year: int | None = None
title: str | None = None
journal: str | None = None
volume: str | None = None
issue: str | None = None
page: str | None = None
doi: str | None = None
_permalink: str | None = None
times_cited: int | None = None
references: list["Article"] = field(default_factory=list)
keywords: list[str] = field(default_factory=list)
sources: set[str] = field(default_factory=set)
Expand Down Expand Up @@ -61,7 +61,7 @@ def key(self) -> str:
return next(iter(sorted(self.ids)))

@property
def simple_label(self) -> Optional[str]:
def simple_label(self) -> str | None:
"""Return a simple label for the article."""
pieces = {
"AU": self.authors[0].replace(",", "") if self.authors else None,
Expand All @@ -76,7 +76,7 @@ def simple_label(self) -> Optional[str]:
return ", ".join(value for value in pieces.values() if value)

@property
def permalink(self) -> Optional[str]:
def permalink(self) -> str | None:
"""Return the permalink of the article."""
if self._permalink is not None:
return self._permalink
Expand All @@ -85,7 +85,7 @@ def permalink(self) -> Optional[str]:
return None

@property
def simple_id(self) -> Optional[str]:
def simple_id(self) -> str | None:
"""Return a simple ID for the article."""
if not self.authors or self.year is None:
return None
Expand All @@ -112,7 +112,7 @@ def set_simple_label(self) -> "Article":

def info(
self,
) -> dict[str, Union[str, int, list[str], None]]:
) -> dict[str, str | int | list[str] | None]:
"""Return a dictionary with the information of the article."""
return {
"permalink": self.permalink,
Expand Down
4 changes: 2 additions & 2 deletions src/bibx/collection.py → src/bibx/models/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def published_by_year(self) -> dict[int, int]:

:return: a dictionary with the number of articles published each year.
"""
current_year = datetime.datetime.now(datetime.timezone.utc).year
current_year = datetime.datetime.now(datetime.UTC).year
years = {}
for year in range(self._first_year, current_year + 1):
years[year] = 0
Expand All @@ -164,7 +164,7 @@ def cited_by_year(self) -> dict[int, int]:

:return: a dictionary with the number of citations each year.
"""
current_year = datetime.datetime.now(datetime.timezone.utc).year
current_year = datetime.datetime.now(datetime.UTC).year
cited_items_per_year = {}
for year in range(self._first_year, current_year + 1):
cited_items_per_year[year] = 0
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/bibx/builders/base.py → src/bibx/sources/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Protocol

from bibx.collection import Collection
from bibx.models.collection import Collection


class CollectionBuilder(Protocol):
class Source(Protocol):
"""Protocol for classes that build collections of articles."""

def build(self) -> Collection:
Expand Down
11 changes: 5 additions & 6 deletions src/bibx/builders/openalex.py → src/bibx/sources/openalex.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import logging
from collections import Counter
from enum import Enum
from typing import Optional
from urllib.parse import urlparse

from bibx.article import Article
from bibx.clients.openalex import OpenAlexClient, Work
from bibx.collection import Collection
from bibx.models.article import Article
from bibx.models.collection import Collection

from .base import CollectionBuilder
from .base import Source

logger = logging.getLogger(__name__)

Expand All @@ -25,15 +24,15 @@ class EnrichReferences(Enum):
FULL = "full"


class OpenAlexCollectionBuilder(CollectionBuilder):
class OpenAlexSource(Source):
"""Builder for collections of articles from the OpenAlex API."""

def __init__(
self,
query: str,
limit: int = 600,
enrich: EnrichReferences = EnrichReferences.BASIC,
client: Optional[OpenAlexClient] = None,
client: OpenAlexClient | None = None,
) -> None:
self.query = query
self.limit = limit
Expand Down
Loading
Loading