File size: 5,772 Bytes
15eb4b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb9acda
 
 
 
 
 
 
 
 
 
15eb4b9
bb9acda
15eb4b9
bb9acda
 
 
 
15eb4b9
 
 
 
bb9acda
 
 
 
 
15eb4b9
 
 
 
 
8cfe0c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15eb4b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8eaca44
 
 
 
 
 
 
 
15eb4b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""HF Storage Bucket helpers: list available CC crawls and validate the target.

All functions degrade gracefully: listing falls back to a static crawl list, and
validation returns a (ok, message) tuple instead of raising, so the UI can show a
friendly message.
"""
from __future__ import annotations

import re
from typing import List, Optional, Tuple

from . import config

_CRAWL_RE = re.compile(r"(CC-MAIN-\d{4}-\d{2})")


def _item_path(item) -> str:
    for attr in ("path", "name", "key"):
        val = getattr(item, attr, None)
        if isinstance(val, str) and val:
            return val
    return str(item)


def _crawls_under(api, prefix: str, token: Optional[str]) -> set:
    """Crawl ids found among the immediate children of `prefix` on the CC bucket."""
    crawls = set()
    for item in api.list_bucket_tree(config.CC_BUCKET, prefix=prefix, recursive=False, token=token):
        m = _CRAWL_RE.search(_item_path(item))
        if m:
            crawls.add(m.group(1))
    return crawls


def list_available_crawls(token: Optional[str] = None) -> List[str]:
    """Crawl ids usable for repackaging, newest first.

    A crawl is only usable if BOTH its WARC data (``crawl-data/``) and its columnar
    index (``cc-index/table/cc-main/warc/``) are present, so we return the
    intersection of crawls found under both prefixes. Falls back to
    ``config.FALLBACK_CRAWLS`` on any error (offline, auth, API change)."""
    try:
        from huggingface_hub import HfApi

        api = HfApi()
        data_crawls = _crawls_under(api, "crawl-data", token)
        index_crawls = _crawls_under(api, config.CC_INDEX_SUBPATH.rstrip("/"), token)
        usable = data_crawls & index_crawls
        if usable:
            return sorted(usable, reverse=True)
    except Exception:
        pass
    return list(config.FALLBACK_CRAWLS)


# Org roles that grant write access to the org's buckets.
_WRITE_ROLES = {"admin", "write", "contributor"}


def list_writable_buckets(token: Optional[str] = None) -> List[str]:
    """Bucket ids ("namespace/bucket") the signed-in user can write to.

    Covers the user's own buckets plus buckets in organizations where they have a
    write role. Returns [] on any error (offline, not signed in)."""
    if not token:
        return []
    try:
        from huggingface_hub import HfApi

        api = HfApi()
        me = api.whoami(token=token)
        namespaces = [me["name"]]
        namespaces += [
            o["name"] for o in me.get("orgs", []) if o.get("roleInOrg") in _WRITE_ROLES
        ]
        ids = set()
        for ns in namespaces:
            try:
                for b in api.list_buckets(namespace=ns, token=token):
                    bid = getattr(b, "id", None)
                    if bid:
                        ids.add(bid)
            except Exception:  # noqa: BLE001 - skip a namespace we can't list
                continue
        return sorted(ids)
    except Exception:  # noqa: BLE001
        return []


def parse_targets(text: str) -> List[str]:
    """Split a free-text list of hostnames/domains (commas, whitespace, newlines)."""
    if not text:
        return []
    parts = re.split(r"[\s,]+", text.strip())
    return [p.strip().lower() for p in parts if p.strip()]


# Hostnames/domains only contain these characters (also what cdx_toolkit accepts).
_HOST_RE = re.compile(r"^[A-Za-z0-9.\-]+$")


def validate_targets(hostnames: List[str], domains: List[str]) -> Tuple[bool, str]:
    if not hostnames and not domains:
        return False, "Enter at least one hostname or domain to filter for."
    for value in hostnames + domains:
        if not _HOST_RE.match(value) or "." not in value:
            return False, f"Invalid hostname/domain: {value!r} (letters, digits, dot, hyphen only)."
    return True, ""


def validate_languages(languages: List[str]) -> Tuple[bool, str]:
    """Language codes must come from the hardcoded ISO-639-3 list (empty = any)."""
    unknown = [c for c in (languages or []) if c not in config.CONTENT_LANGUAGE_CODES]
    if unknown:
        return False, "Unknown ISO-639-3 language code(s): " + ", ".join(unknown)
    return True, ""


def validate_crawls(crawls: List[str]) -> Tuple[bool, str]:
    if not crawls:
        return False, "Select at least one crawl."
    if len(crawls) > config.COST_GUARD_MAX_CRAWLS:
        return (
            True,
            f"⚠️ {len(crawls)} crawls selected (> {config.COST_GUARD_MAX_CRAWLS}). The index scan "
            "may be large and slow; consider fewer crawls.",
        )
    return True, ""


def validate_target_bucket(
    bucket_id: str, path: str, token: Optional[str] = None
) -> Tuple[bool, str]:
    """Confirm the target is an existing HF **bucket** (not a dataset/model repo).

    Dataset repos are not supported as a target (HF Jobs cannot mount/write a
    dataset the way it mounts a bucket), which `bucket_info` enforces naturally:
    it only resolves bucket ids.
    """
    bucket_id = (bucket_id or "").strip().strip("/")
    if bucket_id.count("/") != 1:
        return False, "Target bucket must be of the form `namespace/bucket`."
    if any(c in (path or "") for c in ("..",)) or (path or "").startswith("/"):
        return False, "Target path must be a relative sub-path inside the bucket."
    try:
        from huggingface_hub import HfApi

        HfApi().bucket_info(bucket_id, token=token)
    except Exception as e:  # noqa: BLE001 - surface a friendly message
        return (
            False,
            f"Could not find a writable bucket `{bucket_id}`. It must exist before launching "
            f"(dataset repos are not supported as a target). Details: {type(e).__name__}: {e}",
        )
    return True, f"Target bucket `{bucket_id}` found."