sapsf-shared 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.
- sapsf_shared-0.1.0/.github/workflows/ci.yml +27 -0
- sapsf_shared-0.1.0/.github/workflows/publish.yml +44 -0
- sapsf_shared-0.1.0/.gitignore +12 -0
- sapsf_shared-0.1.0/LICENSE +21 -0
- sapsf_shared-0.1.0/PKG-INFO +224 -0
- sapsf_shared-0.1.0/README.md +192 -0
- sapsf_shared-0.1.0/docs/findings-schema.md +92 -0
- sapsf_shared-0.1.0/favicon.svg +19 -0
- sapsf_shared-0.1.0/pyproject.toml +68 -0
- sapsf_shared-0.1.0/src/sapsf_shared/__init__.py +45 -0
- sapsf_shared-0.1.0/src/sapsf_shared/auth.py +357 -0
- sapsf_shared-0.1.0/src/sapsf_shared/client.py +344 -0
- sapsf_shared-0.1.0/src/sapsf_shared/config.py +153 -0
- sapsf_shared-0.1.0/src/sapsf_shared/exceptions.py +37 -0
- sapsf_shared-0.1.0/src/sapsf_shared/flask_base.py +186 -0
- sapsf_shared-0.1.0/src/sapsf_shared/logging_config.py +98 -0
- sapsf_shared-0.1.0/src/sapsf_shared/utils.py +143 -0
- sapsf_shared-0.1.0/tests/__init__.py +0 -0
- sapsf_shared-0.1.0/tests/test_auth.py +349 -0
- sapsf_shared-0.1.0/tests/test_client.py +272 -0
- sapsf_shared-0.1.0/tests/test_config.py +110 -0
- sapsf_shared-0.1.0/tests/test_exceptions.py +53 -0
- sapsf_shared-0.1.0/tests/test_flask_base.py +104 -0
- sapsf_shared-0.1.0/tests/test_utils.py +135 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.11", "3.12"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install
|
|
25
|
+
run: pip install -e ".[dev,flask]"
|
|
26
|
+
- name: Test
|
|
27
|
+
run: pytest -q
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- name: Run tests
|
|
20
|
+
run: |
|
|
21
|
+
pip install -e ".[dev,flask]"
|
|
22
|
+
pytest -q
|
|
23
|
+
- name: Build distributions
|
|
24
|
+
run: |
|
|
25
|
+
pip install build
|
|
26
|
+
python -m build
|
|
27
|
+
- uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write # required for PyPI trusted publishing
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- name: Publish to PyPI
|
|
44
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sahir Vhora
|
|
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,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sapsf-shared
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared Python SDK for SAP SuccessFactors tools - OData client, auth, config, and Flask base
|
|
5
|
+
Project-URL: Homepage, https://github.com/SahirVhora/sapsf-shared
|
|
6
|
+
Project-URL: Repository, https://github.com/SahirVhora/sapsf-shared
|
|
7
|
+
Project-URL: Issues, https://github.com/SahirVhora/sapsf-shared/issues
|
|
8
|
+
Project-URL: SF Compass Suite, https://sahirvhora.github.io/sf-compass/
|
|
9
|
+
Author: Sahir Vhora
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: odata,sap,sdk,successfactors
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: requests>=2.31
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
29
|
+
Provides-Extra: flask
|
|
30
|
+
Requires-Dist: flask>=2.3; extra == 'flask'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# sapsf-shared
|
|
34
|
+
|
|
35
|
+
**Shared Python SDK for SAP SuccessFactors tools.**
|
|
36
|
+
|
|
37
|
+
A single, well-tested library that extracts the common patterns repeated across every SAP SF tool in your workspace: OData HTTP client, authentication, config loading, logging, utilities, and Flask boilerplate.
|
|
38
|
+
|
|
39
|
+
## Why this exists
|
|
40
|
+
|
|
41
|
+
Every SAP SF tool in your workspace reimplements:
|
|
42
|
+
- OData v2 HTTP client with retries and pagination
|
|
43
|
+
- Basic Auth / OAuth2 / Certificate auth handling
|
|
44
|
+
- Keyring vs file-based credential storage
|
|
45
|
+
- Config loader (YAML/JSON with env var substitution)
|
|
46
|
+
- Coloured logging setup
|
|
47
|
+
- Flask CSRF, error handlers, health endpoint
|
|
48
|
+
|
|
49
|
+
`sapsf-shared` consolidates all of this into one package. When you fix a bug in the auth layer, it's fixed everywhere.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd sapsf/_shared
|
|
55
|
+
pip install -e ".[dev,flask]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### 1. Connect to SAP SuccessFactors
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from sapsf_shared import AuthConfig, SFClient
|
|
64
|
+
|
|
65
|
+
config = AuthConfig(
|
|
66
|
+
base_url="https://api4.successfactors.com/odata/v2",
|
|
67
|
+
username="admin@companyId",
|
|
68
|
+
password="secret",
|
|
69
|
+
company_id="companyId",
|
|
70
|
+
auth_type="basic",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with SFClient(config) as client:
|
|
74
|
+
# Fetch all departments
|
|
75
|
+
depts = client.get("FODepartment")
|
|
76
|
+
print(f"Found {len(depts)} departments")
|
|
77
|
+
|
|
78
|
+
# Fetch with filter and pagination
|
|
79
|
+
positions = client.get(
|
|
80
|
+
"Position",
|
|
81
|
+
filter_expr="cust_Country eq 'GBR'",
|
|
82
|
+
select=["code", "externalName", "cust_JobFunction"],
|
|
83
|
+
expand=["cust_JobFunction"],
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. OAuth 2.0
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
config = AuthConfig(
|
|
91
|
+
base_url="https://api4.successfactors.com/odata/v2",
|
|
92
|
+
auth_type="oauth2",
|
|
93
|
+
client_id="my_client_id",
|
|
94
|
+
client_secret="my_secret",
|
|
95
|
+
company_id="companyId",
|
|
96
|
+
)
|
|
97
|
+
with SFClient(config) as client:
|
|
98
|
+
ok, msg = client.test_connection()
|
|
99
|
+
print(ok, msg)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Secure credential storage
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from sapsf_shared.auth import CredentialStore
|
|
106
|
+
|
|
107
|
+
store = CredentialStore(service="my_tool")
|
|
108
|
+
store.set("prd:password", "secret123")
|
|
109
|
+
pwd = store.get("prd:password")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Automatically uses OS keyring when available; falls back to a chmod-600 JSON file on headless systems.
|
|
113
|
+
|
|
114
|
+
### 4. Config from YAML
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from sapsf_shared.config import load_config
|
|
118
|
+
|
|
119
|
+
cfg = load_config("config.yaml")
|
|
120
|
+
# Supports ${ENV_VAR} substitution inside the YAML file
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 5. Flask base app
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from sapsf_shared.flask_base import create_app
|
|
127
|
+
|
|
128
|
+
app = create_app(__name__, log_dir="logs", enable_csrf=True)
|
|
129
|
+
|
|
130
|
+
@app.route("/")
|
|
131
|
+
def index():
|
|
132
|
+
return {"status": "ok"}
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
app.run(port=5050)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Comes with built-in:
|
|
139
|
+
- `/api/health` endpoint
|
|
140
|
+
- CSRF token generation + validation
|
|
141
|
+
- JSON error handlers (400, 403, 404, 500)
|
|
142
|
+
- CORS preflight support
|
|
143
|
+
- Rotating file logging
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### `SFClient`
|
|
148
|
+
|
|
149
|
+
| Method | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `get(entity_set, **kwargs)` | Fetch all records with auto-pagination |
|
|
152
|
+
| `get_entity_by_code(entity_set, external_code, **kwargs)` | Filter by externalCode |
|
|
153
|
+
| `post(entity_set, payload)` | Create a record |
|
|
154
|
+
| `patch(entity_set, payload)` | Update a record |
|
|
155
|
+
| `delete(entity_set, key)` | Delete a record |
|
|
156
|
+
| `test_connection()` | Quick connectivity probe |
|
|
157
|
+
| `entity_exists(entity_set, external_code)` | Check existence |
|
|
158
|
+
|
|
159
|
+
### `AuthConfig`
|
|
160
|
+
|
|
161
|
+
Dataclass that normalises auth settings across all your tools. Fields: `base_url`, `company_id`, `auth_type`, `username`, `password`, `client_id`, `client_secret`, `token_url`, `cert_path`, `key_path`, `timeout_sec`.
|
|
162
|
+
|
|
163
|
+
### `SFEnvConfig`
|
|
164
|
+
|
|
165
|
+
Loads configuration from environment variables with the standard `SF_*` prefix:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
export SF_BASE_URL=https://api4.successfactors.com/odata/v2
|
|
169
|
+
export SF_USERNAME=admin
|
|
170
|
+
export SF_PASSWORD=secret
|
|
171
|
+
export SF_COMPANY_ID=companyId
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
cfg = SFEnvConfig.from_env()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `CredentialStore`
|
|
179
|
+
|
|
180
|
+
Keyring-backed secret storage with automatic fallback to a local `.secrets.json` file (chmod 600). Use `store.clear_alias(alias)` to delete all secrets for a tenant.
|
|
181
|
+
|
|
182
|
+
### Utilities
|
|
183
|
+
|
|
184
|
+
| Function | Description |
|
|
185
|
+
|----------|-------------|
|
|
186
|
+
| `parse_sf_date(raw)` | Parse `/Date(millis)/` and ISO formats |
|
|
187
|
+
| `is_active_today(record)` | Check effective dating + status |
|
|
188
|
+
| `flatten_record(record)` | Flatten nested OData for CSV export |
|
|
189
|
+
| `build_odata_filter(dict)` | Build `$filter` strings from dicts |
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pip install -e ".[dev,flask]"
|
|
195
|
+
pytest -v # 13 tests
|
|
196
|
+
mypy src/sapsf_shared # Type checking
|
|
197
|
+
ruff check src tests # Linting
|
|
198
|
+
ruff format src tests # Formatting
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Roadmap
|
|
202
|
+
|
|
203
|
+
- [ ] Vectorised batch operations (`upsert_many`, `delete_many`)
|
|
204
|
+
- [ ] Connection pooling tuning
|
|
205
|
+
- [ ] Async support (httpx-based client)
|
|
206
|
+
- [ ] SAP SF API v4 support
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
|
211
|
+
|
|
212
|
+
## Adoption status
|
|
213
|
+
|
|
214
|
+
| Tool | Status |
|
|
215
|
+
|---|---|
|
|
216
|
+
| sf-config-compare | Adopted - `parse_sf_date` via `sapsf_shared.utils` |
|
|
217
|
+
| sf-position-integrity-checker | Next - client/pagination migration pending tenant testing |
|
|
218
|
+
| sf-object-sync | Planned |
|
|
219
|
+
|
|
220
|
+
Depend on it from any tool:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
sapsf-shared @ git+https://github.com/SahirVhora/sapsf-shared
|
|
224
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# sapsf-shared
|
|
2
|
+
|
|
3
|
+
**Shared Python SDK for SAP SuccessFactors tools.**
|
|
4
|
+
|
|
5
|
+
A single, well-tested library that extracts the common patterns repeated across every SAP SF tool in your workspace: OData HTTP client, authentication, config loading, logging, utilities, and Flask boilerplate.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Every SAP SF tool in your workspace reimplements:
|
|
10
|
+
- OData v2 HTTP client with retries and pagination
|
|
11
|
+
- Basic Auth / OAuth2 / Certificate auth handling
|
|
12
|
+
- Keyring vs file-based credential storage
|
|
13
|
+
- Config loader (YAML/JSON with env var substitution)
|
|
14
|
+
- Coloured logging setup
|
|
15
|
+
- Flask CSRF, error handlers, health endpoint
|
|
16
|
+
|
|
17
|
+
`sapsf-shared` consolidates all of this into one package. When you fix a bug in the auth layer, it's fixed everywhere.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd sapsf/_shared
|
|
23
|
+
pip install -e ".[dev,flask]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Connect to SAP SuccessFactors
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from sapsf_shared import AuthConfig, SFClient
|
|
32
|
+
|
|
33
|
+
config = AuthConfig(
|
|
34
|
+
base_url="https://api4.successfactors.com/odata/v2",
|
|
35
|
+
username="admin@companyId",
|
|
36
|
+
password="secret",
|
|
37
|
+
company_id="companyId",
|
|
38
|
+
auth_type="basic",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
with SFClient(config) as client:
|
|
42
|
+
# Fetch all departments
|
|
43
|
+
depts = client.get("FODepartment")
|
|
44
|
+
print(f"Found {len(depts)} departments")
|
|
45
|
+
|
|
46
|
+
# Fetch with filter and pagination
|
|
47
|
+
positions = client.get(
|
|
48
|
+
"Position",
|
|
49
|
+
filter_expr="cust_Country eq 'GBR'",
|
|
50
|
+
select=["code", "externalName", "cust_JobFunction"],
|
|
51
|
+
expand=["cust_JobFunction"],
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. OAuth 2.0
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
config = AuthConfig(
|
|
59
|
+
base_url="https://api4.successfactors.com/odata/v2",
|
|
60
|
+
auth_type="oauth2",
|
|
61
|
+
client_id="my_client_id",
|
|
62
|
+
client_secret="my_secret",
|
|
63
|
+
company_id="companyId",
|
|
64
|
+
)
|
|
65
|
+
with SFClient(config) as client:
|
|
66
|
+
ok, msg = client.test_connection()
|
|
67
|
+
print(ok, msg)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Secure credential storage
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from sapsf_shared.auth import CredentialStore
|
|
74
|
+
|
|
75
|
+
store = CredentialStore(service="my_tool")
|
|
76
|
+
store.set("prd:password", "secret123")
|
|
77
|
+
pwd = store.get("prd:password")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Automatically uses OS keyring when available; falls back to a chmod-600 JSON file on headless systems.
|
|
81
|
+
|
|
82
|
+
### 4. Config from YAML
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from sapsf_shared.config import load_config
|
|
86
|
+
|
|
87
|
+
cfg = load_config("config.yaml")
|
|
88
|
+
# Supports ${ENV_VAR} substitution inside the YAML file
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 5. Flask base app
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from sapsf_shared.flask_base import create_app
|
|
95
|
+
|
|
96
|
+
app = create_app(__name__, log_dir="logs", enable_csrf=True)
|
|
97
|
+
|
|
98
|
+
@app.route("/")
|
|
99
|
+
def index():
|
|
100
|
+
return {"status": "ok"}
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
app.run(port=5050)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Comes with built-in:
|
|
107
|
+
- `/api/health` endpoint
|
|
108
|
+
- CSRF token generation + validation
|
|
109
|
+
- JSON error handlers (400, 403, 404, 500)
|
|
110
|
+
- CORS preflight support
|
|
111
|
+
- Rotating file logging
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### `SFClient`
|
|
116
|
+
|
|
117
|
+
| Method | Description |
|
|
118
|
+
|--------|-------------|
|
|
119
|
+
| `get(entity_set, **kwargs)` | Fetch all records with auto-pagination |
|
|
120
|
+
| `get_entity_by_code(entity_set, external_code, **kwargs)` | Filter by externalCode |
|
|
121
|
+
| `post(entity_set, payload)` | Create a record |
|
|
122
|
+
| `patch(entity_set, payload)` | Update a record |
|
|
123
|
+
| `delete(entity_set, key)` | Delete a record |
|
|
124
|
+
| `test_connection()` | Quick connectivity probe |
|
|
125
|
+
| `entity_exists(entity_set, external_code)` | Check existence |
|
|
126
|
+
|
|
127
|
+
### `AuthConfig`
|
|
128
|
+
|
|
129
|
+
Dataclass that normalises auth settings across all your tools. Fields: `base_url`, `company_id`, `auth_type`, `username`, `password`, `client_id`, `client_secret`, `token_url`, `cert_path`, `key_path`, `timeout_sec`.
|
|
130
|
+
|
|
131
|
+
### `SFEnvConfig`
|
|
132
|
+
|
|
133
|
+
Loads configuration from environment variables with the standard `SF_*` prefix:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export SF_BASE_URL=https://api4.successfactors.com/odata/v2
|
|
137
|
+
export SF_USERNAME=admin
|
|
138
|
+
export SF_PASSWORD=secret
|
|
139
|
+
export SF_COMPANY_ID=companyId
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
cfg = SFEnvConfig.from_env()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `CredentialStore`
|
|
147
|
+
|
|
148
|
+
Keyring-backed secret storage with automatic fallback to a local `.secrets.json` file (chmod 600). Use `store.clear_alias(alias)` to delete all secrets for a tenant.
|
|
149
|
+
|
|
150
|
+
### Utilities
|
|
151
|
+
|
|
152
|
+
| Function | Description |
|
|
153
|
+
|----------|-------------|
|
|
154
|
+
| `parse_sf_date(raw)` | Parse `/Date(millis)/` and ISO formats |
|
|
155
|
+
| `is_active_today(record)` | Check effective dating + status |
|
|
156
|
+
| `flatten_record(record)` | Flatten nested OData for CSV export |
|
|
157
|
+
| `build_odata_filter(dict)` | Build `$filter` strings from dicts |
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install -e ".[dev,flask]"
|
|
163
|
+
pytest -v # 13 tests
|
|
164
|
+
mypy src/sapsf_shared # Type checking
|
|
165
|
+
ruff check src tests # Linting
|
|
166
|
+
ruff format src tests # Formatting
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Roadmap
|
|
170
|
+
|
|
171
|
+
- [ ] Vectorised batch operations (`upsert_many`, `delete_many`)
|
|
172
|
+
- [ ] Connection pooling tuning
|
|
173
|
+
- [ ] Async support (httpx-based client)
|
|
174
|
+
- [ ] SAP SF API v4 support
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
179
|
+
|
|
180
|
+
## Adoption status
|
|
181
|
+
|
|
182
|
+
| Tool | Status |
|
|
183
|
+
|---|---|
|
|
184
|
+
| sf-config-compare | Adopted - `parse_sf_date` via `sapsf_shared.utils` |
|
|
185
|
+
| sf-position-integrity-checker | Next - client/pagination migration pending tenant testing |
|
|
186
|
+
| sf-object-sync | Planned |
|
|
187
|
+
|
|
188
|
+
Depend on it from any tool:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
sapsf-shared @ git+https://github.com/SahirVhora/sapsf-shared
|
|
192
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# SF Compass Suite - Findings Schema (v1)
|
|
2
|
+
|
|
3
|
+
A common JSON format for analysis findings, so every scanner in the suite
|
|
4
|
+
(sf-config-debt-radar, sf-position-integrity-checker, sf-config-compare, ...)
|
|
5
|
+
can emit results that any other tool (the sf-compass dashboard, AI agents via
|
|
6
|
+
MCP, future trend tooling) can consume without tool-specific parsing.
|
|
7
|
+
|
|
8
|
+
Schema identifier: `sf-compass-findings/v1`
|
|
9
|
+
|
|
10
|
+
## Top-level document
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"schema": "sf-compass-findings/v1",
|
|
15
|
+
"tool": "sf-position-integrity-checker",
|
|
16
|
+
"tool_version": "1.4.0",
|
|
17
|
+
"generated_at": "2026-06-11T14:32:00",
|
|
18
|
+
"tenant": "https://***masked***.successfactors.eu",
|
|
19
|
+
"scope": {
|
|
20
|
+
"country": "CAN",
|
|
21
|
+
"as_of_date": "2026-06-11"
|
|
22
|
+
},
|
|
23
|
+
"summary": {
|
|
24
|
+
"total_records": 1240,
|
|
25
|
+
"findings": 37,
|
|
26
|
+
"by_severity": { "high": 4, "medium": 21, "low": 12 }
|
|
27
|
+
},
|
|
28
|
+
"findings": []
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Field | Required | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `schema` | yes | Always `sf-compass-findings/v1` |
|
|
35
|
+
| `tool` | yes | Repo name of the emitting tool |
|
|
36
|
+
| `tool_version` | yes | Version string of the emitting tool |
|
|
37
|
+
| `generated_at` | yes | ISO 8601 local timestamp |
|
|
38
|
+
| `tenant` | no | Masked tenant URL only - never the raw subdomain |
|
|
39
|
+
| `scope` | no | Tool-specific run parameters (country, module, object types) |
|
|
40
|
+
| `summary` | yes | Counts; `by_severity` keys are lowercase severities |
|
|
41
|
+
| `findings` | yes | Array of finding objects, may be empty |
|
|
42
|
+
|
|
43
|
+
## Finding object
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"id": "POS-014",
|
|
48
|
+
"severity": "high",
|
|
49
|
+
"category": "Org Assignment",
|
|
50
|
+
"object_type": "Position",
|
|
51
|
+
"object_id": "POS_10023",
|
|
52
|
+
"field": "costCenter",
|
|
53
|
+
"message": "Cost centre is inactive as of the as-of date",
|
|
54
|
+
"details": {}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Field | Required | Notes |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `id` | yes | Stable rule/check identifier within the tool |
|
|
61
|
+
| `severity` | yes | One of `critical`, `high`, `medium`, `low`, `info` (lowercase) |
|
|
62
|
+
| `category` | yes | Tool-defined grouping label |
|
|
63
|
+
| `object_type` | yes | e.g. `Position`, `Picklist`, `ObjectDefinition`, `BusinessRule` |
|
|
64
|
+
| `object_id` | yes | External code / ID of the affected record |
|
|
65
|
+
| `field` | no | Affected field name, if applicable |
|
|
66
|
+
| `message` | yes | Plain-English description of the finding |
|
|
67
|
+
| `details` | no | Free-form object for tool-specific extras |
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
- Severities are normalised to lowercase. Map tool-native scales onto
|
|
72
|
+
critical/high/medium/low/info before emitting.
|
|
73
|
+
- Never include employee personal data in `message` or `details`. IDs and
|
|
74
|
+
codes only.
|
|
75
|
+
- Always mask tenant URLs (`***masked***` subdomain convention, see
|
|
76
|
+
`sapsf_shared.utils`).
|
|
77
|
+
- Emitters write the file alongside their other reports as
|
|
78
|
+
`<tool>_findings_<datestamp>.json`.
|
|
79
|
+
|
|
80
|
+
## Current emitters
|
|
81
|
+
|
|
82
|
+
- `sf-position-integrity-checker` - `write_findings_json` in `reporters.py`
|
|
83
|
+
(also exposed via its MCP server tool `sf_validate_positions`)
|
|
84
|
+
- `sf-config-debt-radar` - `build_findings_v1` in `cli.py`, written as
|
|
85
|
+
`config_debt_findings.json` on every scan
|
|
86
|
+
|
|
87
|
+
## Consumers
|
|
88
|
+
|
|
89
|
+
- `sf-compass` - [Tenant Findings Viewer](https://sahirvhora.github.io/sf-compass/findings.html)
|
|
90
|
+
loads any number of v1 files into one combined, filterable view
|
|
91
|
+
|
|
92
|
+
Planned emitter: sf-config-compare.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SF Tools">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#141820"/>
|
|
5
|
+
<stop offset="100%" stop-color="#0a0d14"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="gold" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
+
<stop offset="0%" stop-color="#f0c850"/>
|
|
9
|
+
<stop offset="50%" stop-color="#c8a84e"/>
|
|
10
|
+
<stop offset="100%" stop-color="#8b6914"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<!-- Background -->
|
|
14
|
+
<rect x="1" y="1" width="62" height="62" rx="12" fill="url(#bg)" stroke="#1c212b" stroke-width="1.5"/>
|
|
15
|
+
<!-- SF text -->
|
|
16
|
+
<text x="32" y="43" font-family="'Segoe UI',system-ui,-apple-system,sans-serif" font-size="28" font-weight="700" fill="url(#gold)" text-anchor="middle" letter-spacing="-1">SF</text>
|
|
17
|
+
<!-- Subtle accent dot -->
|
|
18
|
+
<circle cx="48" cy="15" r="3.5" fill="#c8a84e" opacity="0.7"/>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sapsf-shared"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared Python SDK for SAP SuccessFactors tools - OData client, auth, config, and Flask base"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{name = "Sahir Vhora"}]
|
|
13
|
+
keywords = ["sap", "successfactors", "odata", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"requests>=2.31",
|
|
24
|
+
"pyyaml>=6.0",
|
|
25
|
+
"rich>=13.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/SahirVhora/sapsf-shared"
|
|
30
|
+
Repository = "https://github.com/SahirVhora/sapsf-shared"
|
|
31
|
+
Issues = "https://github.com/SahirVhora/sapsf-shared/issues"
|
|
32
|
+
"SF Compass Suite" = "https://sahirvhora.github.io/sf-compass/"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
flask = ["flask>=2.3"]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-xdist>=3.0",
|
|
39
|
+
"responses>=0.25",
|
|
40
|
+
"mypy>=1.0",
|
|
41
|
+
"ruff>=0.6",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/sapsf_shared"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
addopts = "-v"
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.11"
|
|
53
|
+
warn_return_any = true
|
|
54
|
+
warn_unused_configs = true
|
|
55
|
+
disallow_untyped_defs = true
|
|
56
|
+
disallow_incomplete_defs = true
|
|
57
|
+
check_untyped_defs = true
|
|
58
|
+
warn_redundant_casts = true
|
|
59
|
+
warn_unused_ignores = true
|
|
60
|
+
show_error_codes = true
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
target-version = "py311"
|
|
64
|
+
line-length = 100
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I", "W", "UP", "B", "C4", "SIM"]
|
|
68
|
+
ignore = ["E501", "SIM115"]
|