├── feeder ├── __init__.py ├── cli.py └── feeder.py ├── tests ├── __init__.py ├── test_feed_fetch.py ├── test_settings.py └── test_generate.py ├── pyproject.toml ├── setup.py ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── README.md └── .gitignore /feeder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | 4 | [tool.black] 5 | line-length = 120 6 | 7 | [tool.isort] 8 | profile = "black" 9 | 10 | [tool.bandit] 11 | exclude_dirs = ["up"] 12 | 13 | -------------------------------------------------------------------------------- /tests/test_feed_fetch.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from feeder.feeder import fetch_existing_feeditems 4 | 5 | 6 | class FeedFetchingTestCase(TestCase): 7 | def test_fetch_feed(self): 8 | items = list(fetch_existing_feeditems("https://www.jsonfeed.org/feed.json")) 9 | 10 | self.assertEqual(len(items), 2) 11 | self.assertEqual("JSON Feed version 1.1", items[0].title) 12 | self.assertEqual(items[0].id, "http://jsonfeed.micro.blog/2020/08/07/json-feed-version.html") 13 | self.assertEqual(items[0].url, "https://www.jsonfeed.org/2020/08/07/json-feed-version.html") 14 | self.assertIsNotNone(items[0].content_html) 15 | self.assertIsNone(items[0].content_text) 16 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from unittest import TestCase 4 | 5 | from feeder.feeder import load_settings 6 | 7 | 8 | class LoadSettingsTestCase(TestCase): 9 | def test_default_settings(self): 10 | settings = load_settings(None) 11 | self.assertEqual(settings["FEED_FILENAME"], "out/feed.json") 12 | 13 | def test_settings_loads_from_module(self): 14 | with tempfile.TemporaryDirectory(dir=os.getcwd(), prefix="loads_from_module") as dir: 15 | with open(dir + "/__init__.py", "w") as f: 16 | f.write("\n") 17 | f.flush() 18 | os.fsync(f.fileno()) 19 | 20 | with open(dir + "/settings.py", "w") as f: 21 | f.write("FEED_FILENAME = 'dist/feed.json'") 22 | f.flush() 23 | os.fsync(f.fileno()) 24 | 25 | module_name = dir.split("/")[-1] + ".settings" 26 | settings = load_settings(module_name) 27 | 28 | self.assertEqual(settings["FEED_FILENAME"], "dist/feed.json") 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | VERSION = "0.1.1" 6 | 7 | 8 | def get_long_description(): 9 | with open( 10 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"), 11 | encoding="utf8", 12 | ) as fp: 13 | return fp.read() 14 | 15 | 16 | setup( 17 | name="json-feeder", 18 | description="A framework for generate JSON feeds", 19 | long_description=get_long_description(), 20 | long_description_content_type="text/markdown", 21 | author="Brenton Cleeland", 22 | url="https://github.com/sesh/feeder", 23 | project_urls={ 24 | "Issues": "https://github.com/sesh/feeder/issues", 25 | "CI": "https://github.com/sesh/feeder/actions", 26 | "Changelog": "https://github.com/sesh/feeder/releases", 27 | }, 28 | version=VERSION, 29 | packages=["feeder"], 30 | install_requires=["thttp"], 31 | python_requires=">=3.8", 32 | license="MIT License", 33 | entry_points=""" 34 | [console_scripts] 35 | feeder=feeder.cli:cli 36 | """, 37 | ) 38 | -------------------------------------------------------------------------------- /feeder/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def parse_args(args): 5 | result = { 6 | a.split("=")[0]: int(a.split("=")[1]) 7 | if "=" in a and a.split("=")[1].isnumeric() 8 | else a.split("=")[1] 9 | if "=" in a 10 | else True 11 | for a in args 12 | if "--" in a 13 | } 14 | result["[]"] = [a for a in args if not a.startswith("--")] 15 | return result 16 | 17 | 18 | SETTINGS_FILE = """# settings for feeder 19 | 20 | FEED_FILENAME = "out/feed.json" 21 | FEED_FUNCTION = "feed.get_items" 22 | FEED_VERSION = "1.1" 23 | 24 | FEED_TITLE = "" 25 | FEED_URL = "" 26 | FEED_HOMEPAGE_URL = "" 27 | FEED_ICON = "" 28 | """ 29 | 30 | GENERATE_FILE = """from feeder.feeder import generate, load_settings 31 | import os 32 | 33 | 34 | if __name__ == "__main__": 35 | settings_module_name = os.environ.get("FEEDER_SETTINGS_MODULE", "settings") 36 | settings = load_settings(settings_module_name) 37 | generate(settings=settings) 38 | """ 39 | 40 | FEED_FILE = """from feeder.feeder import FeedItem 41 | 42 | def get_items(): 43 | return [] 44 | """ 45 | 46 | 47 | def cli(): 48 | args = parse_args(sys.argv[1:]) 49 | 50 | if args["[]"][0] == "startfeed": 51 | with open("settings.py", "w") as f: 52 | f.write(SETTINGS_FILE) 53 | 54 | with open("generate.py", "w") as f: 55 | f.write(GENERATE_FILE) 56 | 57 | with open("feed.py", "w") as f: 58 | f.write(FEED_FILE) 59 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.11"] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install test dependencies 24 | run: | 25 | pip install -e '.[test]' 26 | 27 | - name: Test with unittest 28 | run: | 29 | python -m unittest discover 30 | 31 | 32 | pypi-publish: 33 | name: Upload release to PyPI 34 | runs-on: ubuntu-latest 35 | needs: [test] 36 | environment: 37 | name: pypi 38 | url: https://pypi.org/p/json-feeder 39 | permissions: 40 | id-token: write 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | 45 | - name: Set up Python 3.11 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: 3.11 49 | 50 | - name: Install test dependencies 51 | run: | 52 | pip install -e '.[test]' 53 | 54 | - name: Install publishing dependencies 55 | run: | 56 | pip install setuptools wheel twine build 57 | 58 | - name: Build python package 59 | run: | 60 | python -m build 61 | 62 | - name: Publish package to PyPI 63 | uses: pypa/gh-action-pypi-publish@release/v1 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _This framework is **highly experimental** and **very likely** to change significantly_ 2 | 3 | # feeder 4 | 5 | `feeder` is a small Python framework that helps you generate [JSON Feeds][jsonfeed.org]. 6 | 7 | 8 | ## Usage 9 | 10 | Install from PyPI: 11 | 12 | ``` 13 | python3 -m pip install json-feeder 14 | ``` 15 | 16 | Create a directory for your new feed, the start the new feed with: 17 | 18 | ``` 19 | feeder startfeed 20 | ``` 21 | 22 | `settings.py`, `feed.py` and `generate.py` files will be created for you. 23 | 24 | To get started simply update the `get_items()` function in `feed.py` to return a list of `FeedItems`. 25 | 26 | Generate the feed with: 27 | 28 | ``` 29 | python3 generate.py 30 | ``` 31 | 32 | The following settings can be configured: 33 | 34 | - `FEED_FUNCTION` the path to a Python function that will return a list of `FeedItem` objects. 35 | - `FEED_FILENAME` the filename on disk for the feed. If you are using Github Pages then this should be set to `out/feed.json` or similar. 36 | - `FEED_URL` the remote url of the feed. This is used to ensure that duplicates are not added to the file. 37 | - `FEED_TITLE` is the title of your feed. 38 | - `FEED_HOMEPAGE_URL` is the homepage of your feed, this is optional. 39 | - `FEED_ICON` is a url to an icon that feed readers might use for your feed. Very optional. 40 | - `FEED_VERSION` defaults to "1.1" and represents the JSON Feed version. 41 | - `FEED_MAX_ITEMS` limits the number of items to output in the feed. Default is 100. 42 | 43 | 44 | 45 | ### Runnings Tests 46 | 47 | ``` 48 | python3 -m unittest discover 49 | ``` 50 | 51 | 52 | [jsonfeed.org]: https://www.jsonfeed.org 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python Checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.11"] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install test dependencies 26 | run: | 27 | pip install -e '.[test]' 28 | 29 | - name: Test with unittest 30 | run: | 31 | python -m unittest discover 32 | 33 | black: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v3 38 | 39 | - name: Set up Python 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: "3.11" 43 | 44 | - name: Install black 45 | run: | 46 | python -m pip install black 47 | 48 | - name: Run black 49 | run: | 50 | black --check . 51 | 52 | isort: 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v3 57 | 58 | - name: Set up Python 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: "3.11" 62 | 63 | - name: Install isort 64 | run: | 65 | python -m pip install isort 66 | 67 | - name: Run isort 68 | run: | 69 | isort --check . 70 | 71 | ruff: 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@v3 76 | 77 | - name: Set up Python 78 | uses: actions/setup-python@v4 79 | with: 80 | python-version: "3.11" 81 | 82 | - name: Install ruff 83 | run: | 84 | python -m pip install ruff 85 | 86 | - name: Run ruff 87 | run: | 88 | ruff --format=github . 89 | 90 | bandit: 91 | runs-on: ubuntu-latest 92 | 93 | steps: 94 | - uses: actions/checkout@v3 95 | 96 | - name: Set up Python 97 | uses: actions/setup-python@v4 98 | with: 99 | python-version: 3 100 | 101 | - name: Install bandit 102 | run: | 103 | python -m pip install bandit 104 | 105 | - name: Run bandit scan 106 | run: | 107 | bandit -r . 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Generated by gibo (https://github.com/simonwhitaker/gibo) 2 | ### https://raw.github.com/github/gitignore/96d68766538413194aabd55e3622734cd501e715/Python.gitignore 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # Cython debug symbols 148 | cython_debug/ 149 | 150 | # PyCharm 151 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 152 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 153 | # and can be added to the global gitignore or merged into this file. For a more nuclear 154 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 155 | #.idea/ 156 | 157 | 158 | -------------------------------------------------------------------------------- /feeder/feeder.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import json 3 | import os 4 | from pathlib import Path 5 | 6 | import thttp 7 | 8 | 9 | class FeedItem: 10 | def __init__(self, id, url, title, content_text, content_html, date_published, authors=[]): 11 | self.id = id 12 | self.url = url 13 | self.title = title 14 | self.content_text = content_text 15 | self.content_html = content_html 16 | self.date_published = date_published 17 | self.authors = authors 18 | 19 | 20 | def fetch_existing_feeditems(url): 21 | response = thttp.request(url) 22 | if response.status == 200 and response.json: 23 | for item in response.json.get("items"): 24 | yield FeedItem( 25 | item.get("id"), 26 | item.get("url"), 27 | item.get("title"), 28 | item.get("content_text"), 29 | item.get("content_html"), 30 | item.get("date_published"), 31 | item.get("authors"), 32 | ) 33 | else: 34 | return [] 35 | 36 | 37 | def feed_item_as_json(item): 38 | j = { 39 | "id": item.id, 40 | "url": item.url, 41 | } 42 | 43 | optional_fields = ["title", "content_text", "content_html", "date_published", "authors"] 44 | for field in optional_fields: 45 | if getattr(item, field): 46 | j[field] = getattr(item, field) 47 | 48 | return j 49 | 50 | 51 | def load_settings(module_name): 52 | default_settings = { 53 | "FEED_FUNCTION": None, 54 | "FEED_FILENAME": "out/feed.json", 55 | "FEED_URL": None, 56 | "FEED_TITLE": "Generated JSON Feed", 57 | "FEED_HOMEPAGE_URL": None, 58 | "FEED_ICON": None, 59 | "FEED_VERSION": "1.1", 60 | "FEED_MAX_ITEMS": 100, 61 | } 62 | 63 | settings = default_settings 64 | 65 | if module_name: 66 | settings_module = importlib.import_module(module_name) 67 | 68 | for k, v in default_settings.items(): 69 | settings[k] = getattr(settings_module, k, v) 70 | 71 | return settings 72 | 73 | 74 | def load_feed_function(fn_str): 75 | module_name, fn = fn_str.rsplit(".", 1) 76 | fn_module = importlib.import_module(module_name) 77 | fn = getattr(fn_module, fn) 78 | return fn 79 | 80 | 81 | def json_feed( 82 | title, 83 | icon, 84 | home_page_url, 85 | feed_url, 86 | items, 87 | existing_items, 88 | *, 89 | max_items=100, 90 | version="1.1", 91 | ): 92 | feed_items = [] 93 | existing_item_ids = [item.id for item in existing_items] 94 | 95 | for item in items: 96 | if item.id not in existing_item_ids: 97 | feed_items.append(item) 98 | 99 | feed_items.extend(existing_items) 100 | feed_items = feed_items[:max_items] 101 | 102 | feed = { 103 | "version": f"https://jsonfeed.org/version/{version}", 104 | "title": title, 105 | "icon": icon, 106 | "home_page_url": home_page_url, 107 | "feed_url": feed_url, 108 | "items": [feed_item_as_json(item) for item in feed_items], 109 | } 110 | 111 | # remove empty values 112 | feed = {k: v for k, v in feed.items() if v is not None} 113 | return feed 114 | 115 | 116 | def generate(*, settings=None): 117 | feed_path = Path(settings["FEED_FILENAME"]) 118 | feed_directory = Path(os.path.dirname(feed_path)) 119 | feed_directory.mkdir(parents=True, exist_ok=True) 120 | 121 | if settings["FEED_URL"]: 122 | existing_items = list(fetch_existing_feeditems(settings["FEED_URL"])) 123 | else: 124 | existing_items = [] 125 | 126 | if settings["FEED_FUNCTION"]: 127 | fn = load_feed_function(settings["FEED_FUNCTION"]) 128 | items = fn() 129 | else: 130 | items = [] 131 | 132 | feed = json_feed( 133 | settings["FEED_TITLE"], 134 | settings["FEED_ICON"], 135 | settings["FEED_HOMEPAGE_URL"], 136 | settings["FEED_URL"], 137 | items, 138 | existing_items, 139 | version=settings["FEED_VERSION"], 140 | max_items=settings["FEED_MAX_ITEMS"], 141 | ) 142 | 143 | with open(feed_path, "w") as f: 144 | f.write(json.dumps(feed, indent=2)) 145 | -------------------------------------------------------------------------------- /tests/test_generate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import tempfile 4 | from unittest import TestCase, mock 5 | 6 | import thttp 7 | 8 | from feeder.feeder import generate 9 | 10 | JSONFEED_ORG_JSONFEED = { 11 | "version": "https://jsonfeed.org/version/1", 12 | "title": "JSON Feed", 13 | "icon": "https://micro.blog/jsonfeed/avatar.jpg", 14 | "home_page_url": "https://www.jsonfeed.org/", 15 | "feed_url": "https://www.jsonfeed.org/feed.json", 16 | "items": [ 17 | { 18 | "id": "http://jsonfeed.micro.blog/2020/08/07/json-feed-version.html", 19 | "title": "JSON Feed version 1.1", 20 | "content_html": '
We’ve updated the spec to version 1.1. It’s a minor update to JSON Feed, clarifying a few things in the spec and adding a couple new fields such as authors and language.
For version 1.1, we’re starting to move to the more specific MIME type application/feed+json. Clients that parse HTML to discover feeds should prefer that MIME type, while still falling back to accepting application/json too.
The code page has also been updated with several new code libraries and apps that support JSON Feed.
\n', # noqa 21 | "date_published": "2020-08-07T11:44:36-05:00", 22 | "url": "https://www.jsonfeed.org/2020/08/07/json-feed-version.html", 23 | }, 24 | { 25 | "id": "http://jsonfeed.micro.blog/2017/05/17/announcing-json-feed.html", 26 | "title": "Announcing JSON Feed", 27 | "content_html": '\n\nWe —\xa0Manton Reece and Brent Simmons —\xa0have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.
\n\nSo we developed JSON Feed, a format similar to RSS and Atom but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.
\n\nSee the spec. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.
\n\nWe have a WordPress plugin and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the code page.
\n\nSee Mapping RSS and Atom to JSON Feed for more on the similarities between the formats.
\n\nThis website —\xa0the Markdown files and supporting resources —\xa0is up on GitHub, and you’re welcome to comment there.
\n\nThis website is also a blog, and you can subscribe to the RSS feed or the JSON feed (if your reader supports it).
\n\nWe worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the spec. But — most importantly — Craig Hockenberry spent a little time making it look pretty. :)
\n', # noqa 28 | "date_published": "2017-05-17T10:02:12-05:00", 29 | "url": "https://www.jsonfeed.org/2017/05/17/announcing-json-feed.html", 30 | }, 31 | ], 32 | } 33 | 34 | 35 | class GenerateFeedTestCase(TestCase): 36 | def test_generate_jsonfeed_feed(self): 37 | with tempfile.TemporaryDirectory(dir=os.getcwd(), prefix="generate_jsonfeed_feed") as dir: 38 | settings = { 39 | "FEED_FILENAME": f"{dir}/feed_jsonfeed.org.json", 40 | "FEED_TITLE": "JSON Feed", 41 | "FEED_ICON": "https://micro.blog/jsonfeed/avatar.jpg", 42 | "FEED_HOMEPAGE_URL": "https://www.jsonfeed.org/", 43 | "FEED_URL": "https://www.jsonfeed.org/feed.json", 44 | "FEED_VERSION": "1", 45 | "FEED_FUNCTION": None, 46 | "FEED_MAX_ITEMS": 100, 47 | } 48 | 49 | mock_return_value = thttp.Response(None, None, JSONFEED_ORG_JSONFEED, 200, None, None, None) 50 | with mock.patch("feeder.feeder.thttp.request", return_value=mock_return_value): 51 | generate(settings=settings) 52 | 53 | original_feed = JSONFEED_ORG_JSONFEED 54 | 55 | with open(f"{dir}/feed_jsonfeed.org.json", "r") as f: 56 | generated_feed = json.loads(f.read()) 57 | 58 | self.assertDictEqual(original_feed, generated_feed) 59 | 60 | def test_generate_feed_with_items(self): 61 | with tempfile.TemporaryDirectory(dir=os.getcwd(), prefix="feed_with_items") as dir: 62 | settings = { 63 | "FEED_FILENAME": f"{dir}/feed_with_items.json", 64 | "FEED_TITLE": "Test Feed", 65 | "FEED_ICON": None, 66 | "FEED_HOMEPAGE_URL": "https://www.example.org/", 67 | "FEED_URL": None, 68 | "FEED_VERSION": "1.1", 69 | "FEED_FUNCTION": dir.split("/")[-1] + ".core.fn", 70 | "FEED_MAX_ITEMS": 100, 71 | } 72 | 73 | with open(dir + "/core.py", "w") as f: 74 | f.write("from feeder.feeder import FeedItem\n\n") 75 | f.write("def fn():\n") 76 | f.write( 77 | " return [FeedItem('https://example.org', 'https://example.org', 'Example', None, 'example.org
', '2023‐07‐04T00:25:39+00:00')]" # noqa 78 | ) 79 | f.flush() 80 | os.fsync(f.fileno()) 81 | 82 | generate(settings=settings) 83 | 84 | with open(f"{dir}/feed_with_items.json", "r") as f: 85 | generated_feed = json.loads(f.read()) 86 | 87 | self.assertEqual(generated_feed["home_page_url"], "https://www.example.org/") 88 | self.assertTrue("feed_url" not in generated_feed) 89 | self.assertEqual(len(generated_feed["items"]), 1) 90 | --------------------------------------------------------------------------------