ihalent 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.
- ihalent-0.1.0/.github/workflows/ci.yml +41 -0
- ihalent-0.1.0/.gitignore +22 -0
- ihalent-0.1.0/CHANGELOG.md +21 -0
- ihalent-0.1.0/CONTRIBUTING.md +31 -0
- ihalent-0.1.0/LICENSE +21 -0
- ihalent-0.1.0/PKG-INFO +224 -0
- ihalent-0.1.0/README.md +191 -0
- ihalent-0.1.0/docs/schema.md +64 -0
- ihalent-0.1.0/examples/README.md +25 -0
- ihalent-0.1.0/examples/build_sample.py +63 -0
- ihalent-0.1.0/examples/notices/2025-2174700.md +25 -0
- ihalent-0.1.0/examples/notices/2025-2198370.md +25 -0
- ihalent-0.1.0/examples/notices/2025-2337797.md +25 -0
- ihalent-0.1.0/examples/notices/2025-2388267.md +26 -0
- ihalent-0.1.0/examples/sample-awards.jsonl +4 -0
- ihalent-0.1.0/pyproject.toml +96 -0
- ihalent-0.1.0/src/ihalent/__init__.py +29 -0
- ihalent-0.1.0/src/ihalent/analytics.py +280 -0
- ihalent-0.1.0/src/ihalent/cli.py +187 -0
- ihalent-0.1.0/src/ihalent/ingest.py +108 -0
- ihalent-0.1.0/src/ihalent/mcp_server.py +110 -0
- ihalent-0.1.0/src/ihalent/model.py +111 -0
- ihalent-0.1.0/src/ihalent/normalize.py +64 -0
- ihalent-0.1.0/src/ihalent/parse.py +168 -0
- ihalent-0.1.0/src/ihalent/py.typed +0 -0
- ihalent-0.1.0/src/ihalent/render.py +103 -0
- ihalent-0.1.0/src/ihalent/store.py +49 -0
- ihalent-0.1.0/tests/conftest.py +60 -0
- ihalent-0.1.0/tests/test_analytics.py +108 -0
- ihalent-0.1.0/tests/test_ingest.py +85 -0
- ihalent-0.1.0/tests/test_mcp_tools.py +95 -0
- ihalent-0.1.0/tests/test_normalize.py +29 -0
- ihalent-0.1.0/tests/test_parse.py +91 -0
- ihalent-0.1.0/tests/test_store_cli.py +142 -0
- ihalent-0.1.0/uv.lock +1349 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, windows-latest]
|
|
14
|
+
python: ["3.10", "3.14"]
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
|
|
23
|
+
- name: Install
|
|
24
|
+
run: uv sync --dev
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: uv run ruff check src tests
|
|
28
|
+
|
|
29
|
+
- name: Types
|
|
30
|
+
run: uv run mypy src
|
|
31
|
+
|
|
32
|
+
- name: Tests
|
|
33
|
+
run: uv run pytest
|
|
34
|
+
|
|
35
|
+
- name: The bundled example must rebuild and analyze cleanly
|
|
36
|
+
shell: bash
|
|
37
|
+
run: |
|
|
38
|
+
uv run python examples/build_sample.py
|
|
39
|
+
# --json is ASCII by contract, so this is safe on any console code page;
|
|
40
|
+
# the negative-discount case must survive the round trip.
|
|
41
|
+
uv run ihalent overview examples/sample-awards.jsonl --json | grep -q '"min": -2.96'
|
ihalent-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
|
|
9
|
+
# tooling caches
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
|
|
14
|
+
# editors / os
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
Thumbs.db
|
|
18
|
+
.DS_Store
|
|
19
|
+
|
|
20
|
+
# local award data (users collect their own; only the example ships)
|
|
21
|
+
/awards.jsonl
|
|
22
|
+
/bundle.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-07-17
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
- Parse EKAP result notices (Sonuç İlanı) into structured `Award` records: İKN, estimate,
|
|
8
|
+
contract price, winner (with joint-venture members), bid counts, dates — deterministically,
|
|
9
|
+
with missing fields left `None` rather than guessed.
|
|
10
|
+
- Analytics: firm profiles (folded across spelling variants), discount (kırım) distributions
|
|
11
|
+
grouped by authority / province / tender type, and competition stats including single-bid
|
|
12
|
+
share.
|
|
13
|
+
- The coverage discipline: every statistic reports how many records it used and how many it
|
|
14
|
+
dropped for missing data; discounts are never computed from a missing estimate.
|
|
15
|
+
- `ingest` from ihale-mcp / EKAP output (pure function, no network), plus `overview`, `firm`,
|
|
16
|
+
`discounts` and `parse` commands, each with a `--json` form.
|
|
17
|
+
- Optional MCP server (`pip install 'ihalent[mcp]'`, `ihalent-mcp`) exposing the analytics as
|
|
18
|
+
agent tools over a dataset from `IHALENT_AWARDS`.
|
|
19
|
+
- Worked example built from four real December-2025 awards across four authority types,
|
|
20
|
+
including a negative-discount emergency-procurement case and the code that structures them.
|
|
21
|
+
- 44 tests over the exact JSON shapes EKAP and ihale-mcp emit.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for considering it. ihalent stays useful only if it stays honest about what it does
|
|
4
|
+
and does not know, so a few rules protect that.
|
|
5
|
+
|
|
6
|
+
## Ground rules
|
|
7
|
+
|
|
8
|
+
- **The core is a pure function of its input.** No network calls, no signing keys, no live
|
|
9
|
+
scraping in `parse`, `analytics`, `ingest` or `store`. Collection is ihale-mcp's job.
|
|
10
|
+
- **Never invent a number.** A missing field is `None`, not `0` or `""`. A statistic reports
|
|
11
|
+
its coverage. If a change would let a mean hide how many records lacked the value, it's the
|
|
12
|
+
wrong change.
|
|
13
|
+
- **Parsing changes ship with a real notice.** The tests are built on the exact markdown EKAP
|
|
14
|
+
serves. If you handle a new notice variant, add the real (trimmed) notice that motivated it
|
|
15
|
+
to `tests/` so the parser is pinned to reality, not to a tidy invention.
|
|
16
|
+
- **Company-name folding errs toward splitting, not merging.** Merging two real firms into one
|
|
17
|
+
row corrupts the analysis silently; showing two rows for one firm is visible and fixable.
|
|
18
|
+
New normalization rules need a test that they don't over-merge.
|
|
19
|
+
|
|
20
|
+
## Mechanics
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
uv sync --dev
|
|
24
|
+
uv run pytest
|
|
25
|
+
uv run ruff check src tests
|
|
26
|
+
uv run mypy src
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
All three must be clean; CI runs them on Linux and Windows. Keep PRs scoped to one change,
|
|
30
|
+
and if you touch the parser or the example, re-run `python examples/build_sample.py` in the
|
|
31
|
+
same PR so the committed sample matches the code.
|
ihalent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mehmet Eren Gülmez
|
|
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.
|
ihalent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ihalent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turkish public-tender award intelligence: who won, for how much, at what discount, against how many bidders.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gulmezeren2-byte/ihalent
|
|
6
|
+
Project-URL: Repository, https://github.com/gulmezeren2-byte/ihalent
|
|
7
|
+
Project-URL: Issues, https://github.com/gulmezeren2-byte/ihalent/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/gulmezeren2-byte/ihalent/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Mehmet Eren Gülmez
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,analytics,ekap,government-data,mcp,public-procurement,tenders,turkey
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Provides-Extra: mcp
|
|
31
|
+
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# ihalent
|
|
35
|
+
|
|
36
|
+
**Turkish public-tender award intelligence: who won, for how much, at what discount, against how many bidders.**
|
|
37
|
+
|
|
38
|
+
[](https://github.com/gulmezeren2-byte/ihalent/actions/workflows/ci.yml)
|
|
39
|
+
[](pyproject.toml)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
Every public tender in Turkey ends with a *Sonuç İlanı* — a result notice that states the
|
|
43
|
+
estimate, the contract price, the winner, and how many firms bid. The notices are public.
|
|
44
|
+
They are also one at a time, unstructured, and impossible to reason across: you cannot ask
|
|
45
|
+
EKAP "what has this company won in the last two years" or "how far below estimate does this
|
|
46
|
+
authority actually award." ihalent turns those notices into structured records and answers
|
|
47
|
+
exactly those questions — firm histories, discount (kırım) distributions, and competition
|
|
48
|
+
metrics — with every number traceable back to the notice it came from.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
$ ihalent overview awards.jsonl
|
|
52
|
+
|
|
53
|
+
Dataset overview
|
|
54
|
+
4 tenders: 4 awarded, 0 cancelled
|
|
55
|
+
total awarded value: 677.878.066,48 TL
|
|
56
|
+
Discount (kırım): mean 18.22% median 20.99% min -2.96% max 33.87%
|
|
57
|
+
based on all 4 awards.
|
|
58
|
+
competition: 5.75 valid bids on average (median 4.0); 0.0% had a single valid bid
|
|
59
|
+
data gaps: 0 awards without an estimate, 0 without a bid count.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That is real output over four real December-2025 awards (in
|
|
63
|
+
[`examples/`](examples/)). The `-2.96%` is not a bug: Istanbul University awarded a
|
|
64
|
+
615-million-lira campus job at **2.96% above** its own estimate, through an emergency
|
|
65
|
+
"pazarlık" procedure — a contract signed over the public estimate, which is precisely the
|
|
66
|
+
kind of thing that should be easy to see and currently is not.
|
|
67
|
+
|
|
68
|
+
## Why this exists
|
|
69
|
+
|
|
70
|
+
I'm an industrial engineer and I work in construction. When our firm weighs a public
|
|
71
|
+
tender, the questions that decide the bid are all about other people's history: how far
|
|
72
|
+
below estimate does this authority award, how many firms usually show up, what has a given
|
|
73
|
+
competitor been winning and at what price. All of that is public — it is sitting in tens of
|
|
74
|
+
thousands of result notices on EKAP — and none of it is queryable. So everyone rebuilds a
|
|
75
|
+
private, partial version of it by hand, in spreadsheets, badly.
|
|
76
|
+
|
|
77
|
+
There is already an excellent tool for *reaching* this data:
|
|
78
|
+
[saidsurucu/ihale-mcp](https://github.com/saidsurucu/ihale-mcp) solved authenticated EKAP
|
|
79
|
+
access and hands you the notices. ihalent is the layer above it — the one that turns a pile
|
|
80
|
+
of notices into an answer. It does not re-scrape EKAP; it structures what you have collected
|
|
81
|
+
and does the analytics that don't exist yet.
|
|
82
|
+
|
|
83
|
+
## What it answers
|
|
84
|
+
|
|
85
|
+
**A firm's history** — every award across the dataset, folded across spelling variants:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
$ ihalent firm awards.jsonl "ÖZDEN YEL"
|
|
89
|
+
|
|
90
|
+
ÖZDEN YEL
|
|
91
|
+
wins: 1 joint ventures: 1 total contract value: 19.709.997,40 TL
|
|
92
|
+
Discount (kırım): mean 33.87%
|
|
93
|
+
top awarding authorities:
|
|
94
|
+
1x DSİ 14. Bölge Müdürlüğü
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Where the discounts are** — grouped and sorted, highest first:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
$ ihalent discounts awards.jsonl --by authority
|
|
101
|
+
|
|
102
|
+
Mean discount by authority
|
|
103
|
+
DSİ 14. Bölge Müdürlüğü 33.87% 1/1
|
|
104
|
+
Esenler Belediyesi 21.99% 1/1
|
|
105
|
+
Ağrı İl Özel İdaresi 19.99% 1/1
|
|
106
|
+
İstanbul Üniversitesi-Cerrahpaşa -2.96% 1/1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Competition** — how many valid bids show up, and how often exactly one does (a single-bid
|
|
110
|
+
award is a flag procurement watchdogs care about). It's in the overview above.
|
|
111
|
+
|
|
112
|
+
## The one rule
|
|
113
|
+
|
|
114
|
+
**A number is never shown without the ground it stands on.** A mean discount always comes
|
|
115
|
+
with "over how many awards, and how many were dropped for a missing estimate." A firm's
|
|
116
|
+
total contract value says so when some of its wins had no published price, so you read it as
|
|
117
|
+
a floor, not the full figure. A discount is `None`, never `0`, when the estimate wasn't
|
|
118
|
+
published — ihalent does not invent the numbers the government didn't print. Half the value
|
|
119
|
+
of a tool like this is refusing to guess.
|
|
120
|
+
|
|
121
|
+
## Install
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
pip install git+https://github.com/gulmezeren2-byte/ihalent
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A PyPI release (`pip install ihalent`) follows once the first users have kicked the tires.
|
|
128
|
+
|
|
129
|
+
## The workflow
|
|
130
|
+
|
|
131
|
+
ihalent reads a JSONL file of awards. You produce it by collecting result notices — the
|
|
132
|
+
easy path is ihale-mcp — and letting ihalent structure them:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
# 1. collect: with ihale-mcp connected to your agent, save what its
|
|
136
|
+
# get_tender_announcements returns (one or many tenders) to bundle.json
|
|
137
|
+
|
|
138
|
+
# 2. structure:
|
|
139
|
+
ihalent ingest bundle.json -o awards.jsonl
|
|
140
|
+
|
|
141
|
+
# 3. ask:
|
|
142
|
+
ihalent overview awards.jsonl
|
|
143
|
+
ihalent firm awards.jsonl "ACME İNŞAAT"
|
|
144
|
+
ihalent discounts awards.jsonl --by province --min 5
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Every command takes `--json` for pipelines and agents. Or skip collection and try the
|
|
148
|
+
bundled example directly:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
git clone https://github.com/gulmezeren2-byte/ihalent && cd ihalent
|
|
152
|
+
python examples/build_sample.py # parses the real notices in examples/notices/
|
|
153
|
+
ihalent overview examples/sample-awards.jsonl
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Commands
|
|
157
|
+
|
|
158
|
+
| command | what it does |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `ihalent overview AWARDS` | value, discount, competition and the data gaps of a dataset |
|
|
161
|
+
| `ihalent firm AWARDS "NAME"` | one company's wins, total value, discount, and where it wins |
|
|
162
|
+
| `ihalent discounts AWARDS --by X` | mean/median discount grouped by authority, province or tender_type |
|
|
163
|
+
| `ihalent parse NOTICE.md` | one result notice → structured JSON |
|
|
164
|
+
| `ihalent ingest BUNDLE.json` | collected ihale-mcp/EKAP output → awards JSONL |
|
|
165
|
+
|
|
166
|
+
## Using it with AI agents
|
|
167
|
+
|
|
168
|
+
The result notice is unstructured text; the interesting questions are aggregate. That is an
|
|
169
|
+
awkward fit for an agent working notice-by-notice, and a natural fit for a tool: `--json`
|
|
170
|
+
output with stable fields, an exit code that means something, and a firm-name match that
|
|
171
|
+
folds spelling variants so an agent doesn't have to.
|
|
172
|
+
|
|
173
|
+
There is a native **MCP server** (`pip install 'ihalent[mcp]'`) that exposes the analytics
|
|
174
|
+
as tools — `overview`, `firm`, `discounts`, `parse_notice`, `ingest_bundle` — over a dataset
|
|
175
|
+
you point it at:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
IHALENT_AWARDS=awards.jsonl ihalent-mcp
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Pair it with ihale-mcp and an agent can collect notices and reason across them in one
|
|
182
|
+
session: ihale-mcp fetches, ihalent structures and aggregates. The Python API
|
|
183
|
+
(`ihalent.ingest_bundle`, `ihalent.analytics.firm_profile`) is three calls deep if you'd
|
|
184
|
+
rather script it.
|
|
185
|
+
|
|
186
|
+
## Scope, honestly
|
|
187
|
+
|
|
188
|
+
- **This is analytics, not a scraper.** Collection is ihale-mcp's job (it does it well);
|
|
189
|
+
ihalent deliberately owns the layer above and stays a pure function of the data you give
|
|
190
|
+
it — no network, no signing keys, nothing that breaks when EKAP rotates a header.
|
|
191
|
+
- **Company-name folding is conservative on purpose.** It merges legal-form suffixes and
|
|
192
|
+
Turkish spelling variants, and it would rather show two rows for one firm than one row for
|
|
193
|
+
two — so `firm` reports how many distinct spellings a query matched, and warns you if that
|
|
194
|
+
is more than one.
|
|
195
|
+
- **The parser tracks one document: the result notice.** Bid-level detail (who else bid, at
|
|
196
|
+
what price) is not in the notice and so is not here. Discount is the estimate-to-contract
|
|
197
|
+
gap, which the notice does carry.
|
|
198
|
+
- **Award values are nominal lira, as printed.** Comparing 2023 and 2026 contracts is your
|
|
199
|
+
analysis to make with the dates in hand; ihalent does not silently inflation-adjust.
|
|
200
|
+
|
|
201
|
+
## What ihalent is not
|
|
202
|
+
|
|
203
|
+
It is not a replacement for [ihale-mcp](https://github.com/saidsurucu/ihale-mcp) — it sits
|
|
204
|
+
on top of it. It is not a live dashboard or a paid tender-alert service; it is a library and
|
|
205
|
+
a CLI you point at data you control. And it does not redistribute a dataset: it ships the
|
|
206
|
+
handful of real notices in `examples/` and the code to structure your own.
|
|
207
|
+
|
|
208
|
+
## A note on the data
|
|
209
|
+
|
|
210
|
+
Public-tender results are public information, published by the state for public scrutiny.
|
|
211
|
+
ihalent structures what EKAP already discloses. Company names that appear are legal entities
|
|
212
|
+
in their public commercial capacity, not private individuals.
|
|
213
|
+
|
|
214
|
+
## How this project is built
|
|
215
|
+
|
|
216
|
+
I designed the model and the analytics and I review every line; I use AI agents (Claude
|
|
217
|
+
Code) heavily for implementation speed, and the commit trailers say so. The contract is the
|
|
218
|
+
tests — 44 of them, built on the exact JSON shapes EKAP and ihale-mcp emit, including a
|
|
219
|
+
result notice with a negative discount and a cancelled tender. They don't care who typed
|
|
220
|
+
them.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
[MIT](LICENSE) — Mehmet Eren Gülmez
|
ihalent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# ihalent
|
|
2
|
+
|
|
3
|
+
**Turkish public-tender award intelligence: who won, for how much, at what discount, against how many bidders.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/gulmezeren2-byte/ihalent/actions/workflows/ci.yml)
|
|
6
|
+
[](pyproject.toml)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Every public tender in Turkey ends with a *Sonuç İlanı* — a result notice that states the
|
|
10
|
+
estimate, the contract price, the winner, and how many firms bid. The notices are public.
|
|
11
|
+
They are also one at a time, unstructured, and impossible to reason across: you cannot ask
|
|
12
|
+
EKAP "what has this company won in the last two years" or "how far below estimate does this
|
|
13
|
+
authority actually award." ihalent turns those notices into structured records and answers
|
|
14
|
+
exactly those questions — firm histories, discount (kırım) distributions, and competition
|
|
15
|
+
metrics — with every number traceable back to the notice it came from.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
$ ihalent overview awards.jsonl
|
|
19
|
+
|
|
20
|
+
Dataset overview
|
|
21
|
+
4 tenders: 4 awarded, 0 cancelled
|
|
22
|
+
total awarded value: 677.878.066,48 TL
|
|
23
|
+
Discount (kırım): mean 18.22% median 20.99% min -2.96% max 33.87%
|
|
24
|
+
based on all 4 awards.
|
|
25
|
+
competition: 5.75 valid bids on average (median 4.0); 0.0% had a single valid bid
|
|
26
|
+
data gaps: 0 awards without an estimate, 0 without a bid count.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That is real output over four real December-2025 awards (in
|
|
30
|
+
[`examples/`](examples/)). The `-2.96%` is not a bug: Istanbul University awarded a
|
|
31
|
+
615-million-lira campus job at **2.96% above** its own estimate, through an emergency
|
|
32
|
+
"pazarlık" procedure — a contract signed over the public estimate, which is precisely the
|
|
33
|
+
kind of thing that should be easy to see and currently is not.
|
|
34
|
+
|
|
35
|
+
## Why this exists
|
|
36
|
+
|
|
37
|
+
I'm an industrial engineer and I work in construction. When our firm weighs a public
|
|
38
|
+
tender, the questions that decide the bid are all about other people's history: how far
|
|
39
|
+
below estimate does this authority award, how many firms usually show up, what has a given
|
|
40
|
+
competitor been winning and at what price. All of that is public — it is sitting in tens of
|
|
41
|
+
thousands of result notices on EKAP — and none of it is queryable. So everyone rebuilds a
|
|
42
|
+
private, partial version of it by hand, in spreadsheets, badly.
|
|
43
|
+
|
|
44
|
+
There is already an excellent tool for *reaching* this data:
|
|
45
|
+
[saidsurucu/ihale-mcp](https://github.com/saidsurucu/ihale-mcp) solved authenticated EKAP
|
|
46
|
+
access and hands you the notices. ihalent is the layer above it — the one that turns a pile
|
|
47
|
+
of notices into an answer. It does not re-scrape EKAP; it structures what you have collected
|
|
48
|
+
and does the analytics that don't exist yet.
|
|
49
|
+
|
|
50
|
+
## What it answers
|
|
51
|
+
|
|
52
|
+
**A firm's history** — every award across the dataset, folded across spelling variants:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
$ ihalent firm awards.jsonl "ÖZDEN YEL"
|
|
56
|
+
|
|
57
|
+
ÖZDEN YEL
|
|
58
|
+
wins: 1 joint ventures: 1 total contract value: 19.709.997,40 TL
|
|
59
|
+
Discount (kırım): mean 33.87%
|
|
60
|
+
top awarding authorities:
|
|
61
|
+
1x DSİ 14. Bölge Müdürlüğü
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Where the discounts are** — grouped and sorted, highest first:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
$ ihalent discounts awards.jsonl --by authority
|
|
68
|
+
|
|
69
|
+
Mean discount by authority
|
|
70
|
+
DSİ 14. Bölge Müdürlüğü 33.87% 1/1
|
|
71
|
+
Esenler Belediyesi 21.99% 1/1
|
|
72
|
+
Ağrı İl Özel İdaresi 19.99% 1/1
|
|
73
|
+
İstanbul Üniversitesi-Cerrahpaşa -2.96% 1/1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Competition** — how many valid bids show up, and how often exactly one does (a single-bid
|
|
77
|
+
award is a flag procurement watchdogs care about). It's in the overview above.
|
|
78
|
+
|
|
79
|
+
## The one rule
|
|
80
|
+
|
|
81
|
+
**A number is never shown without the ground it stands on.** A mean discount always comes
|
|
82
|
+
with "over how many awards, and how many were dropped for a missing estimate." A firm's
|
|
83
|
+
total contract value says so when some of its wins had no published price, so you read it as
|
|
84
|
+
a floor, not the full figure. A discount is `None`, never `0`, when the estimate wasn't
|
|
85
|
+
published — ihalent does not invent the numbers the government didn't print. Half the value
|
|
86
|
+
of a tool like this is refusing to guess.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
pip install git+https://github.com/gulmezeren2-byte/ihalent
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A PyPI release (`pip install ihalent`) follows once the first users have kicked the tires.
|
|
95
|
+
|
|
96
|
+
## The workflow
|
|
97
|
+
|
|
98
|
+
ihalent reads a JSONL file of awards. You produce it by collecting result notices — the
|
|
99
|
+
easy path is ihale-mcp — and letting ihalent structure them:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
# 1. collect: with ihale-mcp connected to your agent, save what its
|
|
103
|
+
# get_tender_announcements returns (one or many tenders) to bundle.json
|
|
104
|
+
|
|
105
|
+
# 2. structure:
|
|
106
|
+
ihalent ingest bundle.json -o awards.jsonl
|
|
107
|
+
|
|
108
|
+
# 3. ask:
|
|
109
|
+
ihalent overview awards.jsonl
|
|
110
|
+
ihalent firm awards.jsonl "ACME İNŞAAT"
|
|
111
|
+
ihalent discounts awards.jsonl --by province --min 5
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every command takes `--json` for pipelines and agents. Or skip collection and try the
|
|
115
|
+
bundled example directly:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
git clone https://github.com/gulmezeren2-byte/ihalent && cd ihalent
|
|
119
|
+
python examples/build_sample.py # parses the real notices in examples/notices/
|
|
120
|
+
ihalent overview examples/sample-awards.jsonl
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
| command | what it does |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `ihalent overview AWARDS` | value, discount, competition and the data gaps of a dataset |
|
|
128
|
+
| `ihalent firm AWARDS "NAME"` | one company's wins, total value, discount, and where it wins |
|
|
129
|
+
| `ihalent discounts AWARDS --by X` | mean/median discount grouped by authority, province or tender_type |
|
|
130
|
+
| `ihalent parse NOTICE.md` | one result notice → structured JSON |
|
|
131
|
+
| `ihalent ingest BUNDLE.json` | collected ihale-mcp/EKAP output → awards JSONL |
|
|
132
|
+
|
|
133
|
+
## Using it with AI agents
|
|
134
|
+
|
|
135
|
+
The result notice is unstructured text; the interesting questions are aggregate. That is an
|
|
136
|
+
awkward fit for an agent working notice-by-notice, and a natural fit for a tool: `--json`
|
|
137
|
+
output with stable fields, an exit code that means something, and a firm-name match that
|
|
138
|
+
folds spelling variants so an agent doesn't have to.
|
|
139
|
+
|
|
140
|
+
There is a native **MCP server** (`pip install 'ihalent[mcp]'`) that exposes the analytics
|
|
141
|
+
as tools — `overview`, `firm`, `discounts`, `parse_notice`, `ingest_bundle` — over a dataset
|
|
142
|
+
you point it at:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
IHALENT_AWARDS=awards.jsonl ihalent-mcp
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Pair it with ihale-mcp and an agent can collect notices and reason across them in one
|
|
149
|
+
session: ihale-mcp fetches, ihalent structures and aggregates. The Python API
|
|
150
|
+
(`ihalent.ingest_bundle`, `ihalent.analytics.firm_profile`) is three calls deep if you'd
|
|
151
|
+
rather script it.
|
|
152
|
+
|
|
153
|
+
## Scope, honestly
|
|
154
|
+
|
|
155
|
+
- **This is analytics, not a scraper.** Collection is ihale-mcp's job (it does it well);
|
|
156
|
+
ihalent deliberately owns the layer above and stays a pure function of the data you give
|
|
157
|
+
it — no network, no signing keys, nothing that breaks when EKAP rotates a header.
|
|
158
|
+
- **Company-name folding is conservative on purpose.** It merges legal-form suffixes and
|
|
159
|
+
Turkish spelling variants, and it would rather show two rows for one firm than one row for
|
|
160
|
+
two — so `firm` reports how many distinct spellings a query matched, and warns you if that
|
|
161
|
+
is more than one.
|
|
162
|
+
- **The parser tracks one document: the result notice.** Bid-level detail (who else bid, at
|
|
163
|
+
what price) is not in the notice and so is not here. Discount is the estimate-to-contract
|
|
164
|
+
gap, which the notice does carry.
|
|
165
|
+
- **Award values are nominal lira, as printed.** Comparing 2023 and 2026 contracts is your
|
|
166
|
+
analysis to make with the dates in hand; ihalent does not silently inflation-adjust.
|
|
167
|
+
|
|
168
|
+
## What ihalent is not
|
|
169
|
+
|
|
170
|
+
It is not a replacement for [ihale-mcp](https://github.com/saidsurucu/ihale-mcp) — it sits
|
|
171
|
+
on top of it. It is not a live dashboard or a paid tender-alert service; it is a library and
|
|
172
|
+
a CLI you point at data you control. And it does not redistribute a dataset: it ships the
|
|
173
|
+
handful of real notices in `examples/` and the code to structure your own.
|
|
174
|
+
|
|
175
|
+
## A note on the data
|
|
176
|
+
|
|
177
|
+
Public-tender results are public information, published by the state for public scrutiny.
|
|
178
|
+
ihalent structures what EKAP already discloses. Company names that appear are legal entities
|
|
179
|
+
in their public commercial capacity, not private individuals.
|
|
180
|
+
|
|
181
|
+
## How this project is built
|
|
182
|
+
|
|
183
|
+
I designed the model and the analytics and I review every line; I use AI agents (Claude
|
|
184
|
+
Code) heavily for implementation speed, and the commit trailers say so. The contract is the
|
|
185
|
+
tests — 44 of them, built on the exact JSON shapes EKAP and ihale-mcp emit, including a
|
|
186
|
+
result notice with a negative discount and a cancelled tender. They don't care who typed
|
|
187
|
+
them.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
[MIT](LICENSE) — Mehmet Eren Gülmez
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Data model and JSON schema
|
|
2
|
+
|
|
3
|
+
ihalent's interchange format is JSON Lines: one award per line. This page documents the
|
|
4
|
+
`Award` record so you can produce it from any source, not only ihale-mcp.
|
|
5
|
+
|
|
6
|
+
## Award
|
|
7
|
+
|
|
8
|
+
| field | type | meaning |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `ikn` | string | İhale Kayıt Numarası, e.g. `"2025/2198370"`. The primary key. |
|
|
11
|
+
| `title` | string \| null | Tender title. |
|
|
12
|
+
| `authority` | string \| null | Awarding authority (idare). |
|
|
13
|
+
| `province` | string \| null | Province. |
|
|
14
|
+
| `tender_type` | string | `mal` \| `yapim` \| `hizmet` \| `danismanlik` \| `unknown`. |
|
|
15
|
+
| `estimate_try` | number \| null | Yaklaşık Maliyeti (public estimate), nominal lira. |
|
|
16
|
+
| `contract_try` | number \| null | Sözleşme Bedeli (contract price), nominal lira. |
|
|
17
|
+
| `winner` | string \| null | Lead bidder (first named party in a joint venture). |
|
|
18
|
+
| `winners_all` | string[] | All named parties (the JV members). |
|
|
19
|
+
| `is_joint_venture` | bool | True if the award went to an İş Ortaklığı. |
|
|
20
|
+
| `downloaders` | int \| null | Firms that downloaded the documents. |
|
|
21
|
+
| `bid_count` | int \| null | Toplam Teklif Sayısı. |
|
|
22
|
+
| `valid_bid_count` | int \| null | Toplam Geçerli Teklif Sayısı. |
|
|
23
|
+
| `result_date` | string \| null | Contract date, ISO (`YYYY-MM-DD`) where parseable. |
|
|
24
|
+
| `cancelled` | bool | True for a cancelled tender (result notice with no winner/price). |
|
|
25
|
+
|
|
26
|
+
Two fields are **computed** and appear in serialized output but are never stored as input:
|
|
27
|
+
|
|
28
|
+
| field | type | meaning |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `discount_pct` | number \| null | `100 * (estimate - contract) / estimate`, rounded to 2 dp. `null` if either value is missing or the estimate is 0. Negative means the contract came in *above* estimate. |
|
|
31
|
+
| `single_bid` | bool \| null | `valid_bid_count <= 1`. `null` if the count is missing. |
|
|
32
|
+
|
|
33
|
+
### The null discipline
|
|
34
|
+
|
|
35
|
+
Every optional field is `null` when the source did not carry it — never `0`, never `""`.
|
|
36
|
+
A discount computed from a missing estimate is `null`, not `0%`. This is load-bearing: the
|
|
37
|
+
analytics layer counts these gaps and reports them (`data_gaps` in the overview, the
|
|
38
|
+
`coverage` block on every statistic), so a mean is always qualified by how many records
|
|
39
|
+
actually had the number.
|
|
40
|
+
|
|
41
|
+
## Example line
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{"ikn": "2025/2198370", "title": "İSTANBUL ARNAVUTKÖY TÜRKKÖŞE DERESİ ISLAHI", "authority": "DSİ 14. Bölge Müdürlüğü", "province": "İstanbul", "tender_type": "yapim", "estimate_try": 29802943.48, "contract_try": 19709997.4, "winner": "ÖZDEN YEL", "winners_all": ["ÖZDEN YEL", "ATAŞ MÜHENDİSLİK ..."], "is_joint_venture": true, "downloaders": 34, "bid_count": 20, "valid_bid_count": 12, "result_date": "2026-02-13", "cancelled": false, "discount_pct": 33.87, "single_bid": false}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Producing awards
|
|
48
|
+
|
|
49
|
+
Three routes, all landing on the same JSONL:
|
|
50
|
+
|
|
51
|
+
1. **From ihale-mcp / EKAP** — `ihalent ingest bundle.json` (or `ihalent.ingest_bundle` in
|
|
52
|
+
Python). Accepts a `get_tender_announcements` object, a list of them, or a list of
|
|
53
|
+
`{tender, announcements}` pairs.
|
|
54
|
+
2. **From a single notice** — `ihalent parse notice.md` emits one record you can append.
|
|
55
|
+
3. **From your own pipeline** — build `Award` objects directly and `dump_awards(...)`; only
|
|
56
|
+
`ikn` is required.
|
|
57
|
+
|
|
58
|
+
## Company-name folding
|
|
59
|
+
|
|
60
|
+
Firm queries and JV membership are matched on a normalized key: Turkish diacritics folded to
|
|
61
|
+
ASCII, legal-form suffixes (`LİMİTED ŞİRKETİ`, `LTD ŞTİ`, `SANAYİ VE TİCARET ...`) stripped,
|
|
62
|
+
whitespace and punctuation collapsed. The fold is deliberately conservative — it will not
|
|
63
|
+
merge `ANADOLU İNŞAAT` with `ANKARA İNŞAAT` — and `firm` reports `distinct_spellings` so an
|
|
64
|
+
over-broad query is visible rather than silently merging unrelated companies.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Example: four real December-2025 awards
|
|
2
|
+
|
|
3
|
+
The `notices/` folder holds four real result notices (Sonuç İlanı), pulled from EKAP via
|
|
4
|
+
ihale-mcp, one per file, named by İKN. They span four kinds of awarding authority on
|
|
5
|
+
purpose:
|
|
6
|
+
|
|
7
|
+
| İKN | authority | discount | note |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| 2025/2337797 | Ağrı İl Özel İdaresi | 19.99% | joint venture, provincial repair job |
|
|
10
|
+
| 2025/2198370 | DSİ 14. Bölge Müdürlüğü | 33.87% | 12 valid bids — real competition |
|
|
11
|
+
| 2025/2174700 | Esenler Belediyesi | 21.99% | municipal school/mosque repairs |
|
|
12
|
+
| 2025/2388267 | İstanbul Üniversitesi-Cerrahpaşa | **-2.96%** | emergency "pazarlık", awarded *above* estimate |
|
|
13
|
+
|
|
14
|
+
We ship the notices, not a redistributed dataset. `build_sample.py` parses them into
|
|
15
|
+
`sample-awards.jsonl` exactly as `ihalent ingest` would with your own collected notices:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
python build_sample.py
|
|
19
|
+
ihalent overview sample-awards.jsonl
|
|
20
|
+
ihalent discounts sample-awards.jsonl --by authority
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `-2.96%` case is the one to notice. A contract signed 2.96% above the public estimate,
|
|
24
|
+
through an emergency procedure that skips open bidding, is exactly the kind of thing these
|
|
25
|
+
notices disclose and nobody aggregates — which is the whole reason this tool exists.
|