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.
- profileout-0.1.0/LICENSE +21 -0
- profileout-0.1.0/PKG-INFO +173 -0
- profileout-0.1.0/README.md +151 -0
- profileout-0.1.0/pyproject.toml +35 -0
- profileout-0.1.0/setup.cfg +4 -0
- profileout-0.1.0/src/profileout/__init__.py +217 -0
- profileout-0.1.0/src/profileout/py.typed +0 -0
- profileout-0.1.0/src/profileout.egg-info/PKG-INFO +173 -0
- profileout-0.1.0/src/profileout.egg-info/SOURCES.txt +9 -0
- profileout-0.1.0/src/profileout.egg-info/dependency_links.txt +1 -0
- profileout-0.1.0/src/profileout.egg-info/top_level.txt +1 -0
profileout-0.1.0/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
profileout
|