profileout 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.

Potentially problematic release.


This version of profileout might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProfileOut
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,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: profileout
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the ProfileOut API
5
+ Author: ProfileOut
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://docs.profileout.com
8
+ Project-URL: Documentation, https://docs.profileout.com
9
+ Project-URL: Repository, https://github.com/ProfileOut/profileout-python
10
+ Project-URL: Issues, https://github.com/ProfileOut/profileout-python/issues
11
+ Keywords: profileout,sdk,people-data,resume-parsing,contact-enrichment
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # ProfileOut Python SDK
24
+
25
+ Official synchronous Python client for the [ProfileOut API](https://docs.profileout.com).
26
+
27
+ Requires Python 3.10 or newer. No runtime dependencies.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ pip install profileout
33
+ ```
34
+
35
+ The installation command above becomes available after the first PyPI publication. Until then, install a local checkout with `pip install /path/to/profileout-python`.
36
+
37
+ ## Quick start
38
+
39
+ Get an API key from the [ProfileOut console](https://console.profileout.com). The account must be active and email-verified, with sufficient credits.
40
+
41
+ ```python
42
+ import os
43
+ from profileout import ProfileOut
44
+
45
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"])
46
+ request = client.create_people_data({"linkedin_id": "example-person"})
47
+ response = (
48
+ request if request["status"] in ("completed", "failed")
49
+ else client.wait_for_people_data(request["request_id"])
50
+ )
51
+
52
+ if response["status"] == "failed":
53
+ print(response.get("error"))
54
+ else:
55
+ print(response["result"])
56
+ ```
57
+
58
+ Responses are dictionaries with the API's original snake_case fields. Nested profile fields can vary.
59
+
60
+ ## APIs
61
+
62
+ | Operation | Method |
63
+ | --- | --- |
64
+ | Submit / read a profile | `create_people_data(input)` / `get_people_data(request_id)` |
65
+ | Download profile image | `get_people_data_image(request_id)` → `bytes` |
66
+ | Upload / read a resume | `create_profile_parsing(path)` / `get_profile_parsing(request_id)` |
67
+ | Submit / read contact enrichment | `create_contact_enrichment(input)` / `get_contact_enrichment(request_id)` |
68
+ | Start / continue a search | `create_people_search({"message": ...})` / `continue_people_search(session_id, {"message": ...})` |
69
+ | Read a search | `get_people_search(session_id)` |
70
+ | Check service health | `get_health()` (does not send the API key) |
71
+
72
+ People Data and Contact Enrichment accept exactly one identity form:
73
+
74
+ ```python
75
+ {"linkedin_id": "example-person"}
76
+ {"profile_url": "https://www.linkedin.com/in/example-person"}
77
+ {"platform": "github", "account_id": "example-person"}
78
+ ```
79
+
80
+ Platforms: `linkedin`, `github`, `x`, `twitter`, `facebook`. Facebook numeric IDs must be strings.
81
+
82
+ ### Resume upload
83
+
84
+ ```python
85
+ from pathlib import Path
86
+
87
+ request = client.create_profile_parsing(Path("resume.pdf"))
88
+ response = client.wait_for_profile_parsing(request["request_id"])
89
+ print(response["status"], response["result"], response.get("error"))
90
+ ```
91
+
92
+ Accepts a filesystem path, reads at most 10 MiB plus one byte to check the limit, and sends exactly one multipart `file` field. Files must be nonempty PDF, PNG, JPEG, or TIFF with a matching extension; the service validates the file signature.
93
+
94
+ ### Search conversation
95
+
96
+ ```python
97
+ session = client.create_people_search({"message": "Find senior backend engineers in Seoul."})
98
+ response = client.wait_for_people_search(session["session_id"])
99
+ print(response["status"], response["follow_up"], response["role_summary"], response["filters"])
100
+
101
+ # After reviewing the response, optionally send a new paid revision:
102
+ revision = client.continue_people_search(session["session_id"], {"message": "Require Python experience."})
103
+ print(revision["revision"])
104
+ ```
105
+
106
+ Wait methods read the current request/session state; they do not pin a search revision. Avoid concurrently updating a session while waiting for a particular revision.
107
+
108
+ ## Polling, errors, and timeouts
109
+
110
+ `wait_for_people_data`, `wait_for_profile_parsing`, `wait_for_contact_enrichment`, and `wait_for_people_search` accept an ID. They wait at least five seconds before the first GET and between pending results, honor longer `next_poll_after_seconds` hints, and return on either `completed` or `failed`. Always inspect `status`; HTTP 200 can contain a failed job, and search outputs can contain their own errors.
111
+
112
+ ```python
113
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"], timeout=120)
114
+ response = client.wait_for_people_data(request_id, timeout=1800)
115
+ ```
116
+
117
+ Timeout values use **seconds**. The client default is a 120-second timeout for blocking network operations. Wait methods default to a 20-minute polling deadline and cap each request's socket timeout by the remaining time. Python's standard HTTP transport uses a per-operation socket timeout; a slowly streaming response can overrun the polling deadline until that read finishes. A wait deadline raises `TimeoutError`. Local timeouts or interruption do not cancel processing on the server. This client is synchronous; calls block the calling thread.
118
+
119
+ ```python
120
+ from profileout import ProfileOutError
121
+
122
+ try:
123
+ response = client.get_people_data(request_id)
124
+ print(response["status"])
125
+ except ProfileOutError as error:
126
+ print(error.status, error.code, error.request_id)
127
+ # error.body contains the response; error.headers includes retry-after if present.
128
+ ```
129
+
130
+ HTTP failures and invalid response objects raise `ProfileOutError`. Network errors use the standard library's `urllib.error.URLError`, `OSError`, or HTTP exceptions; invalid local inputs raise `ValueError` or `TypeError`.
131
+
132
+ **No requests are automatically retried.** Each accepted POST, including repeated submissions and search revisions, can reserve credits. After an ambiguous failure, recover a known request ID and use GET instead of blindly repeating POST. GET polling does not reserve additional credits. See [billing](https://docs.profileout.com/guides/billing).
133
+
134
+ The optional `base_url` defaults to `https://api.profileout.com`. HTTPS is required except on localhost; redirects are rejected so credentials and paid requests cannot be forwarded.
135
+
136
+ ## Publishing to PyPI (maintainers)
137
+
138
+ Distribution and import name: `profileout`. Initial version: `0.1.0`. This repository is ready to package, but no registry account or project is created by cloning it.
139
+
140
+ 1. [Create a PyPI account](https://pypi.org/account/register/), verify your email, and configure two-factor authentication.
141
+ 2. Check whether [the project name](https://pypi.org/project/profileout/) is available. If another owner takes it, change `project.name` in `pyproject.toml` and installation examples; the Python import can remain `profileout`.
142
+ 3. Create a PyPI API token with upload access. For a first upload, the project does not yet exist; use an account-scoped token, then replace it with a project-scoped token after publication. Store it outside the repository, and never paste it into chat.
143
+ 4. From this repository, prepare build tools in a local virtual environment:
144
+
145
+ ```sh
146
+ python3 -m venv .venv
147
+ source .venv/bin/activate
148
+ python -m pip install build twine
149
+ python -m build --outdir dist/0.1.0
150
+ python -m twine check dist/0.1.0/*
151
+ ```
152
+
153
+ The standard build command installs the declared setuptools backend in an isolated build environment. Runtime installation of the SDK has no third-party dependencies.
154
+
155
+ 5. Upload the verified wheel and source distribution:
156
+
157
+ ```sh
158
+ python -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/0.1.0/*
159
+ ```
160
+
161
+ When prompted for an API token, paste your token including its `pypi-` prefix. If your Twine version asks for a username and password, use `__token__` as the username and your API token as the password. Uploading creates the project on the first successful publication. Build tools may also obtain credentials through the system keyring.
162
+
163
+ Confirm the release at [PyPI](https://pypi.org/project/profileout/). For each later release, change the version in `pyproject.toml` and use a matching, new output directory; do not upload old artifacts. A published name/version cannot be reused. The files that are published become public.
164
+
165
+ See the [official Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/).
166
+
167
+ ## API compatibility
168
+
169
+ Targets the ProfileOut v1 public API. When that contract changes, review both SDKs against the service's OpenAPI specification and update methods and examples together.
170
+
171
+ ## License
172
+
173
+ [MIT](LICENSE). The license covers SDK code; API access and credits are provided separately by ProfileOut.
@@ -0,0 +1,151 @@
1
+ # ProfileOut Python SDK
2
+
3
+ Official synchronous Python client for the [ProfileOut API](https://docs.profileout.com).
4
+
5
+ Requires Python 3.10 or newer. No runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install profileout
11
+ ```
12
+
13
+ The installation command above becomes available after the first PyPI publication. Until then, install a local checkout with `pip install /path/to/profileout-python`.
14
+
15
+ ## Quick start
16
+
17
+ Get an API key from the [ProfileOut console](https://console.profileout.com). The account must be active and email-verified, with sufficient credits.
18
+
19
+ ```python
20
+ import os
21
+ from profileout import ProfileOut
22
+
23
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"])
24
+ request = client.create_people_data({"linkedin_id": "example-person"})
25
+ response = (
26
+ request if request["status"] in ("completed", "failed")
27
+ else client.wait_for_people_data(request["request_id"])
28
+ )
29
+
30
+ if response["status"] == "failed":
31
+ print(response.get("error"))
32
+ else:
33
+ print(response["result"])
34
+ ```
35
+
36
+ Responses are dictionaries with the API's original snake_case fields. Nested profile fields can vary.
37
+
38
+ ## APIs
39
+
40
+ | Operation | Method |
41
+ | --- | --- |
42
+ | Submit / read a profile | `create_people_data(input)` / `get_people_data(request_id)` |
43
+ | Download profile image | `get_people_data_image(request_id)` → `bytes` |
44
+ | Upload / read a resume | `create_profile_parsing(path)` / `get_profile_parsing(request_id)` |
45
+ | Submit / read contact enrichment | `create_contact_enrichment(input)` / `get_contact_enrichment(request_id)` |
46
+ | Start / continue a search | `create_people_search({"message": ...})` / `continue_people_search(session_id, {"message": ...})` |
47
+ | Read a search | `get_people_search(session_id)` |
48
+ | Check service health | `get_health()` (does not send the API key) |
49
+
50
+ People Data and Contact Enrichment accept exactly one identity form:
51
+
52
+ ```python
53
+ {"linkedin_id": "example-person"}
54
+ {"profile_url": "https://www.linkedin.com/in/example-person"}
55
+ {"platform": "github", "account_id": "example-person"}
56
+ ```
57
+
58
+ Platforms: `linkedin`, `github`, `x`, `twitter`, `facebook`. Facebook numeric IDs must be strings.
59
+
60
+ ### Resume upload
61
+
62
+ ```python
63
+ from pathlib import Path
64
+
65
+ request = client.create_profile_parsing(Path("resume.pdf"))
66
+ response = client.wait_for_profile_parsing(request["request_id"])
67
+ print(response["status"], response["result"], response.get("error"))
68
+ ```
69
+
70
+ Accepts a filesystem path, reads at most 10 MiB plus one byte to check the limit, and sends exactly one multipart `file` field. Files must be nonempty PDF, PNG, JPEG, or TIFF with a matching extension; the service validates the file signature.
71
+
72
+ ### Search conversation
73
+
74
+ ```python
75
+ session = client.create_people_search({"message": "Find senior backend engineers in Seoul."})
76
+ response = client.wait_for_people_search(session["session_id"])
77
+ print(response["status"], response["follow_up"], response["role_summary"], response["filters"])
78
+
79
+ # After reviewing the response, optionally send a new paid revision:
80
+ revision = client.continue_people_search(session["session_id"], {"message": "Require Python experience."})
81
+ print(revision["revision"])
82
+ ```
83
+
84
+ Wait methods read the current request/session state; they do not pin a search revision. Avoid concurrently updating a session while waiting for a particular revision.
85
+
86
+ ## Polling, errors, and timeouts
87
+
88
+ `wait_for_people_data`, `wait_for_profile_parsing`, `wait_for_contact_enrichment`, and `wait_for_people_search` accept an ID. They wait at least five seconds before the first GET and between pending results, honor longer `next_poll_after_seconds` hints, and return on either `completed` or `failed`. Always inspect `status`; HTTP 200 can contain a failed job, and search outputs can contain their own errors.
89
+
90
+ ```python
91
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"], timeout=120)
92
+ response = client.wait_for_people_data(request_id, timeout=1800)
93
+ ```
94
+
95
+ Timeout values use **seconds**. The client default is a 120-second timeout for blocking network operations. Wait methods default to a 20-minute polling deadline and cap each request's socket timeout by the remaining time. Python's standard HTTP transport uses a per-operation socket timeout; a slowly streaming response can overrun the polling deadline until that read finishes. A wait deadline raises `TimeoutError`. Local timeouts or interruption do not cancel processing on the server. This client is synchronous; calls block the calling thread.
96
+
97
+ ```python
98
+ from profileout import ProfileOutError
99
+
100
+ try:
101
+ response = client.get_people_data(request_id)
102
+ print(response["status"])
103
+ except ProfileOutError as error:
104
+ print(error.status, error.code, error.request_id)
105
+ # error.body contains the response; error.headers includes retry-after if present.
106
+ ```
107
+
108
+ HTTP failures and invalid response objects raise `ProfileOutError`. Network errors use the standard library's `urllib.error.URLError`, `OSError`, or HTTP exceptions; invalid local inputs raise `ValueError` or `TypeError`.
109
+
110
+ **No requests are automatically retried.** Each accepted POST, including repeated submissions and search revisions, can reserve credits. After an ambiguous failure, recover a known request ID and use GET instead of blindly repeating POST. GET polling does not reserve additional credits. See [billing](https://docs.profileout.com/guides/billing).
111
+
112
+ The optional `base_url` defaults to `https://api.profileout.com`. HTTPS is required except on localhost; redirects are rejected so credentials and paid requests cannot be forwarded.
113
+
114
+ ## Publishing to PyPI (maintainers)
115
+
116
+ Distribution and import name: `profileout`. Initial version: `0.1.0`. This repository is ready to package, but no registry account or project is created by cloning it.
117
+
118
+ 1. [Create a PyPI account](https://pypi.org/account/register/), verify your email, and configure two-factor authentication.
119
+ 2. Check whether [the project name](https://pypi.org/project/profileout/) is available. If another owner takes it, change `project.name` in `pyproject.toml` and installation examples; the Python import can remain `profileout`.
120
+ 3. Create a PyPI API token with upload access. For a first upload, the project does not yet exist; use an account-scoped token, then replace it with a project-scoped token after publication. Store it outside the repository, and never paste it into chat.
121
+ 4. From this repository, prepare build tools in a local virtual environment:
122
+
123
+ ```sh
124
+ python3 -m venv .venv
125
+ source .venv/bin/activate
126
+ python -m pip install build twine
127
+ python -m build --outdir dist/0.1.0
128
+ python -m twine check dist/0.1.0/*
129
+ ```
130
+
131
+ The standard build command installs the declared setuptools backend in an isolated build environment. Runtime installation of the SDK has no third-party dependencies.
132
+
133
+ 5. Upload the verified wheel and source distribution:
134
+
135
+ ```sh
136
+ python -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/0.1.0/*
137
+ ```
138
+
139
+ When prompted for an API token, paste your token including its `pypi-` prefix. If your Twine version asks for a username and password, use `__token__` as the username and your API token as the password. Uploading creates the project on the first successful publication. Build tools may also obtain credentials through the system keyring.
140
+
141
+ Confirm the release at [PyPI](https://pypi.org/project/profileout/). For each later release, change the version in `pyproject.toml` and use a matching, new output directory; do not upload old artifacts. A published name/version cannot be reused. The files that are published become public.
142
+
143
+ See the [official Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/).
144
+
145
+ ## API compatibility
146
+
147
+ Targets the ProfileOut v1 public API. When that contract changes, review both SDKs against the service's OpenAPI specification and update methods and examples together.
148
+
149
+ ## License
150
+
151
+ [MIT](LICENSE). The license covers SDK code; API access and credits are provided separately by ProfileOut.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "profileout"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the ProfileOut API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "ProfileOut"}]
14
+ keywords = ["profileout", "sdk", "people-data", "resume-parsing", "contact-enrichment"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://docs.profileout.com"
27
+ Documentation = "https://docs.profileout.com"
28
+ Repository = "https://github.com/ProfileOut/profileout-python"
29
+ Issues = "https://github.com/ProfileOut/profileout-python/issues"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.setuptools.package-data]
35
+ profileout = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,217 @@
1
+ """Official synchronous Python client for the ProfileOut API."""
2
+
3
+ import json
4
+ import math
5
+ import re
6
+ import time
7
+ import uuid
8
+ from os import PathLike
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.error import HTTPError
12
+ from urllib.parse import urlsplit
13
+ from urllib.request import HTTPRedirectHandler, Request, build_opener
14
+
15
+ __all__ = ["ProfileOut", "ProfileOutError"]
16
+
17
+
18
+ def _seconds(value: float, name: str) -> float:
19
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0:
20
+ raise ValueError(f"{name} must be a positive finite number.")
21
+ return value
22
+
23
+
24
+ def _id_path(resource: str, identifier: str) -> str:
25
+ if not isinstance(identifier, str) or not re.fullmatch(r"[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}", identifier, re.I | re.ASCII):
26
+ raise ValueError("A UUID request_id or session_id is required.")
27
+ return f"/v1/{resource}/{identifier}"
28
+
29
+
30
+ class _NoRedirect(HTTPRedirectHandler):
31
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
32
+ # Never forward API keys or replay a paid POST through a redirect.
33
+ return None
34
+
35
+
36
+ class ProfileOutError(Exception):
37
+ """HTTP/API protocol failure; processing failures remain in the response."""
38
+
39
+ def __init__(
40
+ self,
41
+ message: str,
42
+ *,
43
+ status: int | None = None,
44
+ code: str | None = None,
45
+ request_id: str | None = None,
46
+ body: Any = None,
47
+ headers: dict[str, str] | None = None,
48
+ ) -> None:
49
+ super().__init__(message)
50
+ self.status = status
51
+ self.code = code
52
+ self.request_id = request_id
53
+ self.body = body
54
+ self.headers = headers or {}
55
+
56
+
57
+ class ProfileOut:
58
+ def __init__(
59
+ self,
60
+ api_key: str,
61
+ *,
62
+ base_url: str = "https://api.profileout.com",
63
+ timeout: float = 120,
64
+ ) -> None:
65
+ if not isinstance(api_key, str) or not re.fullmatch(r"[\x21-\x7e]+", api_key):
66
+ raise ValueError("api_key must be a nonempty API key without whitespace.")
67
+ if not isinstance(base_url, str) or re.search(r"[\x00-\x20\x7f\\]", base_url):
68
+ raise ValueError("base_url must be a valid URL.")
69
+ url = urlsplit(base_url)
70
+ local = url.hostname in {"localhost", "127.0.0.1", "::1"}
71
+ if (
72
+ not url.hostname
73
+ or (url.scheme != "https" and not (url.scheme == "http" and local))
74
+ or url.username is not None or url.password is not None or url.query or url.fragment
75
+ ):
76
+ raise ValueError("base_url must use HTTPS (HTTP is allowed on localhost) without credentials, query, or fragment.")
77
+ self._api_key = api_key
78
+ self._base_url = base_url.rstrip("/")
79
+ self._timeout = _seconds(timeout, "timeout")
80
+ self._opener = build_opener(_NoRedirect())
81
+
82
+ def _request(
83
+ self,
84
+ method: str,
85
+ path: str,
86
+ data: dict[str, Any] | bytes | None = None,
87
+ *,
88
+ content_type: str | None = None,
89
+ timeout: float | None = None,
90
+ binary: bool = False,
91
+ ) -> Any:
92
+ headers = {"Accept": "image/*" if binary else "application/json"}
93
+ if path != "/health":
94
+ headers["Authorization"] = f"Bearer {self._api_key}"
95
+ if data is not None and not isinstance(data, bytes):
96
+ data = json.dumps(data, ensure_ascii=False, allow_nan=False).encode("utf-8")
97
+ content_type = "application/json"
98
+ if content_type:
99
+ headers["Content-Type"] = content_type
100
+ request = Request(self._base_url + path, data=data, headers=headers, method=method)
101
+ try:
102
+ response = self._opener.open(request, timeout=self._timeout if timeout is None else timeout)
103
+ except HTTPError as error:
104
+ response = error
105
+ with response:
106
+ status = response.status
107
+ response_headers = {key.lower(): value for key, value in response.headers.items()}
108
+ raw = response.read()
109
+ if 200 <= status < 300 and binary:
110
+ return raw
111
+ try:
112
+ body = json.loads(raw)
113
+ except (ValueError, UnicodeDecodeError):
114
+ body = raw.decode("utf-8", errors="replace")
115
+ details = dict(status=status, request_id=response_headers.get("x-request-id"), body=body, headers=response_headers)
116
+ if not 200 <= status < 300:
117
+ issue = body.get("error") if isinstance(body, dict) else None
118
+ issue = issue if isinstance(issue, dict) else {}
119
+ message = issue.get("message")
120
+ code = issue.get("code")
121
+ raise ProfileOutError(
122
+ message if isinstance(message, str) else f"ProfileOut returned HTTP {status}.",
123
+ code=code if isinstance(code, str) else None,
124
+ **details,
125
+ )
126
+ if not isinstance(body, dict):
127
+ raise ProfileOutError("ProfileOut returned an invalid JSON object.", code="invalid_response", **details)
128
+ return body
129
+
130
+ def _wait(self, path: str, timeout: float) -> dict[str, Any]:
131
+ deadline = time.monotonic() + _seconds(timeout, "timeout")
132
+ delay = 5.0
133
+ while True:
134
+ remaining = deadline - time.monotonic()
135
+ if remaining <= 0:
136
+ raise TimeoutError("Timed out waiting for ProfileOut; server-side processing may continue.")
137
+ time.sleep(min(delay, remaining))
138
+ remaining = deadline - time.monotonic()
139
+ if remaining <= 0:
140
+ raise TimeoutError("Timed out waiting for ProfileOut; server-side processing may continue.")
141
+ data = self._request("GET", path, timeout=min(self._timeout, remaining))
142
+ if time.monotonic() >= deadline:
143
+ raise TimeoutError("Timed out waiting for ProfileOut; server-side processing may continue.")
144
+ if data.get("status") in ("completed", "failed"):
145
+ return data
146
+ if data.get("status") not in ("queued", "pending", "processing"):
147
+ raise ProfileOutError("ProfileOut returned an unknown processing status.", code="invalid_response", body=data)
148
+ hint = data.get("next_poll_after_seconds")
149
+ delay = max(5.0, hint) if isinstance(hint, (int, float)) and math.isfinite(hint) and hint > 0 else 5.0
150
+
151
+ def create_people_data(self, input: dict[str, Any]) -> dict[str, Any]:
152
+ return self._request("POST", "/v1/people-data", input)
153
+
154
+ def get_people_data(self, request_id: str) -> dict[str, Any]:
155
+ return self._request("GET", _id_path("people-data", request_id))
156
+
157
+ def get_people_data_image(self, request_id: str) -> bytes:
158
+ return self._request("GET", _id_path("people-data", request_id) + "/image", binary=True)
159
+
160
+ def wait_for_people_data(self, request_id: str, *, timeout: float = 1200) -> dict[str, Any]:
161
+ return self._wait(_id_path("people-data", request_id), timeout)
162
+
163
+ def create_profile_parsing(self, file: str | PathLike[str]) -> dict[str, Any]:
164
+ path = Path(file)
165
+ filename = path.name
166
+ media_types = {
167
+ ".pdf": "application/pdf", ".png": "image/png",
168
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
169
+ ".tif": "image/tiff", ".tiff": "image/tiff",
170
+ }
171
+ if (
172
+ len(filename.encode("utf-16-le")) // 2 > 255
173
+ or re.search(r"[\x00-\x1f\x7f\\]", filename)
174
+ or path.suffix.lower() not in media_types
175
+ ):
176
+ raise ValueError("file must have a PDF, PNG, JPEG, or TIFF filename of at most 255 characters.")
177
+ with path.open("rb") as source:
178
+ content = source.read(10 * 1024 * 1024 + 1)
179
+ if not content or len(content) > 10 * 1024 * 1024:
180
+ raise ValueError("file must be nonempty and no larger than 10 MiB.")
181
+ boundary = uuid.uuid4().hex
182
+ filename = filename.replace('"', "%22")
183
+ body = (
184
+ f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\n'
185
+ f'Content-Type: {media_types[path.suffix.lower()]}\r\n\r\n'
186
+ ).encode("utf-8") + content + f"\r\n--{boundary}--\r\n".encode("ascii")
187
+ return self._request("POST", "/v1/profile-parsings", body, content_type=f"multipart/form-data; boundary={boundary}")
188
+
189
+ def get_profile_parsing(self, request_id: str) -> dict[str, Any]:
190
+ return self._request("GET", _id_path("profile-parsings", request_id))
191
+
192
+ def wait_for_profile_parsing(self, request_id: str, *, timeout: float = 1200) -> dict[str, Any]:
193
+ return self._wait(_id_path("profile-parsings", request_id), timeout)
194
+
195
+ def create_contact_enrichment(self, input: dict[str, Any]) -> dict[str, Any]:
196
+ return self._request("POST", "/v1/contact-enrichments", input)
197
+
198
+ def get_contact_enrichment(self, request_id: str) -> dict[str, Any]:
199
+ return self._request("GET", _id_path("contact-enrichments", request_id))
200
+
201
+ def wait_for_contact_enrichment(self, request_id: str, *, timeout: float = 1200) -> dict[str, Any]:
202
+ return self._wait(_id_path("contact-enrichments", request_id), timeout)
203
+
204
+ def create_people_search(self, input: dict[str, Any]) -> dict[str, Any]:
205
+ return self._request("POST", "/v1/people-searches", input)
206
+
207
+ def continue_people_search(self, session_id: str, input: dict[str, Any]) -> dict[str, Any]:
208
+ return self._request("POST", _id_path("people-searches", session_id), input)
209
+
210
+ def get_people_search(self, session_id: str) -> dict[str, Any]:
211
+ return self._request("GET", _id_path("people-searches", session_id))
212
+
213
+ def wait_for_people_search(self, session_id: str, *, timeout: float = 1200) -> dict[str, Any]:
214
+ return self._wait(_id_path("people-searches", session_id), timeout)
215
+
216
+ def get_health(self) -> dict[str, Any]:
217
+ return self._request("GET", "/health")
File without changes
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: profileout
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the ProfileOut API
5
+ Author: ProfileOut
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://docs.profileout.com
8
+ Project-URL: Documentation, https://docs.profileout.com
9
+ Project-URL: Repository, https://github.com/ProfileOut/profileout-python
10
+ Project-URL: Issues, https://github.com/ProfileOut/profileout-python/issues
11
+ Keywords: profileout,sdk,people-data,resume-parsing,contact-enrichment
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # ProfileOut Python SDK
24
+
25
+ Official synchronous Python client for the [ProfileOut API](https://docs.profileout.com).
26
+
27
+ Requires Python 3.10 or newer. No runtime dependencies.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ pip install profileout
33
+ ```
34
+
35
+ The installation command above becomes available after the first PyPI publication. Until then, install a local checkout with `pip install /path/to/profileout-python`.
36
+
37
+ ## Quick start
38
+
39
+ Get an API key from the [ProfileOut console](https://console.profileout.com). The account must be active and email-verified, with sufficient credits.
40
+
41
+ ```python
42
+ import os
43
+ from profileout import ProfileOut
44
+
45
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"])
46
+ request = client.create_people_data({"linkedin_id": "example-person"})
47
+ response = (
48
+ request if request["status"] in ("completed", "failed")
49
+ else client.wait_for_people_data(request["request_id"])
50
+ )
51
+
52
+ if response["status"] == "failed":
53
+ print(response.get("error"))
54
+ else:
55
+ print(response["result"])
56
+ ```
57
+
58
+ Responses are dictionaries with the API's original snake_case fields. Nested profile fields can vary.
59
+
60
+ ## APIs
61
+
62
+ | Operation | Method |
63
+ | --- | --- |
64
+ | Submit / read a profile | `create_people_data(input)` / `get_people_data(request_id)` |
65
+ | Download profile image | `get_people_data_image(request_id)` → `bytes` |
66
+ | Upload / read a resume | `create_profile_parsing(path)` / `get_profile_parsing(request_id)` |
67
+ | Submit / read contact enrichment | `create_contact_enrichment(input)` / `get_contact_enrichment(request_id)` |
68
+ | Start / continue a search | `create_people_search({"message": ...})` / `continue_people_search(session_id, {"message": ...})` |
69
+ | Read a search | `get_people_search(session_id)` |
70
+ | Check service health | `get_health()` (does not send the API key) |
71
+
72
+ People Data and Contact Enrichment accept exactly one identity form:
73
+
74
+ ```python
75
+ {"linkedin_id": "example-person"}
76
+ {"profile_url": "https://www.linkedin.com/in/example-person"}
77
+ {"platform": "github", "account_id": "example-person"}
78
+ ```
79
+
80
+ Platforms: `linkedin`, `github`, `x`, `twitter`, `facebook`. Facebook numeric IDs must be strings.
81
+
82
+ ### Resume upload
83
+
84
+ ```python
85
+ from pathlib import Path
86
+
87
+ request = client.create_profile_parsing(Path("resume.pdf"))
88
+ response = client.wait_for_profile_parsing(request["request_id"])
89
+ print(response["status"], response["result"], response.get("error"))
90
+ ```
91
+
92
+ Accepts a filesystem path, reads at most 10 MiB plus one byte to check the limit, and sends exactly one multipart `file` field. Files must be nonempty PDF, PNG, JPEG, or TIFF with a matching extension; the service validates the file signature.
93
+
94
+ ### Search conversation
95
+
96
+ ```python
97
+ session = client.create_people_search({"message": "Find senior backend engineers in Seoul."})
98
+ response = client.wait_for_people_search(session["session_id"])
99
+ print(response["status"], response["follow_up"], response["role_summary"], response["filters"])
100
+
101
+ # After reviewing the response, optionally send a new paid revision:
102
+ revision = client.continue_people_search(session["session_id"], {"message": "Require Python experience."})
103
+ print(revision["revision"])
104
+ ```
105
+
106
+ Wait methods read the current request/session state; they do not pin a search revision. Avoid concurrently updating a session while waiting for a particular revision.
107
+
108
+ ## Polling, errors, and timeouts
109
+
110
+ `wait_for_people_data`, `wait_for_profile_parsing`, `wait_for_contact_enrichment`, and `wait_for_people_search` accept an ID. They wait at least five seconds before the first GET and between pending results, honor longer `next_poll_after_seconds` hints, and return on either `completed` or `failed`. Always inspect `status`; HTTP 200 can contain a failed job, and search outputs can contain their own errors.
111
+
112
+ ```python
113
+ client = ProfileOut(api_key=os.environ["PROFILEOUT_API_KEY"], timeout=120)
114
+ response = client.wait_for_people_data(request_id, timeout=1800)
115
+ ```
116
+
117
+ Timeout values use **seconds**. The client default is a 120-second timeout for blocking network operations. Wait methods default to a 20-minute polling deadline and cap each request's socket timeout by the remaining time. Python's standard HTTP transport uses a per-operation socket timeout; a slowly streaming response can overrun the polling deadline until that read finishes. A wait deadline raises `TimeoutError`. Local timeouts or interruption do not cancel processing on the server. This client is synchronous; calls block the calling thread.
118
+
119
+ ```python
120
+ from profileout import ProfileOutError
121
+
122
+ try:
123
+ response = client.get_people_data(request_id)
124
+ print(response["status"])
125
+ except ProfileOutError as error:
126
+ print(error.status, error.code, error.request_id)
127
+ # error.body contains the response; error.headers includes retry-after if present.
128
+ ```
129
+
130
+ HTTP failures and invalid response objects raise `ProfileOutError`. Network errors use the standard library's `urllib.error.URLError`, `OSError`, or HTTP exceptions; invalid local inputs raise `ValueError` or `TypeError`.
131
+
132
+ **No requests are automatically retried.** Each accepted POST, including repeated submissions and search revisions, can reserve credits. After an ambiguous failure, recover a known request ID and use GET instead of blindly repeating POST. GET polling does not reserve additional credits. See [billing](https://docs.profileout.com/guides/billing).
133
+
134
+ The optional `base_url` defaults to `https://api.profileout.com`. HTTPS is required except on localhost; redirects are rejected so credentials and paid requests cannot be forwarded.
135
+
136
+ ## Publishing to PyPI (maintainers)
137
+
138
+ Distribution and import name: `profileout`. Initial version: `0.1.0`. This repository is ready to package, but no registry account or project is created by cloning it.
139
+
140
+ 1. [Create a PyPI account](https://pypi.org/account/register/), verify your email, and configure two-factor authentication.
141
+ 2. Check whether [the project name](https://pypi.org/project/profileout/) is available. If another owner takes it, change `project.name` in `pyproject.toml` and installation examples; the Python import can remain `profileout`.
142
+ 3. Create a PyPI API token with upload access. For a first upload, the project does not yet exist; use an account-scoped token, then replace it with a project-scoped token after publication. Store it outside the repository, and never paste it into chat.
143
+ 4. From this repository, prepare build tools in a local virtual environment:
144
+
145
+ ```sh
146
+ python3 -m venv .venv
147
+ source .venv/bin/activate
148
+ python -m pip install build twine
149
+ python -m build --outdir dist/0.1.0
150
+ python -m twine check dist/0.1.0/*
151
+ ```
152
+
153
+ The standard build command installs the declared setuptools backend in an isolated build environment. Runtime installation of the SDK has no third-party dependencies.
154
+
155
+ 5. Upload the verified wheel and source distribution:
156
+
157
+ ```sh
158
+ python -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/0.1.0/*
159
+ ```
160
+
161
+ When prompted for an API token, paste your token including its `pypi-` prefix. If your Twine version asks for a username and password, use `__token__` as the username and your API token as the password. Uploading creates the project on the first successful publication. Build tools may also obtain credentials through the system keyring.
162
+
163
+ Confirm the release at [PyPI](https://pypi.org/project/profileout/). For each later release, change the version in `pyproject.toml` and use a matching, new output directory; do not upload old artifacts. A published name/version cannot be reused. The files that are published become public.
164
+
165
+ See the [official Python packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/).
166
+
167
+ ## API compatibility
168
+
169
+ Targets the ProfileOut v1 public API. When that contract changes, review both SDKs against the service's OpenAPI specification and update methods and examples together.
170
+
171
+ ## License
172
+
173
+ [MIT](LICENSE). The license covers SDK code; API access and credits are provided separately by ProfileOut.
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/profileout/__init__.py
5
+ src/profileout/py.typed
6
+ src/profileout.egg-info/PKG-INFO
7
+ src/profileout.egg-info/SOURCES.txt
8
+ src/profileout.egg-info/dependency_links.txt
9
+ src/profileout.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ profileout