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.
- oeis_tools-0.1.0/LICENSE +21 -0
- oeis_tools-0.1.0/MANIFEST.in +4 -0
- oeis_tools-0.1.0/PKG-INFO +111 -0
- oeis_tools-0.1.0/README.md +84 -0
- oeis_tools-0.1.0/pyproject.toml +56 -0
- oeis_tools-0.1.0/setup.cfg +4 -0
- oeis_tools-0.1.0/src/oeis_tools/__init__.py +34 -0
- oeis_tools-0.1.0/src/oeis_tools/__version__.py +8 -0
- oeis_tools-0.1.0/src/oeis_tools/bfile.py +98 -0
- oeis_tools-0.1.0/src/oeis_tools/sequence.py +265 -0
- oeis_tools-0.1.0/src/oeis_tools/utils.py +77 -0
- oeis_tools-0.1.0/src/oeis_tools.egg-info/PKG-INFO +111 -0
- oeis_tools-0.1.0/src/oeis_tools.egg-info/SOURCES.txt +18 -0
- oeis_tools-0.1.0/src/oeis_tools.egg-info/dependency_links.txt +1 -0
- oeis_tools-0.1.0/src/oeis_tools.egg-info/requires.txt +4 -0
- oeis_tools-0.1.0/src/oeis_tools.egg-info/top_level.txt +1 -0
- oeis_tools-0.1.0/tests/conftest.py +10 -0
- oeis_tools-0.1.0/tests/test_bfile.py +55 -0
- oeis_tools-0.1.0/tests/test_sequence.py +235 -0
- oeis_tools-0.1.0/tests/test_utils.py +42 -0
oeis_tools-0.1.0/LICENSE
ADDED
|
@@ -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,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
|
+
[](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
|
|
31
|
+
[](https://pypi.org/project/oeis-tools/)
|
|
32
|
+
[](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
|
+
[](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/oeis-tools/)
|
|
5
|
+
[](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,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,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
|
+
[](https://github.com/oeistools/oeis-tools/actions/workflows/tests.yml)
|
|
31
|
+
[](https://pypi.org/project/oeis-tools/)
|
|
32
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oeis_tools
|
|
@@ -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"
|