Compare commits

...
15 changed files with 609 additions and 28 deletions

No files matched your search

+15 -21
View File
@@ -1,26 +1,20 @@
# Read the Docs config for https://docs.archivebox.io
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
submodules:
include: all
recursive: true
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.12"
#nodejs: "20" # not needed unless we need the full archivebox to run while building docs for some reason
sphinx:
configuration: docs/conf.py
formats:
- pdf
- epub
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python: "3.13"
jobs:
build:
html:
- uv run --no-project --with-requirements .github/docs/requirements.txt sphinx-build -W --keep-going -b html -c .github/docs docs "$READTHEDOCS_OUTPUT/html"
submodules:
include:
- docs
python:
install:
- requirements: docs/requirements.txt
- method: uv
command: pip
requirements: .github/docs/requirements.txt
sphinx:
configuration: .github/docs/conf.py
fail_on_warning: true
+33
View File
@@ -0,0 +1,33 @@
# Building the 0.8.6rc1 documentation
The `docs` submodule remains pinned to the wiki revision shipped with 0.8.6rc1.
This configuration renders that historical user guide without installing
ArchiveBox, importing Django, or updating the wiki checkout.
Prerequisites: [uv](https://docs.astral.sh/uv/getting-started/installation/) 0.11.3 and Python 3.13, matching CI and Read the Docs. `uv` can install Python when needed.
```sh
git submodule update --init docs
uv run --no-project --python 3.13 --with-requirements .github/docs/requirements.txt \
sphinx-build -W --keep-going -b html -c .github/docs docs /tmp/archivebox-docs
```
`conf.py` adapts GitHub wiki links, heading levels, and obsolete local anchors
in memory. Search options without a prose section link to their exact
0.8.6rc1 source definition; the removed USE_CURL option points to CURL_BINARY. The original user documentation is not rewritten.
Generated Python autodoc scaffolding is outside this standalone user guide;
its legacy Django-dependent builder is not loaded.
Read the Docs uses `.github/.readthedocs.yaml` and the same strict HTML command.
Keep its project configuration-file setting pointed at that path.
The release label follows the immutable `v0.8.6rc1` tag; its original
`pyproject.toml` still declares `0.8.6rc0` and is left unchanged.
`requirements.txt` pins the complete documentation dependency set. To update it,
edit `requirements.in` and run:
```sh
uv pip compile --universal --python-version 3.13 .github/docs/requirements.in \
--output-file .github/docs/requirements.txt
```
+199
View File
@@ -0,0 +1,199 @@
"""Build the pinned 0.8.6rc1 user guide without importing ArchiveBox or Django."""
import os
import re
from pathlib import Path
from urllib.parse import unquote
from markdown_it import MarkdownIt
from myst_parser.mdit_to_docutils.base import default_slugify
ROOT = Path(__file__).resolve().parents[2]
DOCS = ROOT / "docs"
project = "ArchiveBox"
author = "ArchiveBox contributors"
copyright = "ArchiveBox contributors"
html_show_sphinx = False
# The release tag is authoritative: its pyproject still declares 0.8.6rc0.
release = "0.8.6rc1"
version = release
extensions = ["myst_parser", "sphinx_rtd_theme", "sphinxcontrib.mermaid"]
source_suffix = {".rst": "restructuredtext", ".md": "markdown"}
root_doc = "index"
# These are generated autodoc scaffolding, not standalone user documentation.
exclude_patterns = [
"archivebox*.rst",
"modules.rst",
"apidocs/**",
"_Sidebar.md",
"_Footer.md",
".git",
]
html_theme = "sphinx_rtd_theme"
html_theme_options = {
"navigation_depth": 3,
"collapse_navigation": False,
"version_selector": True,
"language_selector": False,
}
html_context = {"current_version": os.environ.get("READTHEDOCS_VERSION", release)}
html_logo = str(DOCS / "logo.png")
html_static_path = [str(DOCS / "_static")]
html_title = f"ArchiveBox {release} documentation"
myst_heading_anchors = 6
myst_fence_as_directive = ["mermaid"]
def rewrite_prose(text, transform):
"""Rewrite navigation outside fenced and indented code blocks."""
lines = text.splitlines(keepends=True)
output = []
start = 0
for token in MarkdownIt().parse(text):
if token.type not in {"fence", "code_block"}:
continue
first, last = token.map
output.append(transform("".join(lines[start:first])))
output.append("".join(lines[first:last]))
start = last
output.append(transform("".join(lines[start:])))
return "".join(output)
def prepare_source(app, docname, source):
"""Adapt GitHub wiki navigation in memory, preserving the pinned sources."""
text = source[0]
if docname == "Contents":
text = re.sub(r"API Reference\n#############.*?(?=Meta\n####)", "", text, flags=re.DOTALL)
text += "\n.. toctree::\n :hidden:\n\n Home\n ArchiveBox-Architecture-Diagrams\n Upgrading-or-Merging-Archives\n"
if docname == "index":
text = text.replace("archivebox info", "archivebox status")
text = text.replace("pip install archivebox", f"pip install archivebox=={release}")
text = text.replace("ArchiveBox/ArchiveBox/tree/master", f"ArchiveBox/ArchiveBox/tree/v{release}")
text = text.replace(
"`Github <https://github.com/ArchiveBox/ArchiveBox/issues>`_",
"`GitHub issues <https://github.com/ArchiveBox/ArchiveBox/issues>`_",
)
text = text.replace(
"==========\nArchiveBox\n==========",
f"ArchiveBox {release}\n" + "=" * (11 + len(release)),
)
if Path(app.env.doc2path(docname)).suffix == ".md":
def wiki_link(match):
parts = match.group(1).split("|", 1)
label, target = (parts[0], parts[-1])
page, separator, anchor = unquote(target).partition("#")
page = page.replace(" ", "-")
page = {"Setting-Up-Authentication": "Setting-up-Authentication"}.get(page, page)
suffix = f"#{anchor}" if separator else ""
return f"[{label}]({page}.md{suffix})"
text = rewrite_prose(text, lambda prose: re.sub(r"\[\[([^\]\n]+)\]\]", wiki_link, prose))
source[0] = text
# GitHub wiki headings allow skipped levels; normalize their hierarchy before MyST
# parses them, without changing fenced code blocks or the historical source files.
def normalize_markdown(app, docname, source):
if Path(app.env.doc2path(docname)).suffix != ".md":
return
text = source[0]
if docname == "Upgrading":
text = text.replace("---\n---", "---")
if docname == "Scheduled-Archiving":
text = text.replace("```bash\n$ cd", "```console\n$ cd", 1)
if docname == "Setting-up-Authentication":
text = text.replace(
"[Wiki: Configuration (`PUBLIC_ADD_VIEW`, `PUBLIC_SNAPSHOTS`, `PUBLIC_INDEX`)]()",
"[Wiki: Configuration (`PUBLIC_ADD_VIEW`, `PUBLIC_SNAPSHOTS`, `PUBLIC_INDEX`)](Configuration.md#public_index--public_snapshots--public_add_view)",
)
if docname in {"Home", "README", "Donations", "Upgrading-or-Merging-Archives"}:
text = "# " + ("ArchiveBox overview" if docname == "README" else docname.replace("-", " ")) + "\n\n" + text
lines = text.splitlines()
stack = []
for token in MarkdownIt().parse(text):
if token.type != "heading_open":
continue
original = int(token.tag[1])
while stack and stack[-1] >= original:
stack.pop()
stack.append(original)
line = token.map[0]
lines[line] = re.sub(r"^#{1,6}(?=\s)", "#" * len(stack), lines[line]).replace("\ufe0f", "")
text = "\n".join(lines) + "\n"
def link(match):
target = match.group(1)
if "://" in target or target.startswith("mailto:"):
return match.group(0)
page, sep, anchor = target.partition("#")
if page and (DOCS / (page + ".md")).exists():
page += ".md"
anchor = unquote(anchor).lower()
if docname == "Install" and not page:
anchor = {
"option-a-docker--docker-compose-setup-️": "option-a-docker--docker-compose-setup-",
"upgrading-archivebox-to-a-new-version": "next-steps-upgrading-archivebox-to-a-new-version",
"next-steps": "next-steps-add-some-urls-to-archive-and-try-out-cli--web-ui",
"ubuntudebian": "ubuntudebian-based-systems",
"bsd": "freebsd",
"manual-setup": "option-c-bare-metal-setup",
}.get(anchor, anchor)
if docname == "README" and not page:
anchor = {
"input-formats": "input-formats-how-to-pass-urls-into-archivebox-for-saving",
"output-formats": "output-formats-what-archivebox-saves-for-each-url",
}.get(anchor, anchor)
if (docname == "Configuration" and not page) or page == "Configuration.md":
other = (DOCS / "Configuration.md").read_text()
for heading in re.findall(r"^#+\s+(.+)$", other, re.MULTILINE):
if anchor in [word.lower() for word in re.findall(r"`([^`]+)`", heading)]:
anchor = default_slugify(heading.replace("`", ""))
break
aliases = {"templates_dir": "custom_templates_dir"}
anchor = aliases.get(anchor, anchor)
if anchor in {
"search_backend_timeout",
"search_backend_engine",
"use_curl",
}:
if anchor == "use_curl":
# This obsolete wiki option no longer exists in the 0.8 config.
return "](Configuration.md#curl_binary)"
code = (ROOT / "archivebox/config/common.py").read_text().splitlines()
number = next(i + 1 for i, line in enumerate(code) if anchor.upper() in line)
return "](https://github.com/ArchiveBox/ArchiveBox/blob/v0.8.6rc1/archivebox/config/common.py#L" + str(number) + ")"
if docname == "README" and anchor == "saves-lots-of-useful-stuff-for-each-imported-link":
anchor = "output-formats-what-archivebox-saves-for-each-url"
if docname == "Web-Archiving-Community":
anchor = {
"blogs": "blogs-friends-of-archivebox",
"articles": "articles-we-like-about-internet-archiving",
}.get(anchor, anchor)
return "](" + page + ("#" + anchor if sep else "") + ")"
def rewrite_links(prose):
prose = re.sub(r"\]\(([^)\s]+)\)", link, prose)
# Raw HTML links are outside MyST's cross-reference resolver.
html_anchors = {
"background--motivation": "background-motivation",
"Caveats": "caveats",
"contents": "web-archiving-community",
"%EF%B8%8F-cli-usage": "cli-usage",
"url_whitelist": "url-allowlist",
"url_blacklist": "url-denylist",
"input-formats": "input-formats-how-to-pass-urls-into-archivebox-for-saving",
}
for old, new in html_anchors.items():
prose = prose.replace(f'href="#{old}"', f'href="#{new}"')
prose = prose.replace("#️-cli-usage", "#cli-usage")
return prose
text = rewrite_prose(text, rewrite_links)
source[0] = text
def setup(app):
app.connect("source-read", prepare_source)
app.connect("source-read", normalize_markdown)
+4
View File
@@ -0,0 +1,4 @@
Sphinx==8.2.3
myst-parser==4.0.1
sphinx-rtd-theme==3.1.0
sphinxcontrib-mermaid==2.1.1
+81
View File
@@ -0,0 +1,81 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --universal --python-version 3.13 .github/docs/requirements.in --output-file .github/docs/requirements.txt
alabaster==1.0.0
# via sphinx
babel==2.18.0
# via sphinx
certifi==2026.7.22
# via requests
charset-normalizer==3.5.1
# via requests
colorama==0.4.6 ; sys_platform == 'win32'
# via sphinx
docutils==0.21.2
# via
# myst-parser
# sphinx
# sphinx-rtd-theme
idna==3.20
# via requests
imagesize==2.0.1
# via sphinx
jinja2==3.1.6
# via
# myst-parser
# sphinx
# sphinxcontrib-mermaid
markdown-it-py==3.0.0
# via
# mdit-py-plugins
# myst-parser
markupsafe==3.0.3
# via jinja2
mdit-py-plugins==0.6.1
# via myst-parser
mdurl==0.1.2
# via markdown-it-py
myst-parser==4.0.1
# via -r .github/docs/requirements.in
packaging==26.3
# via sphinx
pygments==2.21.0
# via sphinx
pyyaml==6.0.3
# via
# myst-parser
# sphinxcontrib-mermaid
requests==2.34.2
# via sphinx
roman-numerals==4.1.0
# via roman-numerals-py
roman-numerals-py==4.1.0
# via sphinx
snowballstemmer==3.1.1
# via sphinx
sphinx==8.2.3
# via
# -r .github/docs/requirements.in
# myst-parser
# sphinx-rtd-theme
# sphinxcontrib-jquery
# sphinxcontrib-mermaid
sphinx-rtd-theme==3.1.0
# via -r .github/docs/requirements.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jquery==4.1
# via sphinx-rtd-theme
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-mermaid==2.1.1
# via -r .github/docs/requirements.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
urllib3==2.8.0
# via requests
+5 -2
View File
@@ -3,8 +3,9 @@ name: Build Docker image
on:
workflow_dispatch:
push:
branches:
- '**'
branches-ignore:
- docs
- 'docs/**'
tags:
- 'v*'
# pull_request:
@@ -14,6 +15,8 @@ env:
jobs:
buildx:
# Also prevent a manual dispatch on a docs repair branch from publishing.
if: github.ref != 'refs/heads/docs' && !startsWith(github.ref, 'refs/heads/docs/')
runs-on: ubuntu-latest
steps:
- name: Checkout
+34
View File
@@ -0,0 +1,34 @@
name: Historical Sphinx documentation
on:
pull_request:
branches: [docs/restore-08-sphinx]
paths: ['.github/docs/**', '.github/.readthedocs.yaml', '.github/workflows/docs.yml', 'docs', 'README.md']
push:
branches: [docs/restore-08-sphinx]
paths: ['.github/docs/**', '.github/.readthedocs.yaml', '.github/workflows/docs.yml', 'docs', 'README.md']
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- run: git submodule update --init docs
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
version: '0.11.3'
python-version: '3.13'
- name: Build the pinned historical user guide
env:
READTHEDOCS: 'True'
READTHEDOCS_VERSION: v0.8.6rc1
run: uv run --no-project --with-requirements .github/docs/requirements.txt sphinx-build -W --keep-going -b html -c .github/docs docs "$RUNNER_TEMP/docs-html"
- name: Verify the version selector
run: |
grep -q 'class="version-switch"' "$RUNNER_TEMP/docs-html/index.html"
grep -q 'readthedocs-addons-data-ready' "$RUNNER_TEMP/docs-html/_static/js/versions.js"
+1 -1
View File
@@ -164,7 +164,7 @@ class BaseModelWithStateMachine(models.Model, MachineMixin):
@classproperty
def ACTIVE_STATE(cls) -> str:
return cls._state_to_str(cls.StateMachineClass.active_state)
return cls._state_to_str(cls.active_state)
@classproperty
def INITIAL_STATE(cls) -> str:
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2025-12-29 10:16
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0074_alter_snapshot_downloaded_at'),
]
operations = [
migrations.AddField(
model_name='archiveresult',
name='retry_at',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
),
]
@@ -0,0 +1,71 @@
# Generated by Django 5.1.2 on 2025-12-29 10:16
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0075_archiveresult_retry_at'),
('crawls', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='snapshot',
name='crawl',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='snapshot_set', to='crawls.crawl'),
),
migrations.AddField(
model_name='snapshot',
name='retry_at',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
),
migrations.AddField(
model_name='snapshot',
name='status',
field=models.CharField(choices=[('queued', 'Queued'), ('started', 'Started'), ('sealed', 'Sealed')], db_index=True, default='queued', max_length=15),
),
migrations.AlterField(
model_name='archiveresult',
name='cmd',
field=models.JSONField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='archiveresult',
name='end_ts',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='archiveresult',
name='extractor',
field=models.CharField(choices=[('htmltotext', 'htmltotext'), ('git', 'git'), ('singlefile', 'singlefile'), ('media', 'media'), ('archive_org', 'archive_org'), ('readability', 'readability'), ('mercury', 'mercury'), ('favicon', 'favicon'), ('pdf', 'pdf'), ('headers', 'headers'), ('screenshot', 'screenshot'), ('dom', 'dom'), ('title', 'title'), ('wget', 'wget')], db_index=True, max_length=32),
),
migrations.AlterField(
model_name='archiveresult',
name='output',
field=models.CharField(blank=True, default=None, max_length=1024, null=True),
),
migrations.AlterField(
model_name='archiveresult',
name='pwd',
field=models.CharField(blank=True, default=None, max_length=256, null=True),
),
migrations.AlterField(
model_name='archiveresult',
name='snapshot',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.snapshot'),
),
migrations.AlterField(
model_name='archiveresult',
name='start_ts',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='archiveresult',
name='status',
field=models.CharField(choices=[('queued', 'Queued'), ('started', 'Started'), ('backoff', 'Waiting to retry'), ('succeeded', 'Succeeded'), ('failed', 'Failed'), ('skipped', 'Skipped')], db_index=True, default='queued', max_length=15),
),
]
@@ -0,0 +1,78 @@
# Generated by Django 5.1.2 on 2025-12-29 10:16
import abid_utils.models
import charidfield.fields
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import statemachine.mixins
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0075_archiveresult_retry_at'),
('seeds', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CrawlSchedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('num_uses_failed', models.PositiveIntegerField(default=0)),
('num_uses_succeeded', models.PositiveIntegerField(default=0)),
('schedule', models.CharField(max_length=64)),
('is_enabled', models.BooleanField(default=True)),
('created_at', abid_utils.models.AutoDateTimeField(db_index=True, default=None)),
('modified_at', models.DateTimeField(auto_now=True)),
('created_by', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Crawl',
fields=[
('num_uses_failed', models.PositiveIntegerField(default=0)),
('num_uses_succeeded', models.PositiveIntegerField(default=0)),
('id', models.UUIDField(default=None, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('abid', charidfield.fields.CharIDField(blank=True, db_index=True, default=None, help_text='ABID-format identifier for this entity (e.g. snp_01BJQMF54D093DXEAWZ6JYRPAQ)', max_length=30, null=True, prefix='crl_', unique=True)),
('created_at', abid_utils.models.AutoDateTimeField(db_index=True, default=None)),
('modified_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('queued', 'Queued'), ('started', 'Started'), ('sealed', 'Sealed')], db_index=True, default='queued', max_length=15)),
('retry_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
('max_depth', models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(4)])),
('tags_str', models.CharField(blank=True, default='', max_length=1024)),
('persona', models.CharField(blank=True, default='auto', max_length=32)),
('config', models.JSONField(default=dict)),
('created_by', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='crawl_set', to=settings.AUTH_USER_MODEL)),
('seed', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='crawl_set', to='seeds.seed')),
('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='crawls.crawlschedule')),
],
options={
'verbose_name': 'Crawl',
'verbose_name_plural': 'Crawls',
},
bases=(models.Model, statemachine.mixins.MachineMixin),
),
migrations.CreateModel(
name='Outlink',
fields=[
('id', models.UUIDField(default=None, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('src', models.URLField()),
('dst', models.URLField()),
('crawl', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outlink_set', to='crawls.crawl')),
('via', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='outlink_set', to='core.archiveresult')),
],
options={
'unique_together': {('src', 'dst', 'via')},
},
),
]
+2 -2
View File
@@ -80,8 +80,8 @@ def write_html_link_details(link: Link, out_dir: Optional[str]=None) -> None:
@enforce_types
def link_details_template(link: Link) -> str:
from abx_plugin_wget_extractor.wget import wget_output_path
from abx_plugin_wget.wget import wget_output_path
SAVE_ARCHIVE_DOT_ORG = abx.pm.hook.get_FLAT_CONFIG().SAVE_ARCHIVE_DOT_ORG
+1 -2
View File
@@ -427,8 +427,7 @@ class Link:
"""predict the expected output paths that should be present after archiving"""
from abx_plugin_wget.wget import wget_output_path
FAVICON_CONFIG = abx.pm.hook.get_CONFIGS().favicon
from abx_plugin_favicon.config import FAVICON_CONFIG
# TODO: banish this awful duplication from the codebase and import these
# from their respective extractor files
@@ -0,0 +1,26 @@
# Generated by Django 5.1.2 on 2025-12-29 10:16
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0074_alter_snapshot_downloaded_at'),
]
operations = [
migrations.CreateModel(
name='SinglefileResult',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('core.archiveresult',),
),
]
@@ -0,0 +1,40 @@
# Generated by Django 5.1.2 on 2025-12-29 10:16
import abid_utils.models
import charidfield.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Seed',
fields=[
('num_uses_failed', models.PositiveIntegerField(default=0)),
('num_uses_succeeded', models.PositiveIntegerField(default=0)),
('id', models.UUIDField(default=None, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('abid', charidfield.fields.CharIDField(blank=True, db_index=True, default=None, help_text='ABID-format identifier for this entity (e.g. snp_01BJQMF54D093DXEAWZ6JYRPAQ)', max_length=30, null=True, prefix='src_', unique=True)),
('uri', models.URLField(max_length=2000)),
('extractor', models.CharField(default='auto', max_length=32)),
('tags_str', models.CharField(blank=True, default='', max_length=255)),
('config', models.JSONField(default=dict)),
('created_at', abid_utils.models.AutoDateTimeField(db_index=True, default=None)),
('modified_at', models.DateTimeField(auto_now=True)),
('created_by', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Seed',
'verbose_name_plural': 'Seeds',
'unique_together': {('created_by', 'uri', 'extractor')},
},
),
]