proxmint-haystack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from haystack_integrations.components.fetchers.proxmint._gateway import GatewayError, proxy_username
3
+ from haystack_integrations.components.fetchers.proxmint.fetcher import ProxmintFetcher
4
+
5
+ __all__ = ["GatewayError", "ProxmintFetcher", "proxy_username"]
@@ -0,0 +1,239 @@
1
+ """Proxmint credentials, gateway usernames and one fetch through the proxy.
2
+
3
+ Targeting rides in the proxy username: the login, then ``__``, then ``key.value``
4
+ parameters joined by ``;``, for example ``login__cr.us;city.newyork;sessid.ab12;sessttl.30``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import secrets
12
+ import unicodedata
13
+ from dataclasses import dataclass
14
+ from html.parser import HTMLParser
15
+
16
+ import httpx
17
+
18
+ ACCOUNT_URL = "https://proxmint.com/api/v1/account"
19
+ PRODUCTS = ("residential", "premium-residential", "datacenter", "mobile")
20
+ USER_AGENT = (
21
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
22
+ "(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
23
+ )
24
+
25
+ # What the gateway puts in the status line when it refuses a request itself.
26
+ GATEWAY_ERRORS = {
27
+ "SITE_PERMANENTLY_BLOCKED": "The site is on the network's blocked list: https://proxmint.com/docs/blocked-websites",
28
+ "PORT_BLOCKED": "The destination port is closed. Web traffic works on 80, 443, 8080, 8443, 8888 and 8090.",
29
+ "HOST_BLOCKED": "You blocked this host yourself under Blocked hosts on your product page.",
30
+ "NO_USER": "The gateway doesn't recognise the login. Copy the credentials again from your dashboard.",
31
+ "TRAFFIC_EXHAUSTED": "The product's bandwidth is used up. Add a pack at https://proxmint.com/pricing",
32
+ "THREADS_EXHAUSTED": "Too many connections at once. Lower your concurrency.",
33
+ "USER_BLOCKED": "The proxy user is blocked, usually after many wrong passwords. Contact support@proxmint.com",
34
+ "NO_HOST_CONNECTION": "The exit IP couldn't reach the site. Retry: a new request gets a new IP.",
35
+ "NO_RAY": "No IPs match your targeting. Drop the city, ZIP or ASN, or check the location spelling.",
36
+ }
37
+
38
+ _FOLD = str.maketrans(
39
+ {"ł": "l", "ø": "o", "æ": "ae", "œ": "oe", "ß": "ss", "đ": "d", "ð": "d", "þ": "th", "ı": "i", "ħ": "h"}
40
+ )
41
+
42
+
43
+ class GatewayError(Exception):
44
+ """The Proxmint gateway refused the request (bad login, no bandwidth, blocked site...)."""
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Credentials:
49
+ username: str
50
+ password: str
51
+ host: str = "gw.proxmint.com"
52
+ port: int = 823
53
+
54
+
55
+ def load_credentials(
56
+ api_key: str | None = None,
57
+ product: str = "residential",
58
+ *,
59
+ username: str | None = None,
60
+ password: str | None = None,
61
+ client: httpx.Client | None = None,
62
+ ) -> Credentials:
63
+ """Proxy login for one product.
64
+
65
+ A username and password (arguments, else ``PROXMINT_USERNAME`` / ``PROXMINT_PASSWORD``)
66
+ are used as they are. Otherwise the API key (argument, else ``PROXMINT_API_KEY``) reads
67
+ them from the account, which also picks the right product.
68
+ """
69
+ if product not in PRODUCTS:
70
+ raise ValueError(f"product must be one of {', '.join(PRODUCTS)}")
71
+ if not api_key:
72
+ username = username or os.getenv("PROXMINT_USERNAME")
73
+ password = password or os.getenv("PROXMINT_PASSWORD")
74
+ if username and password:
75
+ return Credentials(username, password)
76
+ api_key = os.getenv("PROXMINT_API_KEY")
77
+ if not api_key:
78
+ raise ValueError(
79
+ "Set PROXMINT_API_KEY (create one under Settings at https://proxmint.com/dashboard) "
80
+ "or PROXMINT_USERNAME and PROXMINT_PASSWORD."
81
+ )
82
+
83
+ get = client.get if client else httpx.get
84
+ r = get(ACCOUNT_URL, headers={"Authorization": f"Bearer {api_key}"}, timeout=20)
85
+ if r.status_code == 401:
86
+ raise ValueError("Proxmint rejected the API key. Create a new one under Settings in the dashboard.")
87
+ r.raise_for_status()
88
+ for p in r.json()["products"]:
89
+ if p["product"] != product:
90
+ continue
91
+ if p["status"] != "active":
92
+ raise ValueError(f"Your {product} product is {p['status']}. Top it up at https://proxmint.com/dashboard")
93
+ px = p["proxy"]
94
+ return Credentials(px["username"], px["password"], px["host"], int(px["httpPort"]))
95
+ raise ValueError(f"This account has no {product} product. Buy a pack at https://proxmint.com/pricing")
96
+
97
+
98
+ def _geo(value: str) -> str:
99
+ """A place name in the gateway's form: "São Paulo" -> saopaulo, "Łódź" -> lodz."""
100
+ s = unicodedata.normalize("NFKD", value.lower())
101
+ s = "".join(c for c in s if not unicodedata.combining(c)).translate(_FOLD)
102
+ return re.sub(r"[^a-z0-9]", "", s)
103
+
104
+
105
+ def proxy_username(
106
+ login: str,
107
+ *,
108
+ country: str | None = None,
109
+ state: str | None = None,
110
+ city: str | None = None,
111
+ zip_code: str | None = None,
112
+ asn: str | int | None = None,
113
+ session_id: str | None = None,
114
+ session_minutes: int | None = None,
115
+ ) -> str:
116
+ """The login with targeting and sticky-session parameters appended."""
117
+ if (state or city or zip_code or asn) and not country:
118
+ raise ValueError("state, city, zip_code and asn need a country as well.")
119
+ params = []
120
+ if country:
121
+ params.append("cr." + re.sub(r"[^a-z]", "", country.lower())[:2])
122
+ if city:
123
+ params.append("city." + _geo(city))
124
+ if state:
125
+ params.append("state." + _geo(state))
126
+ if zip_code:
127
+ params.append("zip." + str(zip_code).strip())
128
+ if asn:
129
+ params.append("asn." + re.sub(r"\D", "", str(asn)))
130
+ if session_id:
131
+ params.append("sessid." + _geo(session_id))
132
+ if session_minutes:
133
+ params.append(f"sessttl.{max(1, min(120, round(session_minutes)))}")
134
+ return f"{login}__{';'.join(params)}" if params else login
135
+
136
+
137
+ def new_session_id() -> str:
138
+ return secrets.token_hex(6)
139
+
140
+
141
+ def client_for(creds: Credentials, username: str, *, timeout: float = 60.0, headers: dict | None = None) -> httpx.Client:
142
+ """An HTTP client whose every request leaves through the gateway as ``username``."""
143
+ proxy = httpx.Proxy(f"http://{creds.host}:{creds.port}", auth=(username, creds.password))
144
+ return httpx.Client(
145
+ proxy=proxy,
146
+ timeout=timeout,
147
+ follow_redirects=True,
148
+ max_redirects=5,
149
+ headers={"User-Agent": USER_AGENT, **(headers or {})},
150
+ )
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class Page:
155
+ url: str
156
+ status: int
157
+ content_type: str
158
+ html: str
159
+ title: str | None
160
+
161
+ def text(self) -> str:
162
+ return html_to_text(self.html) if "html" in self.content_type else self.html
163
+
164
+
165
+ def fetch(client: httpx.Client, url: str) -> Page:
166
+ """GET one page. Gateway refusals raise GatewayError; the site's own answers return as a Page."""
167
+ if not re.match(r"https?://", url):
168
+ raise ValueError(f"Not an http(s) URL: {url}")
169
+ try:
170
+ r = client.get(url)
171
+ except httpx.ProxyError as e: # HTTPS: the refusal comes back on the CONNECT
172
+ raise _gateway_error(str(e)) from e
173
+ reason = r.reason_phrase.strip().upper()
174
+ if r.status_code in (400, 403, 407, 500, 502, 503) and reason in GATEWAY_ERRORS: # plain HTTP
175
+ raise _gateway_error(f"{r.status_code} {reason}")
176
+ ctype = r.headers.get("content-type", "")
177
+ body = r.text
178
+ title = _Text.parse(body).title if "html" in ctype else None
179
+ return Page(str(r.url), r.status_code, ctype, body, title)
180
+
181
+
182
+ def _gateway_error(status_line: str) -> GatewayError:
183
+ reason = status_line.split()[-1].upper() if status_line.split() else ""
184
+ hint = GATEWAY_ERRORS.get(reason, "See https://proxmint.com/docs/errors")
185
+ return GatewayError(f"Proxmint gateway: {status_line}. {hint}")
186
+
187
+
188
+ class _Text(HTMLParser):
189
+ SKIP = {"script", "style", "noscript", "template", "svg", "head"}
190
+ BLOCK = {
191
+ "address", "article", "aside", "blockquote", "br", "dd", "div", "dl", "dt", "figcaption", "footer",
192
+ "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "ol", "p", "pre",
193
+ "section", "table", "td", "th", "tr", "ul",
194
+ }
195
+
196
+ def __init__(self) -> None:
197
+ super().__init__(convert_charrefs=True)
198
+ self.parts: list[str] = []
199
+ self.title: str | None = None
200
+ self._skip = 0
201
+ self._in_title = False
202
+
203
+ @classmethod
204
+ def parse(cls, html: str) -> _Text:
205
+ p = cls()
206
+ p.feed(html)
207
+ p.close()
208
+ return p
209
+
210
+ def handle_starttag(self, tag, attrs):
211
+ if tag == "title":
212
+ self._in_title = self.title is None # the page's own, not an <svg><title>
213
+ if tag == "body":
214
+ self._skip = 0 # </head> is optional in HTML
215
+ elif tag in self.SKIP:
216
+ self._skip += 1
217
+ elif tag in self.BLOCK:
218
+ self.parts.append("\n")
219
+
220
+ def handle_endtag(self, tag):
221
+ if tag == "title":
222
+ self._in_title = False
223
+ if tag in self.SKIP:
224
+ self._skip = max(0, self._skip - 1)
225
+ elif tag in self.BLOCK:
226
+ self.parts.append("\n")
227
+
228
+ def handle_data(self, data):
229
+ if self._in_title:
230
+ self.title = ((self.title or "") + data).strip()
231
+ elif not self._skip:
232
+ self.parts.append(data)
233
+
234
+
235
+ def html_to_text(html: str) -> str:
236
+ """Readable text of a page: no scripts or styles, one line per block, blank lines squeezed."""
237
+ raw = "".join(_Text.parse(html).parts)
238
+ lines = (re.sub(r"[ \t\r\f\v]+", " ", line).strip() for line in raw.split("\n"))
239
+ return re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip()
@@ -0,0 +1,144 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """ProxmintFetcher: fetch web pages through Proxmint proxies and return Documents."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from typing import Any, Literal
8
+
9
+ import httpx
10
+ from haystack import Document, component, default_from_dict, default_to_dict
11
+ from haystack.utils import Secret, deserialize_secrets_inplace
12
+
13
+ from haystack_integrations.components.fetchers.proxmint._gateway import (
14
+ Credentials,
15
+ GatewayError,
16
+ client_for,
17
+ fetch,
18
+ load_credentials,
19
+ new_session_id,
20
+ proxy_username,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ Product = Literal["residential", "premium-residential", "datacenter", "mobile"]
26
+
27
+
28
+ @component
29
+ class ProxmintFetcher:
30
+ """Fetch web pages through Proxmint residential, datacenter or mobile proxies.
31
+
32
+ Every URL leaves from a new exit IP, in the country, city or ASN you ask for. Set
33
+ ``session_minutes`` to keep one IP across runs. The output is plain Documents, so
34
+ it plugs into a splitter, a converter or an embedder.
35
+
36
+ ```python
37
+ from haystack_integrations.components.fetchers.proxmint import ProxmintFetcher
38
+
39
+ fetcher = ProxmintFetcher(country="gb") # reads PROXMINT_API_KEY
40
+ result = fetcher.run(urls=["https://example.com"])
41
+ print(result["documents"][0].content)
42
+ ```
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ api_key: Secret = Secret.from_env_var("PROXMINT_API_KEY", strict=False), # noqa: B008
48
+ *,
49
+ product: Product = "residential",
50
+ country: str | None = None,
51
+ state: str | None = None,
52
+ city: str | None = None,
53
+ zip_code: str | None = None,
54
+ asn: str | None = None,
55
+ session_minutes: int | None = None,
56
+ output: Literal["text", "html"] = "text",
57
+ timeout: float = 60.0,
58
+ raise_on_failure: bool = False,
59
+ ) -> None:
60
+ """
61
+ :param api_key: Proxmint API key (`pmk_...`, created under Settings in the dashboard). Without one,
62
+ `PROXMINT_USERNAME` and `PROXMINT_PASSWORD` are read instead.
63
+ :param product: `residential`, `premium-residential`, `datacenter` or `mobile`.
64
+ :param country: ISO 3166 alpha-2 code, for example `us`. State, city, ZIP and ASN need a country.
65
+ :param session_minutes: Keep one exit IP for this many minutes (1-120), across runs. Default: a new IP per URL.
66
+ :param output: `text` for readable text, `html` for the raw markup.
67
+ :param raise_on_failure: Raise on a failed URL instead of skipping it (it is listed in `failed_urls`).
68
+ """
69
+ self.api_key = api_key
70
+ self.product = product
71
+ self.targeting = {"country": country, "state": state, "city": city, "zip_code": zip_code, "asn": asn}
72
+ self.session_minutes = session_minutes
73
+ self.output = output
74
+ self.timeout = timeout
75
+ self.raise_on_failure = raise_on_failure
76
+ self._creds: Credentials | None = None
77
+ self._session_id = new_session_id() if session_minutes else None
78
+
79
+ def warm_up(self) -> None:
80
+ """Read the proxy login once, so a pipeline run makes no extra API call."""
81
+ if self._creds is None:
82
+ self._creds = load_credentials(self.api_key.resolve_value(), self.product)
83
+
84
+ @component.output_types(documents=list[Document], failed_urls=list[str])
85
+ def run(self, urls: list[str], country: str | None = None) -> dict[str, Any]:
86
+ """
87
+ :param urls: Pages to fetch (`http` or `https`).
88
+ :param country: Overrides the `country` set at init for this run (the state, city, ZIP and ASN set at init are dropped).
89
+ :returns: `documents` (content = page text or HTML, meta = source, status, title...) and `failed_urls`.
90
+ """
91
+ self.warm_up()
92
+ assert self._creds is not None
93
+ targeting = self.targeting
94
+ if country and country.lower() != (targeting["country"] or "").lower():
95
+ targeting = {"country": country, "state": None, "city": None, "zip_code": None, "asn": None}
96
+ login = proxy_username(
97
+ self._creds.username,
98
+ **targeting,
99
+ session_id=self._session_id,
100
+ session_minutes=self.session_minutes,
101
+ )
102
+ documents: list[Document] = []
103
+ failed: list[str] = []
104
+ with client_for(self._creds, login, timeout=self.timeout) as client:
105
+ for url in urls:
106
+ try:
107
+ page = fetch(client, url)
108
+ except (GatewayError, httpx.HTTPError, ValueError) as e:
109
+ if self.raise_on_failure:
110
+ raise
111
+ logger.warning("Skipped %s: %s", url, e)
112
+ failed.append(url)
113
+ continue
114
+ documents.append(
115
+ Document(
116
+ content=page.html if self.output == "html" else page.text(),
117
+ meta={
118
+ "source": page.url,
119
+ "requested_url": url,
120
+ "status": page.status,
121
+ "title": page.title,
122
+ "content_type": page.content_type,
123
+ "country": targeting["country"],
124
+ },
125
+ )
126
+ )
127
+ return {"documents": documents, "failed_urls": failed}
128
+
129
+ def to_dict(self) -> dict[str, Any]:
130
+ return default_to_dict(
131
+ self,
132
+ api_key=self.api_key.to_dict(),
133
+ product=self.product,
134
+ session_minutes=self.session_minutes,
135
+ output=self.output,
136
+ timeout=self.timeout,
137
+ raise_on_failure=self.raise_on_failure,
138
+ **self.targeting,
139
+ )
140
+
141
+ @classmethod
142
+ def from_dict(cls, data: dict[str, Any]) -> ProxmintFetcher:
143
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
144
+ return default_from_dict(cls, data)
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.5
2
+ Name: proxmint-haystack
3
+ Version: 0.1.0
4
+ Summary: Haystack component that fetches web pages through Proxmint residential, datacenter and mobile proxies, with country, city and ASN targeting and sticky sessions.
5
+ Project-URL: Homepage, https://proxmint.com
6
+ Project-URL: Documentation, https://proxmint.com/docs/haystack
7
+ Project-URL: Repository, https://github.com/proxmint/proxmint-haystack
8
+ Project-URL: Issues, https://github.com/proxmint/proxmint-haystack/issues
9
+ Author-email: Proxmint <support@proxmint.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,fetcher,haystack,proxmint,proxy,residential-proxy,web-scraping
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Internet :: Proxy Servers
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: haystack-ai>=2.10
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Description-Content-Type: text/markdown
22
+
23
+ # proxmint-haystack
24
+
25
+ [Haystack](https://haystack.deepset.ai) integration for [Proxmint](https://proxmint.com) proxies: fetch web pages
26
+ from residential, datacenter or 4G/5G mobile IPs and get them back as `Document`s. Pick the exit country, state,
27
+ city, ZIP or ASN, and keep one IP for up to two hours when a site needs it.
28
+
29
+ - **`ProxmintFetcher`**: a component that takes a list of URLs and returns `documents` (readable page text, or the
30
+ raw HTML) and `failed_urls`. Use it as the first step of an indexing pipeline, or wrap it with `ComponentTool` to
31
+ give an agent a "read this page from Germany" tool.
32
+
33
+ Requests go straight from your machine to the Proxmint gateway (`gw.proxmint.com`). Nothing else sits in
34
+ between, and bandwidth comes out of your own Proxmint account.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install proxmint-haystack
40
+ ```
41
+
42
+ ## Credentials
43
+
44
+ Create an API key under **Settings** in the [Proxmint dashboard](https://proxmint.com/dashboard) and export it:
45
+
46
+ ```bash
47
+ export PROXMINT_API_KEY="pmk_..."
48
+ ```
49
+
50
+ The key reads the proxy login for the product you choose (`residential` by default). You can use the proxy login
51
+ instead, with `PROXMINT_USERNAME` and `PROXMINT_PASSWORD`.
52
+
53
+ ## Fetch pages
54
+
55
+ ```python
56
+ from haystack_integrations.components.fetchers.proxmint import ProxmintFetcher
57
+
58
+ fetcher = ProxmintFetcher(country="gb")
59
+ result = fetcher.run(urls=["https://example.com", "https://example.org"])
60
+
61
+ print(result["documents"][0].content)
62
+ print(result["documents"][0].meta) # source, requested_url, status, title, content_type, country
63
+ print(result["failed_urls"])
64
+ ```
65
+
66
+ ## Use it in a pipeline
67
+
68
+ ```python
69
+ from haystack import Pipeline
70
+ from haystack.components.preprocessors import DocumentSplitter
71
+ from haystack.components.writers import DocumentWriter
72
+ from haystack.document_stores.in_memory import InMemoryDocumentStore
73
+ from haystack_integrations.components.fetchers.proxmint import ProxmintFetcher
74
+
75
+ store = InMemoryDocumentStore()
76
+ pipe = Pipeline()
77
+ pipe.add_component("fetcher", ProxmintFetcher(country="us", city="New York", session_minutes=30))
78
+ pipe.add_component("splitter", DocumentSplitter(split_by="sentence", split_length=5))
79
+ pipe.add_component("writer", DocumentWriter(store))
80
+ pipe.connect("fetcher.documents", "splitter.documents")
81
+ pipe.connect("splitter.documents", "writer.documents")
82
+
83
+ pipe.run({"fetcher": {"urls": ["https://example.com/pricing", "https://example.org/pricing"]}})
84
+ ```
85
+
86
+ ## Use it as an agent tool
87
+
88
+ ```python
89
+ from haystack.tools import ComponentTool
90
+ from haystack_integrations.components.fetchers.proxmint import ProxmintFetcher
91
+
92
+ tool = ComponentTool(
93
+ component=ProxmintFetcher(),
94
+ name="fetch_pages",
95
+ description="Fetch web pages through a proxy in a chosen country and return their text.",
96
+ )
97
+ ```
98
+
99
+ Pass the tool to an `Agent` or a chat generator as usual. The model can set `urls` and an optional `country` per call.
100
+
101
+ ## Options
102
+
103
+ | Option | Default | What it does |
104
+ | --- | --- | --- |
105
+ | `api_key` | `PROXMINT_API_KEY` | A Haystack `Secret` |
106
+ | `product` | `"residential"` | `residential`, `premium-residential`, `datacenter` or `mobile` |
107
+ | `country` | any | Exit country (two-letter code). `run(urls, country=...)` overrides it for one run |
108
+ | `state`, `city`, `zip_code`, `asn` | none | Finer targeting; needs `country` |
109
+ | `session_minutes` | none | Keep one exit IP across runs for 1 to 120 minutes. Default: a new IP for every URL |
110
+ | `output` | `"text"` | `"text"` for readable text, `"html"` for the raw page |
111
+ | `timeout` | `60` | Seconds per request |
112
+ | `raise_on_failure` | `False` | Raise on a failed URL instead of listing it in `failed_urls` |
113
+
114
+ A gateway refusal (no bandwidth left, no IP matching the targeting, a blocked site) names what to change.
115
+
116
+ ## Good to know
117
+
118
+ - Web traffic works on destination ports 80, 443, 8080, 8443, 8888 and 8090 ([port access](https://proxmint.com/docs/ports)).
119
+ - A short list of sites, mostly banking and government, is refused by the network ([blocked websites](https://proxmint.com/docs/blocked-websites)).
120
+ - City, state, ZIP and ASN targeting meter at 2x on residential, datacenter and mobile ([targeting](https://proxmint.com/docs/targeting)).
121
+ - Each product allows up to 2,000 concurrent connections.
122
+
123
+ Full guide: [proxmint.com/docs/haystack](https://proxmint.com/docs/haystack). Pricing: [proxmint.com/pricing](https://proxmint.com/pricing).
124
+ Bandwidth is billed per GB and never expires.
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ uv run --group test pytest
130
+ ```
131
+
132
+ The tests run offline against a stand-in for the gateway.
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,7 @@
1
+ haystack_integrations/components/fetchers/proxmint/__init__.py,sha256=Fupm6pf66-5PPYGjg0GqqRRUtaULm1dU6FYGSaZF6AI,284
2
+ haystack_integrations/components/fetchers/proxmint/_gateway.py,sha256=l3HdW1glpWTG0OPPeyxOx3xOy5Ar4S1UkSWgi4BMfJ4,9163
3
+ haystack_integrations/components/fetchers/proxmint/fetcher.py,sha256=cKW8Yn9AOraXOJIoXEaJ17_AMyTc_DgUOjKSKp38_Qo,5923
4
+ proxmint_haystack-0.1.0.dist-info/METADATA,sha256=IleC4vTqn6GtKgru_yP0hC23rLrR3o_CEYPQm8_6SX4,5557
5
+ proxmint_haystack-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ proxmint_haystack-0.1.0.dist-info/licenses/LICENSE,sha256=dPrcCX38Tr8hwRb1TsW0mdf20fN71cLQ_g6Y5MObf5w,1065
7
+ proxmint_haystack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Proxmint
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.