oeis-tools 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Enrique Pérez Herrero
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include tests *
4
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: oeis-tools
3
+ Version: 0.1.0
4
+ Summary: Tools and utilities for working with OEIS integer sequences
5
+ Author-email: Enrique Pérez Herrero <energycode.org@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/oeistools/oeis-tools
8
+ Project-URL: Repository, https://github.com/oeistools/oeis-tools
9
+ Project-URL: Issues, https://github.com/oeistools/oeis-tools/issues
10
+ Project-URL: Documentation, https://github.com/oeistools/oeis-tools#readme
11
+ Keywords: oeis,integer-sequences,number-theory,math
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.31
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.4; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # oeis-tools
29
+
30
+ [![Tests](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml/badge.svg)](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
31
+ [![PyPI version](https://img.shields.io/pypi/v/oeis-tools.svg)](https://pypi.org/project/oeis-tools/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+
34
+ `oeis-tools` is a Python package for working with the
35
+ [Online Encyclopedia of Integer Sequences (OEIS)](https://oeis.org/).
36
+
37
+ It provides:
38
+ - OEIS ID validation
39
+ - URL and b-file helpers
40
+ - A `Sequence` class for OEIS JSON entries
41
+ - A `BFile` class for b-file numeric data
42
+
43
+ ## Installation
44
+
45
+ From PyPI:
46
+
47
+ ```bash
48
+ pip install oeis-tools
49
+ ```
50
+
51
+ From source:
52
+
53
+ ```bash
54
+ git clone https://github.com/oeistools/oeis-tools.git
55
+ cd oeis-tools
56
+ pip install -e .
57
+ ```
58
+
59
+ Development dependencies (tests):
60
+
61
+ ```bash
62
+ pip install -e ".[dev]"
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ import oeis_tools
69
+
70
+ # Utilities
71
+ print(oeis_tools.check_id("A000001")) # True
72
+ print(oeis_tools.oeis_bfile("A000001")) # b000001.txt
73
+ print(oeis_tools.oeis_url("A000001", fmt="json")) # https://oeis.org/search?q=id:A000001&fmt=json
74
+
75
+ # Sequence API
76
+ seq = oeis_tools.Sequence("A000045")
77
+ print(seq.name) # Fibonacci numbers
78
+ print(seq.data) # comma-separated terms
79
+ print(seq.author) # list[str]
80
+ print(seq.json["id"]) # raw JSON field access
81
+
82
+ # B-file API
83
+ bfile = oeis_tools.BFile("A000045")
84
+ print(bfile.get_filename()) # b000045.txt
85
+ print(bfile.get_url()) # https://oeis.org/A000045/b000045.txt
86
+ print(bfile.get_bfile_data()[:5])
87
+ ```
88
+
89
+ ## API Overview
90
+
91
+ - `check_id(oeis_id: str) -> bool`
92
+ - `oeis_bfile(oeis_id: str) -> str`
93
+ - `oeis_url(oeis_id: str, fmt: str | None = None) -> str`
94
+ - `Sequence(oeis_id: str)`
95
+ - Key attributes: `id`, `json`, `name`, `data`, `author`, `link`, `bfile`
96
+ - `BFile(oeis_id: str)`
97
+ - Methods: `get_filename()`, `get_url()`, `get_bfile_data()`
98
+
99
+ ## Running Tests
100
+
101
+ ```bash
102
+ pytest -q
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT. See `LICENSE`.
108
+
109
+ ## Author
110
+
111
+ Enrique Pérez Herrero - [energycode.org@gmail.com](mailto:energycode.org@gmail.com)
@@ -0,0 +1,84 @@
1
+ # oeis-tools
2
+
3
+ [![Tests](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml/badge.svg)](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/oeis-tools.svg)](https://pypi.org/project/oeis-tools/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ `oeis-tools` is a Python package for working with the
8
+ [Online Encyclopedia of Integer Sequences (OEIS)](https://oeis.org/).
9
+
10
+ It provides:
11
+ - OEIS ID validation
12
+ - URL and b-file helpers
13
+ - A `Sequence` class for OEIS JSON entries
14
+ - A `BFile` class for b-file numeric data
15
+
16
+ ## Installation
17
+
18
+ From PyPI:
19
+
20
+ ```bash
21
+ pip install oeis-tools
22
+ ```
23
+
24
+ From source:
25
+
26
+ ```bash
27
+ git clone https://github.com/oeistools/oeis-tools.git
28
+ cd oeis-tools
29
+ pip install -e .
30
+ ```
31
+
32
+ Development dependencies (tests):
33
+
34
+ ```bash
35
+ pip install -e ".[dev]"
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ import oeis_tools
42
+
43
+ # Utilities
44
+ print(oeis_tools.check_id("A000001")) # True
45
+ print(oeis_tools.oeis_bfile("A000001")) # b000001.txt
46
+ print(oeis_tools.oeis_url("A000001", fmt="json")) # https://oeis.org/search?q=id:A000001&fmt=json
47
+
48
+ # Sequence API
49
+ seq = oeis_tools.Sequence("A000045")
50
+ print(seq.name) # Fibonacci numbers
51
+ print(seq.data) # comma-separated terms
52
+ print(seq.author) # list[str]
53
+ print(seq.json["id"]) # raw JSON field access
54
+
55
+ # B-file API
56
+ bfile = oeis_tools.BFile("A000045")
57
+ print(bfile.get_filename()) # b000045.txt
58
+ print(bfile.get_url()) # https://oeis.org/A000045/b000045.txt
59
+ print(bfile.get_bfile_data()[:5])
60
+ ```
61
+
62
+ ## API Overview
63
+
64
+ - `check_id(oeis_id: str) -> bool`
65
+ - `oeis_bfile(oeis_id: str) -> str`
66
+ - `oeis_url(oeis_id: str, fmt: str | None = None) -> str`
67
+ - `Sequence(oeis_id: str)`
68
+ - Key attributes: `id`, `json`, `name`, `data`, `author`, `link`, `bfile`
69
+ - `BFile(oeis_id: str)`
70
+ - Methods: `get_filename()`, `get_url()`, `get_bfile_data()`
71
+
72
+ ## Running Tests
73
+
74
+ ```bash
75
+ pytest -q
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT. See `LICENSE`.
81
+
82
+ ## Author
83
+
84
+ Enrique Pérez Herrero - [energycode.org@gmail.com](mailto:energycode.org@gmail.com)
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oeis-tools"
7
+ dynamic = ["version"]
8
+ description = "Tools and utilities for working with OEIS integer sequences"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Enrique Pérez Herrero", email = "energycode.org@gmail.com" }
14
+ ]
15
+
16
+ keywords = [
17
+ "oeis",
18
+ "integer-sequences",
19
+ "number-theory",
20
+ "math"
21
+ ]
22
+
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Science/Research",
26
+ "Topic :: Scientific/Engineering :: Mathematics",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.9",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12"
32
+ ]
33
+
34
+ dependencies = [
35
+ "requests>=2.31"
36
+ ]
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=7.4"
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/oeistools/oeis-tools"
45
+ Repository = "https://github.com/oeistools/oeis-tools"
46
+ Issues = "https://github.com/oeistools/oeis-tools/issues"
47
+ Documentation = "https://github.com/oeistools/oeis-tools#readme"
48
+
49
+ [tool.setuptools]
50
+ package-dir = {"" = "src"}
51
+
52
+ [tool.setuptools.dynamic]
53
+ version = {attr = "oeis_tools.__version__.__version__"}
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ """
2
+ oeis_tools: A Python package for reading and manipulating data from OEIS.org.
3
+
4
+ This package provides an object-oriented interface to fetch, parse, and work with
5
+ integer sequences from the Online Encyclopedia of Integer Sequences (OEIS).
6
+ It follows PEP 8 style guidelines and emphasizes OOP principles for extensibility.
7
+
8
+ Key components:
9
+ - Sequence: Core class for representing and interacting with OEIS sequences.
10
+
11
+ Example usage:
12
+ from oeis_tools import Sequence
13
+
14
+ seq = Sequence.from_id('A000045') # Fetch Fibonacci sequence
15
+ print(seq.terms[:12]) # Output first 12 terms
16
+
17
+ For more details, refer to the documentation in individual modules.
18
+ """
19
+
20
+ from .__version__ import __version__
21
+ from .utils import check_id, oeis_bfile, oeis_url
22
+ from .utils import OEIS_URL
23
+ from .bfile import BFile
24
+ from .sequence import Sequence
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "check_id",
29
+ "oeis_bfile",
30
+ "oeis_url",
31
+ "OEIS_URL",
32
+ "BFile",
33
+ "Sequence",
34
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Version information for oeis_tools package.
3
+
4
+ This module defines the package version following PEP 8 conventions.
5
+ It can be imported in __init__.py for easy access to the version string.
6
+ """
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,98 @@
1
+ """
2
+ Utilities for working with OEIS b-files.
3
+
4
+ This module provides the BFile class, which represents an OEIS b-file
5
+ associated with an integer sequence. A b-file contains sequence values
6
+ in the format "n a(n)", one pair per line.
7
+
8
+ The BFile class handles:
9
+ - construction of b-file filenames and URLs
10
+ - downloading b-files from the OEIS website
11
+ - parsing numeric sequence data
12
+
13
+ Typical usage:
14
+ >>> from oeis_tools.bfile import BFile
15
+ >>> bfile = BFile("A000045")
16
+ >>> bfile.get_bfile_data()
17
+ """
18
+
19
+ import requests
20
+ from .utils import oeis_bfile, oeis_url
21
+
22
+
23
+ class BFile:
24
+ """
25
+ Represents an OEIS b-file and provides access to its numeric data.
26
+
27
+ A b-file contains values of an integer sequence in the form:
28
+ n a(n), one pair per line. This class fetches the b-file from
29
+ the OEIS website and parses its contents into a list of integers.
30
+
31
+ Attributes:
32
+ oeis_id (str): The OEIS identifier (e.g., 'A000045').
33
+ filename (str): The b-file name (e.g., 'bA000045.txt').
34
+ url (str): The URL where the b-file can be downloaded.
35
+ bfile_data (list[int] or None): Parsed sequence values from the
36
+ b-file, or None if the b-file could not be retrieved or parsed.
37
+ """
38
+
39
+ def __init__(self, oeis_id):
40
+ self.oeis_id = oeis_id
41
+ self.filename = oeis_bfile(oeis_id)
42
+ self.url = oeis_url(oeis_id, fmt="bfile")
43
+ self.data = self.fetch_bfile_data()
44
+
45
+ def fetch_bfile_data(self):
46
+ """
47
+ Fetch and parse the b-file into a list of integers.
48
+
49
+ Returns:
50
+ list[int] or None: Parsed sequence values, or None on failure.
51
+ """
52
+ try:
53
+ response = requests.get(self.url, timeout=10)
54
+ response.raise_for_status()
55
+ except requests.RequestException:
56
+ return None
57
+
58
+ data = []
59
+ for line in response.text.splitlines():
60
+ line = line.strip()
61
+ if not line or line.startswith("#"):
62
+ continue
63
+ try:
64
+ # format: n a(n)
65
+ _, value = line.split()
66
+ data.append(int(value))
67
+ except (ValueError, IndexError):
68
+ return None
69
+
70
+ return data
71
+
72
+ def get_filename(self):
73
+ """
74
+ Return the local filename of the OEIS b-file.
75
+
76
+ Returns:
77
+ str: The b-file filename (e.g., 'bA000045.txt').
78
+ """
79
+ return self.filename
80
+
81
+ def get_url(self):
82
+ """
83
+ Return the URL of the OEIS b-file.
84
+
85
+ Returns:
86
+ str: The full URL pointing to the b-file on oeis.org.
87
+ """
88
+ return self.url
89
+
90
+ def get_bfile_data(self):
91
+ """
92
+ Return the numeric data parsed from the OEIS b-file.
93
+
94
+ Returns:
95
+ list[int] or None: A list of sequence values extracted from the
96
+ b-file, or None if the b-file could not be fetched or parsed.
97
+ """
98
+ return self.data
@@ -0,0 +1,265 @@
1
+ """Tools and utilities for working with OEIS integer sequences."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ import requests
6
+
7
+ from .__version__ import __version__
8
+ from .utils import check_id, oeis_bfile, oeis_url, OEIS_URL
9
+ from .bfile import BFile
10
+
11
+ class Sequence:
12
+ """
13
+ A class to represent an OEIS sequence, fetching data from the JSON API.
14
+
15
+ Attributes:
16
+ id (str): The OEIS ID.
17
+ json (dict): The JSON data fetched from OEIS for the sequence.
18
+ m_id (str or None): The M ID from the 'id' field (e.g., 'M0692'), or None.
19
+ n_id (str or None): The N ID from the 'id' field (e.g., 'N0256'), or None.
20
+ time (datetime or None): The last modification time from the 'time' field.
21
+ created (datetime or None): The creation time from the 'created' field.
22
+ link (str): Formatted links from the 'link' field as printable text with hyperlinks.
23
+ BFile (BFile or None): The BFile object if available, else None.
24
+ data (str): The sequence data from the 'data' field.
25
+ name (str): The sequence name from the 'name' field.
26
+ comment (str): Comments from the 'comment' field.
27
+ reference (str): References from the 'reference' field.
28
+ formula (str): Formulas from the 'formula' field.
29
+ example (str): Examples from the 'example' field.
30
+ maple (str): Maple code from the 'maple' field.
31
+ mathematica (str): Mathematica code from the 'mathematica' field.
32
+ program (str): Programs from the 'program' field.
33
+ xref (str): Cross-references from the 'xref' field.
34
+ keyword (list[str]): Keywords parsed from the 'keyword' field.
35
+ offset (list[int]): Offset values parsed from the 'offset' field.
36
+ author (list[str]): Author names parsed from the 'author' field.
37
+ references (str): Additional references from the 'references' field.
38
+ revision (str): Revision information from the 'revision' field.
39
+ """
40
+
41
+ def __init__(self, oeis_id):
42
+ """
43
+ Initialize the Sequence with the given OEIS ID.
44
+
45
+ Args:
46
+ oeis_id (str): The OEIS ID, e.g., 'A000001'.
47
+
48
+ Raises:
49
+ ValueError: If the oeis_id is invalid.
50
+ requests.HTTPError: If the request fails.
51
+ """
52
+ if not check_id(oeis_id):
53
+ raise ValueError(f"Invalid OEIS ID: {oeis_id}")
54
+
55
+ json_url = oeis_url(oeis_id, fmt="json")
56
+ response = requests.get(json_url, timeout=10)
57
+ response.raise_for_status()
58
+ self.json = response.json()[0]
59
+ self.id = oeis_id
60
+
61
+ # Add direct attributes from json
62
+ self.data = self.json.get('data', '')
63
+ self.name = self.json.get('name', '')
64
+ comment_raw = self.json.get('comment', [])
65
+ self.comment = ('\n'.join(comment_raw) if isinstance(comment_raw, list)
66
+ else comment_raw)
67
+ reference_raw = self.json.get('reference', [])
68
+ self.reference = ('\n'.join(reference_raw) if isinstance(reference_raw, list)
69
+ else reference_raw)
70
+ formula_raw = self.json.get('formula', [])
71
+ self.formula = ('\n'.join(formula_raw) if isinstance(formula_raw, list)
72
+ else formula_raw)
73
+ example_raw = self.json.get('example', [])
74
+ self.example = ('\n'.join(example_raw) if isinstance(example_raw, list)
75
+ else example_raw)
76
+ maple_raw = self.json.get('maple', [])
77
+ self.maple = ('\n'.join(maple_raw) if isinstance(maple_raw, list)
78
+ else maple_raw)
79
+ mathematica_raw = self.json.get('mathematica', [])
80
+ self.mathematica = ('\n'.join(mathematica_raw) if isinstance(mathematica_raw, list)
81
+ else mathematica_raw)
82
+ program_raw = self.json.get('program', [])
83
+ self.program = ('\n'.join(program_raw) if isinstance(program_raw, list)
84
+ else program_raw)
85
+ xref_raw = self.json.get('xref', [])
86
+ self.xref = ('\n'.join(xref_raw) if isinstance(xref_raw, list)
87
+ else xref_raw)
88
+ self.keyword = self._parse_keywords(self.json.get('keyword', ''))
89
+ self.offset = self._parse_offset(self.json.get('offset', ''))
90
+ self.author = self._parse_authors(self.json.get('author', ''))
91
+ references_raw = self.json.get('references', [])
92
+ self.references = ('\n'.join(references_raw) if isinstance(references_raw, list)
93
+ else references_raw)
94
+ self.revision = self.json.get('revision', '')
95
+
96
+ # Parse M and N IDs from the 'id' field
97
+ id_str = self.json.get('id', '')
98
+ parts = id_str.split() if id_str else []
99
+ self.m_id = parts[0] if parts else None
100
+ self.n_id = parts[1] if len(parts) > 1 else None
101
+
102
+ # Parse time and created as datetime objects
103
+ time_str = self.json.get('time')
104
+ self.time = datetime.fromisoformat(time_str) if time_str else None
105
+ created_str = self.json.get('created')
106
+ self.created = datetime.fromisoformat(created_str) if created_str else None
107
+
108
+ # Parse links as formatted text with hyperlinks
109
+ links = self.json.get('link', [])
110
+ formatted_links = []
111
+ for link in links:
112
+ # Parse HTML <a href="url">text</a> and convert to Markdown [text](url)
113
+ match = re.search(r'<a href="([^"]*)">(.*?)</a>', link)
114
+ if match:
115
+ url, text = match.groups()
116
+ if url.startswith('/'):
117
+ url = OEIS_URL + url
118
+ formatted_links.append(f"[{text}]({url})")
119
+ else:
120
+ # If no <a>, just add the text, but replace relative URLs
121
+ formatted_link = re.sub(r'href="/', f'href="{OEIS_URL}/', link)
122
+ formatted_links.append(formatted_link)
123
+ self.link = '\n'.join(formatted_links) if formatted_links else ''
124
+
125
+ # Fetch BFile if content if b-file link is present
126
+ self.bfile = BFile(self.id)
127
+
128
+ def get_bfile_info(self):
129
+ """
130
+ Return summary information about the attached b-file data.
131
+
132
+ Returns:
133
+ dict: Metadata and basic stats for b-file values.
134
+ """
135
+ data = self.bfile.get_bfile_data() if self.bfile else None
136
+ if data is None:
137
+ return {
138
+ "available": False,
139
+ "filename": self.bfile.get_filename() if self.bfile else None,
140
+ "url": self.bfile.get_url() if self.bfile else None,
141
+ "length": 0,
142
+ "first": None,
143
+ "last": None,
144
+ "min": None,
145
+ "max": None,
146
+ }
147
+
148
+ return {
149
+ "available": True,
150
+ "filename": self.bfile.get_filename(),
151
+ "url": self.bfile.get_url(),
152
+ "length": len(data),
153
+ "first": data[0] if data else None,
154
+ "last": data[-1] if data else None,
155
+ "min": min(data) if data else None,
156
+ "max": max(data) if data else None,
157
+ }
158
+
159
+ @staticmethod
160
+ def _parse_authors(author_raw):
161
+ """
162
+ Parse OEIS author field into a clean list of author names.
163
+
164
+ The OEIS author field may include markdown-like underscores for
165
+ emphasis (e.g. ``_Tom Verhoeff_, _N. J. A. Sloane_``). This
166
+ method removes wrapper underscores and returns a list of names.
167
+
168
+ Args:
169
+ author_raw (str or list[str]): Raw author value from OEIS JSON.
170
+
171
+ Returns:
172
+ list[str]: Cleaned author names.
173
+ """
174
+ if isinstance(author_raw, list):
175
+ chunks = author_raw
176
+ elif isinstance(author_raw, str):
177
+ chunks = author_raw.split(",")
178
+ else:
179
+ return []
180
+
181
+ authors = []
182
+ for chunk in chunks:
183
+ name = chunk.strip()
184
+ name = re.sub(r'^_+|_+$', '', name).strip()
185
+ if Sequence._is_date_token(name):
186
+ continue
187
+ if name:
188
+ authors.append(name)
189
+ return authors
190
+
191
+ @staticmethod
192
+ def _is_date_token(value):
193
+ """
194
+ Return True when a token is a date-like value, not an author name.
195
+
196
+ Examples that should be filtered:
197
+ - ``1964``
198
+ - ``Apr 28 2012``
199
+ - ``Apr 28, 2012``
200
+ - ``2012-04-28``
201
+ """
202
+ if re.fullmatch(r"\d{4}", value):
203
+ return True
204
+
205
+ date_patterns = [
206
+ r"[A-Za-z]{3,9}\.? \d{1,2},? \d{4}",
207
+ r"\d{1,2} [A-Za-z]{3,9}\.? \d{4}",
208
+ r"\d{4}-\d{2}-\d{2}",
209
+ ]
210
+ return any(re.fullmatch(pattern, value) for pattern in date_patterns)
211
+
212
+ @staticmethod
213
+ def _parse_offset(offset_raw):
214
+ """
215
+ Parse OEIS offset field into a list of integers.
216
+
217
+ Typical OEIS values look like ``"0,2"`` and are returned as ``[0, 2]``.
218
+ """
219
+ if isinstance(offset_raw, list):
220
+ tokens = offset_raw
221
+ elif isinstance(offset_raw, str):
222
+ tokens = offset_raw.split(",")
223
+ else:
224
+ return []
225
+
226
+ offsets = []
227
+ for token in tokens:
228
+ value = str(token).strip()
229
+ if not value:
230
+ continue
231
+ try:
232
+ offsets.append(int(value))
233
+ except ValueError:
234
+ continue
235
+ return offsets
236
+
237
+ @staticmethod
238
+ def _parse_keywords(keyword_raw):
239
+ """
240
+ Parse OEIS keyword field into a list of strings.
241
+
242
+ Typical OEIS values look like ``"nonn,easy"`` and are returned as
243
+ ``["nonn", "easy"]``.
244
+ """
245
+ if isinstance(keyword_raw, list):
246
+ tokens = keyword_raw
247
+ elif isinstance(keyword_raw, str):
248
+ tokens = keyword_raw.split(",")
249
+ else:
250
+ return []
251
+
252
+ keywords = []
253
+ for token in tokens:
254
+ value = str(token).strip()
255
+ if value:
256
+ keywords.append(value)
257
+ return keywords
258
+
259
+ __all__ = ["__version__",
260
+ "check_id",
261
+ "oeis_bfile",
262
+ "oeis_url",
263
+ "OEIS_URL",
264
+ "BFile",
265
+ "Sequence"]
@@ -0,0 +1,77 @@
1
+ """
2
+ Utility functions for working with OEIS identifiers and URLs.
3
+
4
+ This module provides small, self-contained helpers related to the
5
+ Online Encyclopedia of Integer Sequences (OEIS), including:
6
+
7
+ - Validation of OEIS sequence identifiers (e.g. ``A000001``).
8
+ - Construction of b-file filenames.
9
+ - Generation of OEIS URLs in different formats (web, JSON, text, b-file).
10
+
11
+ The functions in this module are pure utilities: they perform no network
12
+ requests and have no side effects beyond basic validation.
13
+ """
14
+
15
+ import re
16
+
17
+ OEIS_URL = "https://oeis.org"
18
+
19
+ def check_id(oeis_id):
20
+ """
21
+ Check if the OEIS ID is valid.
22
+ It must start with 'A' followed by exactly 6 digits.
23
+
24
+ Args:
25
+ oeis_id (str): The ID to check.
26
+
27
+ Returns:
28
+ bool: True if valid, False otherwise.
29
+ """
30
+ pattern = r'^A\d{6}$'
31
+ return bool(re.match(pattern, oeis_id))
32
+
33
+ def oeis_bfile(oeis_id):
34
+ """
35
+ Generate the b-file filename for a given OEIS ID.
36
+
37
+ The b-file is a text file containing the sequence data.
38
+ The filename format is 'b' followed by the 6-digit number and '.txt'.
39
+
40
+ Args:
41
+ oeis_id (str): A valid OEIS ID, e.g., 'A000001'.
42
+
43
+ Returns:[print(item[0]) for item in json_dict()]
44
+ str: The b-file filename, e.g., 'b000001.txt'.
45
+
46
+ Raises:
47
+ ValueError: If the oeis_id is not in the correct format.
48
+ """
49
+ if not check_id(oeis_id):
50
+ raise ValueError(f"Invalid OEIS ID: {oeis_id}")
51
+
52
+ # Extract the 6 digits after 'A'
53
+ digits = oeis_id[1:]
54
+ return f"b{digits}.txt"
55
+
56
+ def oeis_url(oeis_id, fmt=None):
57
+ """
58
+ Generate the OEIS webpage URL for a given OEIS ID.
59
+
60
+ Args:
61
+ oeis_id (str): The OEIS ID, e.g., 'A000001'.
62
+ fmt (str, optional): The format of the response.
63
+ - 'json': JSON search URL.
64
+ - 'text': Text search URL.
65
+ - 'bfile': b-file URL.
66
+ - None: Standard webpage URL.
67
+
68
+ Returns:
69
+ str: The URL.
70
+ """
71
+ formats = {
72
+ "json": f"{OEIS_URL}/search?q=id:{oeis_id}&fmt=json",
73
+ "text": f"{OEIS_URL}/search?q=id:{oeis_id}&fmt=text",
74
+ "bfile": f"{OEIS_URL}/{oeis_id}/{oeis_bfile(oeis_id)}",
75
+ None: f"{OEIS_URL}/{oeis_id}",
76
+ }
77
+ return formats.get(fmt, formats[None])
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: oeis-tools
3
+ Version: 0.1.0
4
+ Summary: Tools and utilities for working with OEIS integer sequences
5
+ Author-email: Enrique Pérez Herrero <energycode.org@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/oeistools/oeis-tools
8
+ Project-URL: Repository, https://github.com/oeistools/oeis-tools
9
+ Project-URL: Issues, https://github.com/oeistools/oeis-tools/issues
10
+ Project-URL: Documentation, https://github.com/oeistools/oeis-tools#readme
11
+ Keywords: oeis,integer-sequences,number-theory,math
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.31
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.4; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # oeis-tools
29
+
30
+ [![Tests](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml/badge.svg)](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
31
+ [![PyPI version](https://img.shields.io/pypi/v/oeis-tools.svg)](https://pypi.org/project/oeis-tools/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+
34
+ `oeis-tools` is a Python package for working with the
35
+ [Online Encyclopedia of Integer Sequences (OEIS)](https://oeis.org/).
36
+
37
+ It provides:
38
+ - OEIS ID validation
39
+ - URL and b-file helpers
40
+ - A `Sequence` class for OEIS JSON entries
41
+ - A `BFile` class for b-file numeric data
42
+
43
+ ## Installation
44
+
45
+ From PyPI:
46
+
47
+ ```bash
48
+ pip install oeis-tools
49
+ ```
50
+
51
+ From source:
52
+
53
+ ```bash
54
+ git clone https://github.com/oeistools/oeis-tools.git
55
+ cd oeis-tools
56
+ pip install -e .
57
+ ```
58
+
59
+ Development dependencies (tests):
60
+
61
+ ```bash
62
+ pip install -e ".[dev]"
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ import oeis_tools
69
+
70
+ # Utilities
71
+ print(oeis_tools.check_id("A000001")) # True
72
+ print(oeis_tools.oeis_bfile("A000001")) # b000001.txt
73
+ print(oeis_tools.oeis_url("A000001", fmt="json")) # https://oeis.org/search?q=id:A000001&fmt=json
74
+
75
+ # Sequence API
76
+ seq = oeis_tools.Sequence("A000045")
77
+ print(seq.name) # Fibonacci numbers
78
+ print(seq.data) # comma-separated terms
79
+ print(seq.author) # list[str]
80
+ print(seq.json["id"]) # raw JSON field access
81
+
82
+ # B-file API
83
+ bfile = oeis_tools.BFile("A000045")
84
+ print(bfile.get_filename()) # b000045.txt
85
+ print(bfile.get_url()) # https://oeis.org/A000045/b000045.txt
86
+ print(bfile.get_bfile_data()[:5])
87
+ ```
88
+
89
+ ## API Overview
90
+
91
+ - `check_id(oeis_id: str) -> bool`
92
+ - `oeis_bfile(oeis_id: str) -> str`
93
+ - `oeis_url(oeis_id: str, fmt: str | None = None) -> str`
94
+ - `Sequence(oeis_id: str)`
95
+ - Key attributes: `id`, `json`, `name`, `data`, `author`, `link`, `bfile`
96
+ - `BFile(oeis_id: str)`
97
+ - Methods: `get_filename()`, `get_url()`, `get_bfile_data()`
98
+
99
+ ## Running Tests
100
+
101
+ ```bash
102
+ pytest -q
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT. See `LICENSE`.
108
+
109
+ ## Author
110
+
111
+ Enrique Pérez Herrero - [energycode.org@gmail.com](mailto:energycode.org@gmail.com)
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/oeis_tools/__init__.py
6
+ src/oeis_tools/__version__.py
7
+ src/oeis_tools/bfile.py
8
+ src/oeis_tools/sequence.py
9
+ src/oeis_tools/utils.py
10
+ src/oeis_tools.egg-info/PKG-INFO
11
+ src/oeis_tools.egg-info/SOURCES.txt
12
+ src/oeis_tools.egg-info/dependency_links.txt
13
+ src/oeis_tools.egg-info/requires.txt
14
+ src/oeis_tools.egg-info/top_level.txt
15
+ tests/conftest.py
16
+ tests/test_bfile.py
17
+ tests/test_sequence.py
18
+ tests/test_utils.py
@@ -0,0 +1,4 @@
1
+ requests>=2.31
2
+
3
+ [dev]
4
+ pytest>=7.4
@@ -0,0 +1 @@
1
+ oeis_tools
@@ -0,0 +1,10 @@
1
+ """Pytest configuration for local source imports."""
2
+
3
+ from pathlib import Path
4
+ import sys
5
+
6
+
7
+ SRC_DIR = Path(__file__).resolve().parents[1] / "src"
8
+ src_path = str(SRC_DIR)
9
+ if src_path not in sys.path:
10
+ sys.path.insert(0, src_path)
@@ -0,0 +1,55 @@
1
+ """Tests for ``oeis_tools.bfile.BFile``."""
2
+
3
+ import requests
4
+
5
+ from oeis_tools.bfile import BFile
6
+
7
+
8
+ class DummyResponse:
9
+ """Minimal response object used to mock ``requests.get``."""
10
+
11
+ def __init__(self, text):
12
+ """Store raw text returned by the fake request."""
13
+ self.text = text
14
+
15
+ def raise_for_status(self):
16
+ """Mimic a successful HTTP response."""
17
+ return None
18
+
19
+
20
+ def test_bfile_parses_numeric_values_and_metadata(monkeypatch):
21
+ """Parse b-file values and expose expected filename and URL."""
22
+ def fake_get(url, timeout):
23
+ assert "A000045" in url
24
+ assert timeout == 10
25
+ return DummyResponse("# comment\n0 0\n1 1\n2 1\n3 2\n")
26
+
27
+ monkeypatch.setattr("oeis_tools.bfile.requests.get", fake_get)
28
+
29
+ bfile = BFile("A000045")
30
+
31
+ assert bfile.get_filename() == "b000045.txt"
32
+ assert bfile.get_url() == "https://oeis.org/A000045/b000045.txt"
33
+ assert bfile.get_bfile_data() == [0, 1, 1, 2]
34
+
35
+
36
+ def test_bfile_returns_none_when_request_fails(monkeypatch):
37
+ """Return ``None`` when the HTTP request fails."""
38
+ def fake_get(url, timeout):
39
+ raise requests.RequestException("network error")
40
+
41
+ monkeypatch.setattr("oeis_tools.bfile.requests.get", fake_get)
42
+
43
+ bfile = BFile("A000045")
44
+ assert bfile.get_bfile_data() is None
45
+
46
+
47
+ def test_bfile_returns_none_for_malformed_line(monkeypatch):
48
+ """Return ``None`` when a b-file line cannot be parsed."""
49
+ def fake_get(url, timeout):
50
+ return DummyResponse("0 0\nthis-is-not-valid\n")
51
+
52
+ monkeypatch.setattr("oeis_tools.bfile.requests.get", fake_get)
53
+
54
+ bfile = BFile("A000045")
55
+ assert bfile.get_bfile_data() is None
@@ -0,0 +1,235 @@
1
+ """Tests for ``oeis_tools.sequence.Sequence``."""
2
+
3
+ from datetime import datetime
4
+
5
+ import pytest
6
+ import requests
7
+
8
+ from oeis_tools.sequence import Sequence
9
+
10
+
11
+ class DummyResponse:
12
+ """Minimal JSON response object for mocking API calls."""
13
+
14
+ def __init__(self, payload, error=None):
15
+ """Store mock payload and optional HTTP error."""
16
+ self._payload = payload
17
+ self._error = error
18
+
19
+ def raise_for_status(self):
20
+ """Raise the configured HTTP error when present."""
21
+ if self._error:
22
+ raise self._error
23
+ return None
24
+
25
+ def json(self):
26
+ """Return the stored JSON payload."""
27
+ return self._payload
28
+
29
+
30
+ class DummyBFile:
31
+ """Simple placeholder used to avoid network calls in ``Sequence``."""
32
+
33
+ def __init__(self, oeis_id):
34
+ """Keep the sequence ID passed by ``Sequence``."""
35
+ self.oeis_id = oeis_id
36
+
37
+ def get_filename(self):
38
+ """Provide default b-file filename in tests."""
39
+ return "b000001.txt"
40
+
41
+ def get_url(self):
42
+ """Provide default b-file URL in tests."""
43
+ return "https://oeis.org/A000001/b000001.txt"
44
+
45
+ def get_bfile_data(self):
46
+ """Default to no parsed b-file data."""
47
+ return None
48
+
49
+
50
+ def test_sequence_parses_json_fields_and_builds_links(monkeypatch):
51
+ """Parse key OEIS fields, datetimes, links, and b-file integration."""
52
+ payload = [
53
+ {
54
+ "id": "M1234 N5678",
55
+ "data": "1,1,2,3,5,8",
56
+ "name": "Fibonacci numbers",
57
+ "comment": ["First comment", "Second comment"],
58
+ "reference": ["Ref A", "Ref B"],
59
+ "formula": ["a(n)=a(n-1)+a(n-2)"],
60
+ "example": ["a(5)=5"],
61
+ "maple": ["seq(fibonacci(n),n=0..10);"],
62
+ "mathematica": ["Table[Fibonacci[n], {n,0,10}]"],
63
+ "program": ["Python: ..."],
64
+ "xref": ["Cf. A000204"],
65
+ "keyword": "nonn",
66
+ "offset": "0,2",
67
+ "author": "_Tom Verhoeff_, _N. J. A. Sloane_",
68
+ "references": ["Some extra reference"],
69
+ "revision": "42",
70
+ "time": "2024-01-02 03:04:05",
71
+ "created": "2000-01-01 00:00:00",
72
+ "link": [
73
+ '<a href="/A000045">Main entry</a>',
74
+ '<a href="https://example.com/ref">External ref</a>',
75
+ 'See also <a href="/wiki">wiki</a>',
76
+ ],
77
+ }
78
+ ]
79
+
80
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
81
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
82
+
83
+ seq = Sequence("A000045")
84
+
85
+ assert seq.id == "A000045"
86
+ assert seq.m_id == "M1234"
87
+ assert seq.n_id == "N5678"
88
+ assert seq.name == "Fibonacci numbers"
89
+ assert seq.comment == "First comment\nSecond comment"
90
+ assert seq.reference == "Ref A\nRef B"
91
+ assert seq.author == ["Tom Verhoeff", "N. J. A. Sloane"]
92
+ assert seq.keyword == ["nonn"]
93
+ assert seq.offset == [0, 2]
94
+ assert seq.time == datetime(2024, 1, 2, 3, 4, 5)
95
+ assert seq.created == datetime(2000, 1, 1, 0, 0, 0)
96
+ assert "[Main entry](https://oeis.org/A000045)" in seq.link
97
+ assert "[External ref](https://example.com/ref)" in seq.link
98
+ assert "[wiki](https://oeis.org/wiki)" in seq.link
99
+ assert isinstance(seq.bfile, DummyBFile)
100
+ assert seq.bfile.oeis_id == "A000045"
101
+
102
+
103
+ def test_sequence_rejects_invalid_oeis_id():
104
+ """Raise ``ValueError`` for invalid OEIS identifiers."""
105
+ with pytest.raises(ValueError, match="Invalid OEIS ID"):
106
+ Sequence("invalid-id")
107
+
108
+
109
+ def test_sequence_propagates_http_error(monkeypatch):
110
+ """Propagate HTTP errors from the JSON endpoint."""
111
+ def fake_get(url, timeout):
112
+ return DummyResponse(payload=None, error=requests.HTTPError("request failed"))
113
+
114
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", fake_get)
115
+
116
+ with pytest.raises(requests.HTTPError, match="request failed"):
117
+ Sequence("A000001")
118
+
119
+
120
+ def test_sequence_author_ignores_trailing_year_tokens(monkeypatch):
121
+ """Drop year-only entries when parsing the OEIS author field."""
122
+ payload = [
123
+ {
124
+ "id": "M0001 N0001",
125
+ "author": "_N. J. A. Sloane_, 1964",
126
+ "link": [],
127
+ }
128
+ ]
129
+
130
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
131
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
132
+
133
+ seq = Sequence("A000001")
134
+ assert seq.author == ["N. J. A. Sloane"]
135
+
136
+
137
+ def test_sequence_author_ignores_trailing_full_date_tokens(monkeypatch):
138
+ """Drop full date entries when parsing the OEIS author field."""
139
+ payload = [
140
+ {
141
+ "id": "M0001 N0001",
142
+ "author": "_Pierre CAMI_, Apr 28 2012",
143
+ "link": [],
144
+ }
145
+ ]
146
+
147
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
148
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
149
+
150
+ seq = Sequence("A000001")
151
+ assert seq.author == ["Pierre CAMI"]
152
+
153
+
154
+ def test_sequence_offset_ignores_invalid_tokens(monkeypatch):
155
+ """Parse integer offsets and ignore malformed tokens."""
156
+ payload = [
157
+ {
158
+ "id": "M0001 N0001",
159
+ "offset": "1, bad, -3",
160
+ "link": [],
161
+ }
162
+ ]
163
+
164
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
165
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
166
+
167
+ seq = Sequence("A000001")
168
+ assert seq.offset == [1, -3]
169
+
170
+
171
+ def test_sequence_keyword_splits_and_ignores_empty_tokens(monkeypatch):
172
+ """Parse keywords into a list and drop empty entries."""
173
+ payload = [
174
+ {
175
+ "id": "M0001 N0001",
176
+ "keyword": "nonn, easy, ,look",
177
+ "link": [],
178
+ }
179
+ ]
180
+
181
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
182
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
183
+
184
+ seq = Sequence("A000001")
185
+ assert seq.keyword == ["nonn", "easy", "look"]
186
+
187
+
188
+ def test_sequence_get_bfile_info_with_data(monkeypatch):
189
+ """Return metadata and stats when b-file data is available."""
190
+ payload = [{"id": "M0001 N0001", "link": []}]
191
+
192
+ class BFileWithData(DummyBFile):
193
+ def get_filename(self):
194
+ return "b000045.txt"
195
+
196
+ def get_url(self):
197
+ return "https://oeis.org/A000045/b000045.txt"
198
+
199
+ def get_bfile_data(self):
200
+ return [0, 1, 1, 2, 3]
201
+
202
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
203
+ monkeypatch.setattr("oeis_tools.sequence.BFile", BFileWithData)
204
+
205
+ seq = Sequence("A000001")
206
+ info = seq.get_bfile_info()
207
+
208
+ assert info["available"] is True
209
+ assert info["filename"] == "b000045.txt"
210
+ assert info["url"] == "https://oeis.org/A000045/b000045.txt"
211
+ assert info["length"] == 5
212
+ assert info["first"] == 0
213
+ assert info["last"] == 3
214
+ assert info["min"] == 0
215
+ assert info["max"] == 3
216
+
217
+
218
+ def test_sequence_get_bfile_info_without_data(monkeypatch):
219
+ """Return unavailable metadata when b-file data is missing."""
220
+ payload = [{"id": "M0001 N0001", "link": []}]
221
+
222
+ monkeypatch.setattr("oeis_tools.sequence.requests.get", lambda url, timeout: DummyResponse(payload))
223
+ monkeypatch.setattr("oeis_tools.sequence.BFile", DummyBFile)
224
+
225
+ seq = Sequence("A000001")
226
+ info = seq.get_bfile_info()
227
+
228
+ assert info["available"] is False
229
+ assert info["filename"] == "b000001.txt"
230
+ assert info["url"] == "https://oeis.org/A000001/b000001.txt"
231
+ assert info["length"] == 0
232
+ assert info["first"] is None
233
+ assert info["last"] is None
234
+ assert info["min"] is None
235
+ assert info["max"] is None
@@ -0,0 +1,42 @@
1
+ """Tests for utility helpers in ``oeis_tools.utils``."""
2
+
3
+ import pytest
4
+
5
+ from oeis_tools.utils import OEIS_URL, check_id, oeis_bfile, oeis_url
6
+
7
+
8
+ def test_check_id_accepts_valid_oeis_id():
9
+ """Accept IDs that match the OEIS pattern."""
10
+ assert check_id("A000001") is True
11
+
12
+
13
+ def test_check_id_rejects_invalid_oeis_ids():
14
+ """Reject IDs with wrong prefix or wrong length/characters."""
15
+ assert check_id("A12345") is False
16
+ assert check_id("B000001") is False
17
+ assert check_id("A0000012") is False
18
+ assert check_id("A12ABC") is False
19
+
20
+
21
+ def test_oeis_bfile_builds_expected_filename():
22
+ """Build the expected OEIS b-file filename from a valid ID."""
23
+ assert oeis_bfile("A000045") == "b000045.txt"
24
+
25
+
26
+ def test_oeis_bfile_raises_for_invalid_id():
27
+ """Raise ``ValueError`` when b-file is requested for an invalid ID."""
28
+ with pytest.raises(ValueError, match="Invalid OEIS ID"):
29
+ oeis_bfile("A123")
30
+
31
+
32
+ def test_oeis_url_builds_supported_formats():
33
+ """Generate valid OEIS URLs for default and known formats."""
34
+ assert oeis_url("A000001") == f"{OEIS_URL}/A000001"
35
+ assert oeis_url("A000001", fmt="json") == f"{OEIS_URL}/search?q=id:A000001&fmt=json"
36
+ assert oeis_url("A000001", fmt="text") == f"{OEIS_URL}/search?q=id:A000001&fmt=text"
37
+ assert oeis_url("A000001", fmt="bfile") == f"{OEIS_URL}/A000001/b000001.txt"
38
+
39
+
40
+ def test_oeis_url_falls_back_to_default_for_unknown_format():
41
+ """Use the default entry URL when an unknown format is provided."""
42
+ assert oeis_url("A000001", fmt="unknown") == f"{OEIS_URL}/A000001"