এই পর্বে যা শিখবেন
- src layout কেন ভুলবশত import-এর ফাঁদ আটকায়, আর একটি প্রকাশযোগ্য প্রজেক্টের কাঠামো
- pyproject.toml-এর প্রতিটি অংশ — license (SPDX), classifier, optional dependency, console script
- build backend বাছাই,
python -m buildচালানো আর wheel খুলে ভেতরটা পড়া - PEP 440 version, pre-release, এক জায়গায় version রাখা, আর library বনাম অ্যাপের dependency-নীতি
- TestPyPI, twine, API token আর GitHub Actions থেকে trusted publishing; কোম্পানির private index
১ · ইনস্টল করা অর্ধেক গল্প — নিজের কোড কেন package হবে
নুসরাত ঢাকার একটি ই-কমার্স কোম্পানিতে Python ডেভেলপার। বছরখানেক আগে তিনি একটি ছোট ফাংশন লিখেছিলেন — format_taka(), যা 1234567-কে লেখে 12,34,567, অর্থাৎ বাংলাদেশের লাখ-কোটির নিয়মে কমা বসায়। ফাংশনটি এত কাজের যে সেটি এখন তিন জায়গায় আছে: ইনভয়েসের অ্যাপে, মাসিক বিক্রির রিপোর্ট-স্ক্রিপ্টে আর দোকানের ওয়েবসাইটে। তিনটিই কপি-পেস্ট করা।
একদিন ধরা পড়ল, ঋণাত্মক টাকায় (ফেরত দেওয়া অর্ডার) ফাংশনটি ভুল কমা বসায়। নুসরাত ইনভয়েস-অ্যাপে ঠিক করলেন। রিপোর্ট-স্ক্রিপ্ট আর ওয়েবসাইটে পুরোনো ভুল রয়ে গেল। এর মধ্যে এক সহকর্মী ওয়েবসাইট থেকে পুরোনো কপিটিই নিজের নতুন প্রজেক্টে নিয়ে গেছেন। এখন চারটি কপি, তিন রকম আচরণ, আর কেউ জানে না কোনটি “আসল”।
পর্ব ২৩-এ আপনি শিখেছেন অন্যের package কীভাবে আনতে হয় — pip, venv, version-শর্ত। কিন্তু সেটি গল্পের অর্ধেক। বাকি অর্ধেক হলো নিজের কোডকে এমন একটি জিনিসে রূপ দেওয়া, যা pip install করা যায়: একটি নাম, একটি version, নিজের dependency-তালিকা আর একটি মানক ফাইল-ফরম্যাট। তখন নুসরাতের সমাধান সহজ — taka-format নামে একটি package, version 0.1.0. ভুল ঠিক হলে 0.1.1, আর প্রতিটি প্রজেক্ট নিজের সুবিধামতো, পরীক্ষা করে আপগ্রেড করে। কোড থাকে এক জায়গায়, ইতিহাস থাকে version নম্বরে।
একটি মিষ্টির দোকানের কথা ভাবুন। বাড়িতে নিজের জন্য রসগোল্লা বানাতে রেসিপি মাথায় থাকলেই চলে। কিন্তু দোকানে বিক্রি করতে হলে লাগে প্যাকেট, প্যাকেটের গায়ে নাম, ওজন, উপাদানের তালিকা, তৈরির তারিখ (version) আর একটি নির্দিষ্ট মাপের বাক্স, যা যেকোনো ডেলিভারি-ভ্যানে ধরে। Python-এ রেসিপি হলো আপনার কোড, প্যাকেটের লেবেল pyproject.toml, বাক্স হলো wheel, আর বড় পাইকারি বাজার PyPI.
এই একই পথে তিনটি ভিন্ন গন্তব্যে যাওয়া যায়:
🧑💻 নিজের প্রজেক্টগুলো
একই utility কোড পাঁচটি প্রজেক্টে কপি না করে একটি package. git থেকে বা নিজের ফোল্ডারের wheel থেকে ইনস্টল।
pip install ./dist/*.whl🏢 দল বা কোম্পানি
ভেতরের লাইব্রেরি (যেমন কোম্পানির পেমেন্ট-গেটওয়ের ক্লায়েন্ট) শুধু কর্মীদের জন্য — private package index-এ।
অংশ ৯🌍 সারা পৃথিবী
PyPI-তে প্রকাশ — যে কেউ pip install taka-format দিতে পারবেন। requests, NumPy, Django সবাই এভাবেই এসেছে।
আধুনিক Python-এ পুরো কাজটি ঘোষণামূলক: আপনি একটি ফাইলে লিখে দেন প্রজেক্টটি কী — কোনো স্ক্রিপ্ট লিখে বলতে হয় না কীভাবে বানাতে হবে। তারপর একটি build backend সেই ঘোষণা পড়ে দুটি মানক ফাইল বানায়, যা pip, uv বা যেকোনো ইনস্টলার বোঝে। পুরো যাত্রাটি এক নজরে:
কোড থেকে অন্যের কম্পিউটার পর্যন্ত
source → declarative metadata → standard artifacts → index → install
- কোড
src/taka_format/আপনার module - লেবেল
pyproject.tomlনাম, version, dependency - Build
python -m buildbackend কাজ করে - দুটি ফাইল
.whl + .tar.gzdist/ ফোল্ডারে - Index
PyPI / privatepip install
দুটি শব্দ মনে করিয়ে দিই (পর্ব ২২ ও ২৩)। import package হলো __init__.py-সহ একটি ফোল্ডার, কোডে যার নাম লেখেন (import taka_format). আর distribution হলো সেই ফোল্ডার-সহ নাম ও version লাগানো বান্ডিল, যা pip ইনস্টল করে (pip install taka-format). এই পর্বের পুরোটা দ্বিতীয়টি বানানোর গল্প। একটি পরিচিত নিয়ম: distribution-এর নামে সাধারণত হাইফেন, import-এর নামে আন্ডারস্কোর, কারণ Python-এর নামে হাইফেন চলে না।
C/C++-এ library ভাগ করা মানে সাধারণত header আর compile করা .so/.dll/.a ফাইল দেওয়া, আর প্রতিটি OS ও compiler-এর জন্য আলাদা build — কোনো একক মানক ফরম্যাট বা কেন্দ্রীয় ভাণ্ডার নেই (vcpkg, Conan আছে, তবে সর্বজনীন নয়)। বিশুদ্ধ Python package-এ compile নেই, তাই একটি wheel সব OS-এ চলে; কিন্তু ভেতরে C extension থাকলে Python-কেও প্রতি প্ল্যাটফর্মে আলাদা wheel বানাতে হয় — ঠিক C++-এর সেই সমস্যা। তুলনার জন্য C++ কোর্সের CMake ও build পর্ব দেখুন; Java জানলে Maven Central-এ প্রকাশের ধারণা প্রায় একই — Java কোর্সের Maven ও Gradle পর্ব.
২ · প্রজেক্ট সাজানো — src layout বনাম flat layout
নুসরাতের package-এর পুরো ফোল্ডার নিচে দেখানো হলো। এই পর্ব লেখার আগে আমরা ঠিক এই প্রজেক্টটি সত্যিই বানিয়ে, build করে, আলাদা venv-এ ইনস্টল করে চালিয়ে দেখেছি — সামনের সব আউটপুট সেখান থেকেই নেওয়া।
taka-format/
├── pyproject.toml ← পরিচয়পত্র (অংশ ৩)
├── README.md ← PyPI-র পাতায় দেখাবে
├── LICENSE ← ব্যবহারের অনুমতির শর্ত
├── src/
│ └── taka_format/ ← import করা যায় এমন package
│ ├── __init__.py ← format_taka()
│ ├── cli.py ← terminal-এর "taka" কমান্ড
│ └── units.txt ← package-এর সাথে যাওয়া ডেটা-ফাইল
└── tests/
└── test_format.py
পর্ব ২৩-এ src layout-এর নাম শুনেছেন। এর বিকল্প হলো flat layout, যেখানে taka_format/ ফোল্ডার সরাসরি প্রজেক্টের মূলে থাকে, src/ ছাড়া। দুটিই বৈধ, দুটিই প্রচলিত — Python Packaging User Guide দুটির তুলনাই দেয়। তবে প্রকাশ করার মতো package-এ src layout বেশি নিরাপদ, আর তার কারণ একটি সূক্ষ্ম ফাঁদ।
পর্ব ২২-এ দেখেছেন, কোনো স্ক্রিপ্ট চালালে তার ফোল্ডার (আর python -m বা REPL-এ বর্তমান ফোল্ডার) sys.path-এর একেবারে শুরুতে বসে। Flat layout-এ প্রজেক্টের মূল থেকে টেস্ট চালালে import taka_format তাই ইনস্টল করা package নয়, পাশের ফোল্ডারটিই পেয়ে যায়। ফলে wheel-এ কোনো ফাইল বাদ পড়লেও আপনার কম্পিউটারে সব ঠিকঠাক চলে — ভাঙে শুধু ব্যবহারকারীর কম্পিউটারে। ধাপে ধাপে দেখুন:
- Flat
./taka_format/মূল ফোল্ডারে - অসম্পূর্ণ wheel
units.txt নেইকেউ টের পায়নি - টেস্ট পাস
sys.path[0] = .ভুল জিনিস পরীক্ষা - ব্যবহারকারী
FileNotFoundErrorপ্রকাশের পর - src layout
src/taka_format/ইনস্টল ছাড়া import নেই
taka-format/ ├── pyproject.toml ├── taka_format/ ← সরাসরি এখানে │ ├── __init__.py │ └── units.txt └── tests/
taka_format/__init__.py
taka_format/cli.py
taka_format/units.txt ← নেই!
taka_format-0.1.0.dist-info/...
$ python -m pytest sys.path[0] = taka-format/ → import taka_format = পাশের ফোল্ডার, যেখানে units.txt আছে 1 passed
$ taka FileNotFoundError: ... taka_format/units.txt ইনস্টল করা wheel-এ ফাইলটি কখনো ছিলই না
$ python -c "import taka_format" ModuleNotFoundError: No module named 'taka_format' → আগে ইনস্টল করতেই হবে; টেস্ট চলে ইনস্টল করা কপিতে
- Flat layout. package-এর ফোল্ডার প্রজেক্টের মূলে,
pyproject.toml-এর পাশে। দেখতে সরল, ছোট প্রজেক্টে কোনো সমস্যাও হয় না। কিন্তু মূল ফোল্ডার থেকে কিছু চালালেই এই ফোল্ডার “import করা যায়” — ইনস্টল না করেও। - wheel-এ একটি ফাইল বাদ। ধরুন build-এর সেটিং-এ একটি ভুলে
units.txtwheel-এ ঢোকেনি (অংশ ৭-এ দেখবেন, ডেটা-ফাইল নেওয়ার নিয়ম backend ভেদে আলাদা)। build কোনো error দেয় না — সে যা বলা হয়েছে, তা-ই করেছে। - তবু টেস্ট পাস। নুসরাত মূল ফোল্ডার থেকে pytest চালালেন।
sys.path-এর শুরুতে মূল ফোল্ডার, তাইimport taka_formatপেল পাশের ফোল্ডারটি — যেখানেunits.txtদিব্যি আছে। টেস্টগুলো আসলে wheel পরীক্ষাই করেনি। - প্রকাশের পর ভাঙন। ব্যবহারকারী PyPI থেকে সেই অসম্পূর্ণ wheel পেলেন, আর প্রথম চালানোতেই
FileNotFoundError. সবচেয়ে খারাপ সময়ে ধরা পড়ল — প্রকাশের পরে, অন্যের কম্পিউটারে। আর একবার প্রকাশিত version বদলানোও যায় না (অংশ ৬)। - src layout ফাঁদটি বন্ধ করে। package
src/-এর ভেতরে থাকায় মূল ফোল্ডারটি আর import-যোগ্য নয়। ইনস্টল না করলেModuleNotFoundError— আমরা নিজেরা চালিয়ে ঠিক এই বার্তাই পেয়েছি। তাই টেস্টকে বাধ্য হয়ে ইনস্টল করা কপিতে চলতে হয়, আর ব্যবহারকারী যা পাবেন, আপনিও তা-ই পরীক্ষা করেন।
এখন package-এর আসল কোড। নিচের ফাংশনটি src/taka_format/__init__.py-এর হৃদয় (সেখানে বাড়তি শুধু version পড়ার দুটি লাইন, অংশ ৬-এ দেখবেন)। লাখ-কোটির নিয়ম: শেষ তিন অঙ্ক একটি দল, তারপর বাঁ দিকে দুই অঙ্ক করে। প্রকাশের আগে package-এর আচরণ টেস্ট দিয়ে বেঁধে ফেলা জরুরি — নুসরাতের ঋণাত্মক সংখ্যার বাগটিও এখানে একটি টেস্ট:
def format_taka(amount: int) -> str:
"""1234567 -> "12,34,567": শেষে ৩ অঙ্ক, তারপর ২ অঙ্ক করে দল।"""
sign = "-" if amount < 0 else ""
digits = str(abs(amount))
head, tail = digits[:-3], digits[-3:]
groups = []
while head:
groups.insert(0, head[-2:])
head = head[:-2]
return sign + ",".join(groups + [tail])
tests = {0: "0", 999: "999", 1000: "1,000", 100000: "1,00,000",
1234567: "12,34,567", 10000000: "1,00,00,000", -5000: "-5,000"}
for n, want in tests.items():
got = format_taka(n)
assert got == want, f"{n}: পেলাম {got!r}, চাই {want!r}"
print(f"{n:>9} -> {got}")
print("৭টি টেস্টই পাস")চিহ্ন আলাদা করে রেখে (sign) শুধু অঙ্কগুলো নিয়ে কাজ — ঋণাত্মক সংখ্যার বাগ এভাবেই সারে। digits[-3:] শেষ তিন অঙ্ক; বাকি head থেকে লুপে শেষের দুই অঙ্ক করে কেটে সামনে বসানো হয়। ছোট সংখ্যায় (999) head ফাঁকা, তাই লুপ চলেই না। আউটপুট সাত লাইন (বাঁয়ের সংখ্যা :>9-এর কারণে ৯ ঘরে ডানদিক ঘেঁষা): 0 -> 0, 999 -> 999, 1000 -> 1,000, 100000 -> 1,00,000, 1234567 -> 12,34,567, 10000000 -> 1,00,00,000, -5000 -> -5,000, তারপর “৭টি টেস্টই পাস”। কোনো assert ব্যর্থ হলে প্রোগ্রাম AssertionError দিয়ে থামত। বাস্তব প্রজেক্টে এই টেস্টগুলো থাকে tests/ ফোল্ডারে, pytest দিয়ে চলে (পর্ব ৪২)।
নাম বাছার আগে PyPI দেখুন। PyPI-তে নাম আগে এলে আগে পাবেন, আর নাম তুলনার সময় বড়-ছোট হাতের অক্ষর, -, _ আর . একই ধরা হয় (PEP 503-এর normalization) — তাই Taka_Format আর taka-format একই নাম। আমাদের build-এর ফাইলের নামেও তাই taka_format-0.1.0... দেখবেন, যদিও প্রজেক্টের নাম taka-format.
৩ · pyproject.toml — পরিচয়পত্রের প্রতিটি ঘর
পর্ব ২৩-এ একটি ছোট pyproject.toml দেখেছেন — নাম, version, dependency আর একটি কমান্ড। প্রকাশ করার মতো package-এ আরও কিছু ঘর লাগে, কারণ এগুলোই PyPI-র পাতায় দেখা যায়, আর ব্যবহারকারী ও টুল এগুলো পড়েই সিদ্ধান্ত নেয়। নিচেরটি আমাদের পরীক্ষিত taka-format-এর হুবহু ফাইল:
# pyproject.toml
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
[project]
name = "taka-format"
version = "0.1.0"
description = "Format numbers in Bangladeshi lakh/crore style"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Nusrat Jahan", email = "nusrat@example.com" }]
dependencies = []
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
[project.optional-dependencies]
cli = ["rich>=13"]
[project.urls]
Source = "https://github.com/example/taka-format"
[project.scripts]
taka = "taka_format.cli:main"| ঘর | কী বলে | খেয়াল রাখুন |
|---|---|---|
[build-system] | কোন backend build করবে, আর তার জন্য কী ইনস্টল লাগবে | backend-এর ন্যূনতম version দিন, যদি নতুন কোনো সুবিধা লাগে (এখানে license-এর নতুন নিয়ম) |
name, version | PyPI-তে পরিচয় | একই নাম + version দ্বিতীয়বার আপলোড করা যায় না |
description, readme | এক লাইনের সারাংশ আর PyPI-র পুরো পাতা | README-ই আপনার package-এর দোকানের সাইনবোর্ড |
requires-python | কোন Python-এ চলে | পুরোনো Python-এ pip এই version বাদ দিয়ে আগের উপযুক্ত version খোঁজে |
license, license-files | ব্যবহারের অনুমতি, SPDX ভাষায় | নিচে বিস্তারিত — এখানেই AI সবচেয়ে বেশি পুরোনো রূপ লেখে |
dependencies | package চলতে যা লাগবেই | library-তে ঢিলে পরিসর, == নয় (অংশ ৬) |
optional-dependencies | ঐচ্ছিক “extra” | pip install "taka-format[cli]" দিলে rich-ও আসে |
classifiers | PyPI-র খোঁজ-ফিল্টারের মানক লেবেল | তালিকার বাইরের লেবেল PyPI আপলোডের সময় ফিরিয়ে দেয় |
[project.urls] | source, ডকুমেন্টেশন, বাগ-রিপোর্টের ঠিকানা | ব্যবহারকারী এগুলো দেখেই বিশ্বাস করেন (পর্ব ২৩-এর যাচাই) |
[project.scripts] | terminal-কমান্ড → ফাংশন | ইনস্টলের সময় pip একটি ছোট চালানোর ফাইল বানায় |
License — SPDX expression (PEP 639)
অনেক বছর ধরে license লেখার কোনো একক নিয়ম ছিল না: কেউ লিখতেন license = {text = "MIT"}, কেউ classifier দিতেন "License :: OSI Approved :: MIT License", কেউ দুটোই — আর মাঝে মাঝে দুটি আলাদা কথা বলত। PEP 639 (২০২৪-২৫ সালে গৃহীত) একটি পরিষ্কার নিয়ম আনে: license-এ একটি SPDX license expression, যেমন "MIT", "Apache-2.0" বা দুটির যেকোনো একটি বেছে নেওয়ার জন্য "MIT OR Apache-2.0". আর license-files-এ license-এর আসল লেখার ফাইল, যা wheel-এর ভেতরে যায় — আমাদের wheel-এ সেটি বসেছে .dist-info/licenses/LICENSE-এ।
- backend-কে নতুন হতে হবে: hatchling 1.27 আর setuptools 77 থেকে এই রূপ সমর্থিত। তাই আমাদের ফাইলে
hatchling>=1.27. আমাদের build-এর METADATA-তে এসেছেLicense-Expression: MIT. - পুরোনো রূপ এড়ান:
license = {text = ...}টেবিল-রূপ এখন বাতিলের পথে, আরLicense ::classifier SPDX expression-এর সাথে একসাথে দিলে নতুন টুল error দিতে পারে। একটিই সত্যের উৎস —license. - license না দিলে: আইনের চোখে কোড ব্যবহারের অনুমতি অস্পষ্ট থাকে, আর সাবধানী কোম্পানি সেই package ব্যবহারই করে না। ছোট utility-তে MIT বা Apache-2.0 সবচেয়ে প্রচলিত; কোনটি বাছবেন, সেটি আইনি সিদ্ধান্ত — কোম্পানির কোড হলে আগে অনুমতি নিন।
Classifier, extra আর এমন জিনিস যা প্রকাশ হয় না
Classifier হলো PyPI-র নির্দিষ্ট তালিকা থেকে নেওয়া লেবেল — কোন Python, কোন OS, প্রজেক্টের পরিপক্কতা (Development Status :: 4 - Beta). এগুলো খোঁজায় সাহায্য করে, কিন্তু ইনস্টলের নিয়ম বদলায় না — Python version-এর আসল নিয়ন্ত্রণ requires-python. একটি কাজের কৌশল: Private :: দিয়ে শুরু হওয়া যেকোনো classifier (যেমন "Private :: Do Not Upload") PyPI ইচ্ছাকৃতভাবে ফিরিয়ে দেয়। কোম্পানির ভেতরের package-এ এটি দিলে ভুল করে পাবলিক PyPI-তে আপলোড হওয়া আটকায়।
Optional dependencies (extra) দিয়ে ভারী বা বিশেষ dependency ঐচ্ছিক রাখা যায়। আমাদের METADATA-তে এটি দেখা গেছে এভাবে: Provides-Extra: cli আর Requires-Dist: rich>=13; extra == 'cli' — অর্থাৎ rich আসবে শুধু [cli] চাইলে। আর যেসব dependency শুধু ডেভেলপারের লাগে (pytest, ruff), সেগুলো ব্যবহারকারীর কাছে যাওয়া উচিত নয়। এর জন্য নতুন মানক [dependency-groups] অংশ (PEP 735, ২০২৪) — এটি wheel-এর metadata-তে যায় না; uv এটি পড়ে, আর pip 25.1 থেকে --group দিয়ে। টুলের নিজস্ব সেটিং থাকে [tool.ruff], [tool.pytest.ini_options]-এর মতো [tool.*] অংশে (পর্ব ৪২, ৪৭)।
৪ · Build backend বাছাই — frontend আর backend-এর ভাগাভাগি
package বানানোর কাজে দুই ধরনের টুল আছে, আর এদের ভাগটা বুঝলে বাকি সব সহজ হয়ে যায়। Build frontend হলো যে টুল আপনি চালান — python -m build, pip বা uv build. এটি নিজে কিছু বানায় না। সে [build-system] পড়ে, একটি অস্থায়ী, আলাদা পরিবেশ তৈরি করে, সেখানে backend ইনস্টল করে, তারপর PEP 517-এর মানক ফাংশন (build_wheel, build_sdist) ডেকে backend-কে বলে “বানাও”। এই আলাদা পরিবেশকে বলে build isolation — আপনার venv-এ কী আছে না আছে, build তার ওপর নির্ভর করে না, তাই সবার কম্পিউটারে একই ফল।
Backend হলো যে আসলে ফাইল বানায়। কোনটি বাছবেন? সত্যি কথা হলো, বিশুদ্ধ Python package-এর জন্য সবগুলোই ভালো, আর একই [project] অংশ সবাই পড়ে — backend বদলাতে সাধারণত শুধু [build-system]-এর দুটি লাইন আর backend-নির্ভর [tool.*] সেটিং বদলাতে হয়।
| Backend | build-backend | বৈশিষ্ট্য | কখন ভাববেন |
|---|---|---|---|
| setuptools | setuptools.build_meta | সবচেয়ে পুরোনো (২০০৪ থেকে), বিশাল ইকোসিস্টেম, C extension build করতে পারে; ৬১.০ (২০২২) থেকে [project] পড়ে | পুরোনো প্রজেক্ট, C extension, setup.py থেকে ধীরে সরে আসা |
| hatchling | hatchling.build | Hatch প্রজেক্টের backend, PyPA-র ছাতার নিচে; ভালো ডিফল্ট, plugin দিয়ে বাড়ানো যায় | নতুন বিশুদ্ধ Python প্রজেক্ট (এই পর্বের উদাহরণ) |
| flit-core | flit_core.buildapi | অত্যন্ত ছোট ও সরল, শুধু বিশুদ্ধ Python, কনফিগ প্রায় লাগে না | ছোট library, কম নির্ভরতা চাইলে |
| pdm-backend | pdm.backend | PDM প্রজেক্টের backend, PDM ছাড়াও একা ব্যবহার করা যায় | PDM ব্যবহারকারী দল |
| uv_build | uv_build | Astral-এর uv-এর সাথে আসা দ্রুত backend (২০২৫ সালে স্থিতিশীল), শুধু বিশুদ্ধ Python | uv দিয়ে চলা প্রজেক্ট; uv-এর নতুন version-এ uv init --package এটিই বসায় |
ভেতরে C, C++ বা Rust কোড থাকলে আলাদা জাতের backend লাগে — যেমন CMake-ভিত্তিক scikit-build-core বা Rust-এর জন্য maturin. version নম্বরগুলো এই লেখার সময়ের; backend দ্রুত বদলায়, তাই নতুন প্রজেক্ট শুরুর সময় সংশ্লিষ্ট ডকুমেন্টেশন একবার দেখে নিন।
setup.py কি মরে গেছে? না, তবে তার ভূমিকা বদলেছে। python setup.py install বা setup.py sdist upload-এর মতো সরাসরি কমান্ড বহু আগেই বাতিলের পথে। setuptools এখনো setup.py পড়তে পারে — জটিল C build-এর কিছু কাজের জন্য সেটি রাখা যায় — কিন্তু নাম, version, dependency-র মতো পরিচয় এখন [project]-এ লেখাই মানক। AI যদি নতুন প্রজেক্টের জন্য শুধু setup.py লিখে দেয়, বুঝবেন উত্তরটি পুরোনো যুগের (অংশ ১০)।
৫ · Build করা — আর wheel-এর ভেতরে আসলে কী থাকে
প্রজেক্টের মূল ফোল্ডারে একটি আলাদা venv-এ build নামের frontend ইনস্টল করে একটি কমান্ড দিন:
python -m venv .venv
.venv\Scripts\Activate.ps1 # Mac/Linux: source .venv/bin/activate
python -m pip install build
python -m build # dist/ ফোল্ডারে sdist আর wheelপ্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের নিজের চালানো থেকে, build 1.6.1 দিয়ে:
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- hatchling>=1.27
* Installed build dependency versions:
- hatchling==1.32.4
* Building sdist...
* Building wheel from sdist
...
Successfully built taka_format-0.1.0.tar.gz and taka_format-0.1.0-py3-none-any.whl
লক্ষ করুন Building wheel from sdist: build প্রথমে sdist বানায়, তারপর সেই sdist খুলে তা থেকে wheel বানায়। এটি একটি চমৎকার পরীক্ষা — sdist-এ কোনো দরকারি ফাইল বাদ পড়লে wheel build এখানেই ব্যর্থ হবে, ব্যবহারকারীর কম্পিউটারে নয়। পুরো যাত্রাটি, ইনস্টল পর্যন্ত:
- পড়া
[build-system]frontend - Backend
build_sdisthatchling - wheel
zip + dist-infodist/ - pip install
site-packagesখুলে বসানো - কমান্ড
takaScripts/bin-এ
[build-system] requires = ["hatchling>=1.27"] build-backend = "hatchling.build" → অস্থায়ী venv বানাও, hatchling ইনস্টল করো
taka_format-0.1.0/pyproject.toml taka_format-0.1.0/README.md taka_format-0.1.0/LICENSE taka_format-0.1.0/src/taka_format/__init__.py taka_format-0.1.0/src/taka_format/cli.py taka_format-0.1.0/src/taka_format/units.txt taka_format-0.1.0/tests/test_format.py taka_format-0.1.0/PKG-INFO ← metadata
416 taka_format/__init__.py
357 taka_format/cli.py
36 taka_format/units.txt
575 taka_format-0.1.0.dist-info/METADATA
87 taka_format-0.1.0.dist-info/WHEEL
46 taka_format-0.1.0.dist-info/entry_points.txt
141 taka_format-0.1.0.dist-info/licenses/LICENSE
645 taka_format-0.1.0.dist-info/RECORD
$ python -m pip install dist/taka_format-0.1.0-py3-none-any.whl Processing .\dist\taka_format-0.1.0-py3-none-any.whl Installing collected packages: taka-format Successfully installed taka-format-0.1.0 site-packages/taka_format/ + taka_format-0.1.0.dist-info/
$ taka 1234567 -5000 999 ৳ 12,34,567 ৳ -5,000 ৳ 999 Windows-এ .venv\Scripts\taka.exe, Linux/Mac-এ .venv/bin/taka
- frontend pyproject পড়ে।
python -m buildপ্রথমে শুধু[build-system]দেখে: কোন backend, তার কী লাগবে। তারপর একটি অস্থায়ী, পরিষ্কার পরিবেশ বানিয়ে সেখানে hatchling ইনস্টল করে। আপনার venv-এ hatchling না থাকলেও চলে — build isolation. - backend sdist বানায়। frontend backend-এর
build_sdistডাকে। hatchling[project]পড়ে metadata লেখে (PKG-INFO), আর source, README, LICENSE, tests একটি.tar.gz-এ ভরে। sdist হলো “রান্নার আগের উপকরণ” — যাঁর কম্পিউটারে মিলের wheel নেই, তিনি এটি থেকে নিজে build করবেন। - wheel — একটি zip. sdist থেকে
build_wheel. wheel-এ tests বা pyproject থাকে না; থাকে শুধু ইনস্টলের জিনিস: package-এর ফাইল আর.dist-infoফোল্ডার —METADATA(নাম, version, dependency),WHEEL(কে বানাল, কোন ট্যাগ),entry_points.txt(কমান্ড), license আরRECORD(প্রতিটি ফাইলের hash). - pip খুলে বসায়। ইনস্টল মানে মূলত unzip: ফাইলগুলো চালু venv-এর site-packages-এ, পাশে
.dist-info, আর pip নিজে যোগ করেINSTALLERফাইল (ভেতরে লেখাpip). কোনো কোড চালানো হয় না — wheel-এর বড় সুবিধা।pip uninstallপরেRECORDদেখে ঠিক এই ফাইলগুলোই মোছে। - কমান্ড তৈরি।
entry_points.txt-এ লেখাtaka = taka_format.cli:mainদেখে pip venv-এরScripts(Windows-এtaka.exe) বাbinফোল্ডারে একটি ছোট লঞ্চার বানায়, যাmain()ডাকে। venv activate থাকলে শুধুtakaলিখলেই চলে — আমাদের নতুন venv-এ ঠিক এই আউটপুট এসেছে।
wheel খুলে দেখা — শুধু standard library দিয়ে
wheel যেহেতু সাধারণ zip, Python-এর zipfile দিয়েই খোলা যায়, আর METADATA ফাইলটি ইমেইলের হেডারের মতো “নাম: মান” ফরম্যাটে লেখা — তাই email.parser দিয়ে পড়া যায়। এই পাতায় আসল ফাইল নেই, তাই নিচের কোড মেমরিতে একটি ছোট “wheel” বানিয়ে তারপর সেটি পড়ে:
import io
import zipfile
from email.parser import Parser
DI = "taka_format-0.1.0.dist-info"
buf = io.BytesIO() # ডিস্কে নয়, মেমরিতে একটি "wheel"
with zipfile.ZipFile(buf, "w") as whl:
whl.writestr("taka_format/__init__.py", "def format_taka(n): ...\n")
whl.writestr(f"{DI}/METADATA", "Metadata-Version: 2.4\nName: taka-format\n"
"Version: 0.1.0\nLicense-Expression: MIT\nRequires-Python: >=3.10\n")
whl.writestr(f"{DI}/entry_points.txt", "[console_scripts]\ntaka = taka_format.cli:main\n")
with zipfile.ZipFile(buf) as whl:
print(*whl.namelist(), sep="\n")
meta = Parser().parsestr(whl.read(f"{DI}/METADATA").decode())
print("নাম:", meta["Name"], "| version:", meta["Version"], "| license:", meta["License-Expression"])
name, ver, py, abi, plat = "taka_format-0.1.0-py3-none-any.whl".removesuffix(".whl").split("-")
print(f"Python ট্যাগ={py}, ABI={abi}, প্ল্যাটফর্ম={plat}")প্রথম অংশ io.BytesIO-তে (মেমরির একটি “ফাইল”) তিনটি ফাইলসহ একটি zip লেখে — ডিস্কে কিছুই যায় না। দ্বিতীয় অংশ সেটি আবার খুলে তিনটি নাম ছাপে: taka_format/__init__.py, taka_format-0.1.0.dist-info/METADATA, taka_format-0.1.0.dist-info/entry_points.txt. তারপর METADATA পড়ে লাইন আসে নাম: taka-format | version: 0.1.0 | license: MIT. শেষ লাইনে ফাইলের নাম ভেঙে Python ট্যাগ=py3, ABI=none, প্ল্যাটফর্ম=any. নিজের কম্পিউটারে আসল wheel-এ একই কাজ করতে শুধু buf-এর জায়গায় ফাইলের পথ দিন — আমরা ঠিক এভাবেই ওপরের তালিকা পেয়েছি। আসল hatchling 1.32.4 লিখেছে Metadata-Version: 2.5; এখানে 2.4 রাখা হয়েছে, কারণ License-Expression ঘরটি সেই version-এ এসেছে।
wheel-এর নামের ট্যাগ — কে কোথায় চলবে
wheel-এর নামের গঠন: {নাম}-{version}-{python}-{abi}-{প্ল্যাটফর্ম}.whl. এই শেষ তিনটি অংশকে বলে wheel tag, আর pip এগুলো মিলিয়েই ঠিক করে কোন ফাইল আপনার কম্পিউটারে চলবে:
| ট্যাগ | মানে |
|---|---|
py3-none-any | যেকোনো Python 3, কোনো compile করা অংশ নেই, যেকোনো OS — pure-Python wheel. একটি ফাইলই সবার জন্য। |
cp313-cp313-win_amd64 | শুধু CPython 3.13, তার নিজস্ব binary interface, 64-বিট Windows — C extension-সহ platform wheel. |
cp313-cp313-manylinux_2_28_x86_64 | অনেক Linux distribution-এ চলে এমন মানক (glibc 2.28 বা নতুন)। |
cp310-abi3-macosx_11_0_arm64 | CPython 3.10 বা নতুন সবগুলোতে চলে (stable ABI), Apple Silicon Mac. |
platform wheel বানানো ঝামেলার — প্রতিটি OS, প্রসেসর আর Python version-এর জন্য আলাদা। বড় প্রজেক্টগুলো এজন্য cibuildwheel-এর মতো টুল দিয়ে CI-তে একসাথে ডজনখানেক wheel বানায়। আমাদের taka-format-এর মতো বিশুদ্ধ Python-এ সেই চিন্তা নেই।
python -m pip install twine
twine check dist/* # README ঠিকমতো দেখাবে কি না, metadata বৈধ কি নাপ্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের চালানোয় twine 7.0.0:
Checking dist/taka_format-0.1.0-py3-none-any.whl: PASSED
Checking dist/taka_format-0.1.0.tar.gz: PASSED
৬ · Version — PEP 440, pre-release আর এক জায়গায় version
পর্ব ২৩-এ semantic versioning-এর প্রথা দেখেছেন: MAJOR.MINOR.PATCH. ব্যবহারকারী হিসেবে সেটি ছিল “পড়ার” বিষয়; package-এর লেখক হিসেবে এখন এটি আপনার প্রতিশ্রুতি। taka-format-এ format_taka()-এর নাম বদলালে MAJOR বাড়াতে হবে, নতুন ফাংশন যোগ করলে MINOR, শুধু বাগ সারালে PATCH. 0.x version-এর প্রচলিত মানে “এখনো স্থির নয়, MINOR-এও বড় বদল হতে পারে” — তাই প্রথম প্রকাশ প্রায়ই 0.1.0.
Python-এ কোন version লেখা বৈধ আর কোনটি কার চেয়ে বড়, তার আসল নিয়ম PEP 440-এ। semver-এর চেয়ে এটি একটু আলাদা, বিশেষ করে pre-release-এ:
1.0.0.dev1 < 1.0.0a1 < 1.0.0b2 < 1.0.0rc1 < 1.0.0 < 1.0.0.post1.devN— ডেভেলপমেন্ট চলাকালীন build;aN/bN/rcN— alpha, beta, release candidate. pip এগুলো নিজে থেকে বাছে না:pip install taka-formatসবচেয়ে নতুন স্থিতিশীল version নেয়। pre-release চাইলে--preদিতে হয়, বা শর্তেই pre-release লিখতে হয় (taka-format==1.0.0rc1)..postN— কোড না বদলে শুধু প্রকাশে ভুল সারানো (যেমন README). কোডের বাগ সারালে PATCH বাড়ান, post নয়।- একবার আপলোড, চিরকাল সেই ফাইল: PyPI একই নাম-version-এর ফাইল দ্বিতীয়বার নেয় না — মুছে দিলেও না। ভুল প্রকাশ হলে নতুন version দিন। খারাপ version-কে “yank” (PEP 592) করা যায়: ফাইল থাকে, কিন্তু শর্ত ছাড়া ইনস্টলে pip সেটি আর বাছে না; যাঁরা
==দিয়ে হুবহু চেয়েছেন, শুধু তাঁরা পান।
version এক জায়গায় — importlib.metadata
নতুনরা প্রায়ই version দুই জায়গায় লেখেন — pyproject.toml-এ আর __init__.py-এর __version__-এ। তারপর একটিতে বদলান, অন্যটিতে ভুলে যান। সমাধান: version লিখুন শুধু pyproject.toml-এ, আর কোডে ইনস্টল করা metadata থেকে পড়ুন। আমাদের __init__.py-এর শুরু:
from importlib.metadata import version
__version__ = version("taka-format") # distribution-এর নাম, import-এর নয়importlib.metadata (Python 3.8 থেকে standard library-তে) site-packages-এর .dist-info/METADATA পড়ে। আমাদের নতুন venv-এ version('taka-format') দিয়েছে 0.1.0, আর একই module থেকে metadata('taka-format')['License-Expression'] দিয়েছে MIT. একটি শর্ত: package ইনস্টল করা থাকতে হবে (editable হলেও চলে) — src layout-এ এমনিতেই তা থাকে। বিকল্প পথও আছে: backend-কে dynamic = ["version"] দিয়ে বলা যায় version কোডের ফাইল থেকে বা git tag থেকে নিতে (setuptools-scm, hatch-vcs) — তাহলে git tag v0.2.0 দিলেই version ঠিক হয়।
Library-তে পরিসর, অ্যাপে pin
পর্ব ২৩-এর পরামর্শ ছিল: চালানোর উপযোগী অ্যাপে হুবহু pin. library-র ক্ষেত্রে উল্টো। ধরুন taka-format লিখল requests==2.31.0, আর ব্যবহারকারীর আরেকটি library চায় requests>=2.32. দুটি একসাথে ইনস্টল অসম্ভব — আপনার একটি অকারণ সংখ্যার জন্য অন্যের পুরো প্রজেক্ট আটকে গেল। library জানে না সে কার পাশে বসবে, তাই সে শুধু বলে “আমার কমপক্ষে কী লাগে” (requests>=2.31), আর জানা ভাঙন থাকলে সীমা (<3). হুবহু version ঠিক করার দায়িত্ব শেষ অ্যাপের, তার lock file-এ। ওপরের সীমা (upper bound) নিয়ে অভিজ্ঞদের মধ্যেও বিতর্ক আছে: অকারণ <3 ভবিষ্যতে সমাধানহীন জট বানাতে পারে, আবার না দিলে নতুন MAJOR ভাঙতে পারে — জানা কারণ ছাড়া কড়া সীমা দেবেন না।
এবার ব্যবহারকারীর দিক থেকে দেখুন — pip একটি পরিসর পেলে উপলব্ধ version-গুলো থেকে কীভাবে একটি বাছে (তালিকাটি বোঝানোর জন্য বানানো, requests-এর আসল release-তালিকা নয়):
1উপলব্ধ সব version-এর তালিকা2শর্ত ১: >=2.313শর্ত ২: <34pre-release বাদ (ডিফল্ট)5Requires-Python ও wheel মেলানো6সবচেয়ে বড়টি নেওয়া
| বাকি আছে | |
| এইমাত্র বাদ |
- তালিকা। pip index-এর পাতা থেকে package-এর সব version আর ফাইলের তালিকা আনে। এখানে সাতটি — পুরোনো স্থিতিশীল, নতুন স্থিতিশীল, একটি release candidate আর ভবিষ্যতের 3.x.
>=2.31— নিম্নসীমা। 2.28.2 বাদ। এটিই library-র লেখকের আসল বার্তা: “2.31-এর আগের কোনো সুবিধা আমার যথেষ্ট নয়”।<3— ঊর্ধ্বসীমা। 3.0.0 বাদ, আর PEP 440-এর একটি সূক্ষ্ম নিয়মে 3.0.0b1-ও:<33-এর pre-release-কেও বাইরে রাখে, যদিও সংখ্যায় 3.0.0b1 < 3.0.0. কমা মানে “এবং” — দুটি শর্তই মানতে হবে।- pre-release বাদ। 2.33.0rc1 পরিসরের ভেতরে হলেও pre-release, আর কেউ
--preচায়নি। তাই বাদ। কোনো স্থিতিশীল version না থাকলে তবেই pip pre-release বিবেচনা করে। - পরিবেশ মেলানো। প্রতিটি প্রার্থীর
Requires-Pythonআর wheel tag (অংশ ৫) আপনার Python ও OS-এর সাথে মেলানো হয়। না মিললে সেটিও বাদ যেত — এজন্যই অংশ ৩-এrequires-pythonসঠিক রাখা জরুরি। - সবচেয়ে বড়টি। বাকিদের মধ্যে সবচেয়ে নতুন 2.32.3. বড় প্রজেক্টে resolver এই কাজ একসাথে সব dependency-র জন্য করে, আর কোনো বাছাই অন্যের শর্ত ভাঙলে পিছিয়ে এসে পরেরটি চেষ্টা করে।
৭ · Package data, editable install আর নতুন venv-এ পরীক্ষা
taka কমান্ড কোনো সংখ্যা না পেলে units.txt ফাইল থেকে লাখ-কোটির একটি ছোট সারণি দেখায়। .py নয় এমন ফাইল — JSON, CSV, টেমপ্লেট, ছবি — যা package-এর সাথে যায়, তাকে বলে package data. এটি পড়ার সঠিক উপায় importlib.resources:
# src/taka_format/cli.py (অংশ)
from importlib.resources import files
text = files("taka_format").joinpath("units.txt").read_text(encoding="utf-8")কেন open("units.txt") নয়? কারণ সেটি বর্তমান ফোল্ডারে খোঁজে (পর্ব ২৬-এ দেখবেন), আর ব্যবহারকারী যেকোনো ফোল্ডার থেকে taka চালাতে পারেন। files("taka_format") package যেখানে ইনস্টল হয়েছে সেখান থেকেই খোঁজে — site-packages-এ হোক, editable হোক, এমনকি zip-এর ভেতরে হোক। আরেকটি কথা: ডেটা-ফাইল wheel-এ ঢুকবে কি না, তা backend-এর নিয়মে। hatchling package-ফোল্ডারের ফাইলগুলো ডিফল্টভাবে নেয় (আমাদের wheel-এ units.txt এসেছে), setuptools-এ package-data বা include-package-data সেটিং দেখতে হয়। নিয়ম মুখস্থ করার চেয়ে নিরাপদ অভ্যাস: প্রতিবার wheel খুলে দেখুন (অংশ ৫-এর কোড)।
Editable install — কাজ চলাকালীন
পর্ব ২৩-এ pip install -e . দেখেছেন। package-এর লেখক হিসেবে এটিই আপনার দৈনন্দিন অবস্থা: কোড বদলান, সঙ্গে সঙ্গে পরীক্ষা করুন, আবার ইনস্টল ছাড়াই। আমাদের venv-এ editable ইনস্টলের পর site-packages-এ কোডের কপি নয়, এসেছে _editable_impl_taka_format.pth নামে একটি ছোট নির্দেশক ফাইল আর .dist-info; আর taka_format.__file__ দেখিয়েছে সরাসরি src/taka_format/__init__.py. তবে মনে রাখুন: pyproject.toml বদলালে — নতুন dependency, নতুন কমান্ড, নতুন version — আবার pip install -e . দিতে হয়, কারণ সেগুলো metadata-য় থাকে, কোডে নয়।
প্রকাশের আগে শেষ পরীক্ষা — একদম নতুন venv
editable install আপনার ফোল্ডার দেখায়, ব্যবহারকারী পাবেন wheel. তাই প্রকাশের আগে wheel-টিকে একটি খালি venv-এ ইনস্টল করে ব্যবহারকারীর মতো চালান — প্রজেক্টের ফোল্ডারের বাইরে থেকে:
cd .. # প্রজেক্টের বাইরে
python -m venv try-env
try-env\Scripts\Activate.ps1 # Mac/Linux: source try-env/bin/activate
python -m pip install taka-format/dist/taka_format-0.1.0-py3-none-any.whl
taka 1234567 10000000
taka
python -c "from importlib.metadata import version; print(version('taka-format'))"প্রত্যাশিত আউটপুট (সংক্ষেপে) — আমাদের চালানো থেকে:
Successfully installed taka-format-0.1.0
৳ 12,34,567
৳ 1,00,00,000
taka-format 0.1.0
lakh = 1,00,000
crore = 1,00,00,000
0.1.0
দ্বিতীয় taka (কোনো সংখ্যা ছাড়া) version আর units.txt-এর লেখা দেখিয়েছে — অর্থাৎ package data সত্যিই wheel-এ গেছে আর importlib.resources সেটি খুঁজে পেয়েছে। এরপর এই venv-এ pytest ইনস্টল করে tests/ চালালে টেস্টগুলো চলবে ইনস্টল করা wheel-এর ওপর (পর্ব ৪২)। Windows-এর পুরোনো console-এ ৳ চিহ্ন ভাঙা দেখালে সেটি terminal-এর encoding-এর ব্যাপার, package-এর নয়।
৮ · প্রকাশ — TestPyPI, token আর Trusted Publishing
wheel পরীক্ষিত, এবার পৃথিবীর হাতে তুলে দেওয়ার পালা। প্রথমবারের প্রস্তুতি:
- দুটি অ্যাকাউন্ট:
pypi.orgআর TestPyPI (test.pypi.org) — দুটি সম্পূর্ণ আলাদা সাইট, আলাদা অ্যাকাউন্ট। TestPyPI অনুশীলনের মাঠ: এখানে ভুল করলে কারও ক্ষতি নেই, আর মাঝে মাঝে সেখানকার ডেটা মুছেও ফেলা হয়। - 2FA চালু: ২০২৪ সালের শুরু থেকে PyPI-তে আপলোড করতে সব অ্যাকাউন্টে 2FA বাধ্যতামূলক — authenticator অ্যাপ বা security key. recovery code নিরাপদে রেখে দিন।
- নাম যাচাই: দুটি সাইটেই নামটি খালি কি না দেখুন (অংশ ২-এর normalization মনে রেখে)।
প্রথম পথ — API token আর twine
পাসওয়ার্ড দিয়ে আপলোড PyPI বহু আগেই বন্ধ করেছে। এর বদলে API token: অ্যাকাউন্টের সেটিং থেকে বানানো একটি লম্বা গোপন লেখা, যা pypi- দিয়ে শুরু হয়। প্রথম আপলোডের জন্য পুরো অ্যাকাউন্টের token লাগে; প্রজেক্ট তৈরি হয়ে গেলে শুধু ওই প্রজেক্টে সীমাবদ্ধ (project-scoped) token বানিয়ে পুরোনোটি মুছে দিন — চুরি হলেও ক্ষতি এক প্রজেক্টে সীমিত থাকে।
# ১. আগে TestPyPI-তে (username চাইলে লিখুন __token__, password-এ token)
twine upload --repository testpypi dist/*
# ২. নতুন venv-এ TestPyPI থেকে ইনস্টল করে পরীক্ষা
python -m pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ taka-format
# ৩. সব ঠিক থাকলে আসল PyPI-তে
twine upload dist/*
# uv ব্যবহার করলে একই কাজ: uv build তারপর uv publishদ্বিতীয় ধাপের --extra-index-url এখানে দরকার, কারণ আপনার package-এর dependency-গুলো TestPyPI-তে নাও থাকতে পারে। তবে এটি শুধু পরীক্ষার জন্য — দুটি index একসাথে দেওয়া কেন কোম্পানির কাজে বিপজ্জনক, তা অংশ ৯-এ দেখবেন। token কোথায় রাখবেন? প্রতিবার হাতে পেস্ট, বা OS-এর keyring-এ (twine এটি সমর্থন করে), বা নিজের হোম ফোল্ডারের .pypirc ফাইলে — কিন্তু কখনো প্রজেক্টের ফোল্ডারে বা git-এ নয়। GitHub-এ পাবলিক কোডে ফাঁস হওয়া PyPI token GitHub-এর secret scanning-এর মাধ্যমে PyPI-কে জানানো হয়, আর PyPI সেটি বাতিল করে দেয় — তবু সেই ভরসায় থাকবেন না।
আরও ভালো পথ — Trusted Publishing
token-এর মূল সমস্যা: এটি দীর্ঘস্থায়ী একটি চাবি। CI-তে (যেমন GitHub Actions) স্বয়ংক্রিয় প্রকাশের জন্য সেটি repository-র secret-এ রাখতে হয়, আর যেকোনো জায়গায় জমা থাকা চাবি একদিন চুরি হতে পারে। ২০২৩ সালে PyPI-তে আসা trusted publishing-এ জমা রাখার মতো কোনো চাবিই থাকে না। আপনি PyPI-কে আগে থেকে বলে রাখেন: “এই GitHub repository-র এই workflow ফাইল থেকে আসা অনুরোধ আমার প্রজেক্টে আপলোড করতে পারবে”। তারপর প্রতিবার প্রকাশের সময় GitHub একটি স্বল্পমেয়াদি, স্বাক্ষর করা পরিচয়পত্র দেয় — OIDC token — আর PyPI সেটি যাচাই করে কয়েক মিনিটের জন্য একটি আপলোড-token দেয়। ধাপে ধাপে:
- আগে থেকে চুক্তি
PyPI → Publishingrepo + workflow - Release
v0.1.1workflow শুরু - OIDC token
id-token: writeGitHub স্বাক্ষর করে - PyPI যাচাই
স্বাক্ষর + দাবিমিললে স্বল্পমেয়াদি token - আপলোড
dist/*token মেয়াদোত্তীর্ণ
Owner: nusrat-dev Repository: taka-format Workflow: publish.yml Environment: pypi (ঐচ্ছিক, কিন্তু ভালো) নতুন প্রজেক্টে "pending publisher" হিসেবে আগেই যোগ করা যায়
on:
release:
types: [published]
jobs:
pypi:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write ← OIDC token চাওয়ার অনুমতি
steps:
- uses: actions/checkout@v4
- run: pipx run build
স্বাক্ষর GitHub-এর? ✓
repository = nusrat-dev/taka-format ✓
workflow = publish.yml ✓
environment = pypi ✓
→ শুধু taka-format-এর জন্য স্বল্পমেয়াদি API token
- uses: pypa/gh-action-pypi-publish@release/v1 token বিনিময় + আপলোড এই action-ই করে; কয়েক মিনিট পরে token নিজেই অকেজো
- আগে থেকে বিশ্বাসের চুক্তি। PyPI-তে প্রজেক্টের সেটিং-এ একটি “trusted publisher” যোগ করেন: GitHub-এর মালিক, repository, workflow ফাইলের নাম, আর ঐচ্ছিকভাবে environment-এর নাম। প্রজেক্ট এখনো না থাকলে “pending publisher” হিসেবে আগেই যোগ করা যায় — প্রথম আপলোডেই প্রজেক্ট তৈরি হবে। কোথাও কোনো token কপি হলো না।
- Release দিলে workflow শুরু। নুসরাত GitHub-এ
v0.1.1release প্রকাশ করলেন, আরpublish.ymlচলতে শুরু করল।environment: pypiদিলে GitHub-এ ওই environment-এ বাড়তি সুরক্ষা বসানো যায় — যেমন প্রকাশের আগে কারও হাতে অনুমোদন। - OIDC token চাওয়া।
permissions: id-token: writeথাকলেই job GitHub-এর কাছে একটি OIDC token চাইতে পারে। এটি GitHub-এর স্বাক্ষর করা একটি ছোট পরিচয়পত্র, যার ভেতরে লেখা — কোন repository, কোন workflow, কোন branch বা tag, কোন environment. মেয়াদ খুব কম। repository-র secret-এ কিছুই রাখা হয়নি। - PyPI যাচাই করে। action টি token PyPI-কে পাঠায়। PyPI GitHub-এর প্রকাশ্য চাবি দিয়ে স্বাক্ষর যাচাই করে, তারপর ভেতরের দাবিগুলো ধাপ ১-এর চুক্তির সাথে মেলায়। সব মিললে দেয় শুধু এই প্রজেক্টের জন্য একটি স্বল্পমেয়াদি API token (কয়েক মিনিটের)। অন্য কোনো repository বা ফর্ক করা কপি থেকে চাইলে দাবি মিলবে না।
- আপলোড, তারপর চাবি নেই। সেই token দিয়ে
dist/-এর ফাইল আপলোড হয়, আর কিছুক্ষণ পর token নিজেই অকেজো। কেউ log থেকে চুরি করলেও লাভ নেই। PyPA-র এই action এখন প্রকাশিত ফাইলের জন্য ডিজিটাল attestation-ও (PEP 740) তৈরি করে — কোন workflow ফাইলটি বানিয়েছে তার স্বাক্ষর করা প্রমাণ। ভালো অভ্যাস: build একটি job-এ, আরid-tokenঅনুমতিসহ আপলোড আলাদা একটি ছোট job-এ।
GitHub ছাড়াও GitLab CI/CD, Google Cloud আর ActiveState থেকে trusted publishing সমর্থিত। আর uv publish-ও GitHub Actions-এ চললে trusted publishing ব্যবহার করতে পারে।
৯ · দল ও কোম্পানির জন্য — private index, project manager আর নিরাপত্তা
সব package পৃথিবীর জন্য নয়। ধরুন একটি বাংলাদেশি ফিনটেক কোম্পানির তিনটি দল একই bKash/Nagad পেমেন্ট-ক্লায়েন্ট কোড ব্যবহার করে, যার ভেতরে কোম্পানির নিজস্ব নিয়ম আছে। এটিকেও একই উপায়ে package বানানো হয় — শুধু প্রকাশ হয় কোম্পানির ভেতরের ভাণ্ডারে:
- ছোট দলের জন্য: সরাসরি git থেকে, নির্দিষ্ট tag দিয়ে —
pip install "git+https://github.com/acme/paykit@v1.4.0". index লাগে না, তবে প্রতি ইনস্টলে build হয়, আর tag বদলানো যায় বলে পুরোপুরি স্থির নয়। - প্রতিষ্ঠানের জন্য private package index: PyPI-র মতোই একটি সার্ভার, শুধু ভেতরের লোকদের জন্য — নিজে চালানো যায় (devpi, Sonatype Nexus, JFrog Artifactory), বা ক্লাউডের সেবা (AWS CodeArtifact, Azure Artifacts, Google Artifact Registry, GitLab-এর package registry). প্রকাশ হয় সেই একই
twine upload --repository-url ...বাuv publish --publish-url ...দিয়ে।
--extra-index-url-এর ফাঁদ: কোম্পানির package-এর জন্য pip install --extra-index-url https://pkgs.acme.local/simple/ paykit দিলে pip দুটি index-কেই সমান ধরে, আর যেখানে বড় version পায়, সেটিই নেয়। কেউ পাবলিক PyPI-তে paykit নামে বড় version নম্বরের ক্ষতিকর package রাখলে সেটিই আসবে — পর্ব ২৩-এর dependency confusion. প্রতিরোধ: একটিমাত্র --index-url দিন, যা private index — আর সেই index-ই PyPI-র package proxy করে আনবে; অথবা uv-এর মতো টুল ব্যবহার করুন, যার ডিফল্ট নিয়মে প্রথম যে index-এ নাম পাওয়া যায়, শুধু সেখান থেকেই নেয়; আর ভেতরের নামগুলো দরকার হলে পাবলিক PyPI-তে আগেই নিবন্ধন করে রাখুন। ভেতরের package-এ Private :: Do Not Upload classifier ভুল আপলোড আটকায় (অংশ ৩)।
uv, Poetry, PDM — পুরো প্রজেক্টের ব্যবস্থাপক
এই পর্বে আমরা ইচ্ছা করেই আলাদা আলাদা টুল (build, twine, pip) ব্যবহার করেছি, যাতে প্রতিটি ধাপ চোখে পড়ে। বাস্তবে অনেক দল একটি project manager ব্যবহার করে, যা venv, dependency, lock file, build আর publish এক জায়গায় সামলায়। মনে রাখার বিষয়: এরা সবাই শেষে একই মানক wheel আর sdist বানায়, আর একই PyPI-তে পাঠায় — তাই ব্যবহারকারী জানতেও পারেন না আপনি কোনটি ব্যবহার করেছেন।
⚡ uv
uv init --package src layout-সহ প্রজেক্ট বানায়, uv build আর uv publish বাকি কাজ করে। খুব দ্রুত, Astral কোম্পানির open source টুল।
📜 Poetry
২০১৮ থেকে জনপ্রিয়, নিজস্ব backend poetry-core. Poetry 2.0 (২০২৫) থেকে মানক [project] অংশ সমর্থন করে। poetry build, poetry publish.
🧩 PDM
মানক-ঘনিষ্ঠ project manager, pdm-backend-এর সাথে। pdm build, pdm publish.
প্রকাশকের নিরাপত্তা-দায়িত্ব
পর্ব ২৩-এ আপনি ছিলেন ব্যবহারকারী — অচেনা package থেকে নিজেকে বাঁচানো। এখন আপনি প্রকাশক, আর অন্যরা আপনাকে বিশ্বাস করে pip install দেবেন। Supply-chain attack-এর সবচেয়ে মূল্যবান লক্ষ্য হলো জনপ্রিয় package-এর প্রকাশকের অ্যাকাউন্ট, কারণ তখন আসল নামের package-ই ক্ষতিকর হয়ে যায়।
- অ্যাকাউন্ট: 2FA (বাধ্যতামূলক), ভালো হলে hardware security key; অ্যাকাউন্টের ইমেইলের ডোমেইন সচল রাখুন।
- token: সম্ভব হলে trusted publishing; না হলে project-scoped token, git-এ কখনো নয়,
.gitignore-এ.pypircআর.env. - যা প্রকাশ করছেন তা যাচাই: wheel আর sdist খুলে দেখুন — ভুল করে
.env, পাসওয়ার্ডের ফাইল বা কোম্পানির ডেটা ঢুকে যায়নি তো? sdist-এ ডিফল্টভাবে অনেক ফাইল যায়। - Typosquatting-এর উল্টো দিক: আপনার package জনপ্রিয় হলে কেউ কাছাকাছি বানানে (
taka-fromat) নকল ছাড়তে পারে। README-তে সঠিক ইনস্টল-কমান্ড স্পষ্ট লিখুন, সন্দেহজনক নকল দেখলে PyPI-তে রিপোর্ট করুন। - নিজের dependency কম রাখুন: আপনার প্রতিটি dependency আপনার ব্যবহারকারীরও dependency. পাঁচ লাইনের কাজের জন্য নতুন package নয়।
- wheel আর sdist-এর পার্থক্য? wheel আগে থেকে build করা zip, ইনস্টল মানে কপি; sdist সোর্স-আর্কাইভ, ইনস্টলের সময় build লাগে।
- build frontend আর backend কী? frontend (pip, build, uv) build শুরু করে ও পরিবেশ বানায়; backend (setuptools, hatchling)
[build-system]অনুযায়ী আসল ফাইল বানায় (PEP 517). - src layout কেন? ইনস্টল না করে package import হওয়া আটকায়, তাই টেস্ট চলে ইনস্টল করা কপিতে।
- library-তে
==দিয়ে pin করা খারাপ কেন? ব্যবহারকারীর অন্য package-এর সাথে অমীমাংসেয় version-জট তৈরি করে; library-তে পরিসর, অ্যাপে lock. - Trusted publishing কী? CI-র স্বল্পমেয়াদি OIDC পরিচয় দিয়ে PyPI-তে প্রকাশ, কোনো দীর্ঘস্থায়ী token জমা না রেখে।
১০ · AI-এর যুগে — AI-এর লেখা packaging কনফিগ যাচাই
“আমার কোডকে PyPI-তে দেওয়ার মতো package বানিয়ে দাও” — AI কয়েক সেকেন্ডে পুরো কনফিগ লিখে দেয়। সমস্যা হলো, packaging-এর নিয়ম গত কয়েক বছরে দ্রুত বদলেছে, আর AI-এর শেখার লেখার বড় অংশ পুরোনো যুগের। সাধারণ ভুলগুলো:
| AI যা লেখে | সমস্যা | সঠিক পথ |
|---|---|---|
শুধু setup.py + setup.cfg, pyproject.toml নেই | পুরোনো যুগের পদ্ধতি; build-এর আগে কী লাগবে কেউ জানে না | [build-system] + [project]-সহ pyproject.toml |
python setup.py sdist upload বা setup.py install | বহু আগে বাতিলের পথে; পাসওয়ার্ড-আপলোড বন্ধ | python -m build, twine upload / uv publish |
license = {text = "MIT"} বা License :: classifier | PEP 639-এর আগের রূপ; নতুন টুল সতর্ক করে বা error দেয় | license = "MIT" + license-files |
library-র dependency-তে requests==2.31.0 | ব্যবহারকারীর প্রজেক্টে version-জট | requests>=2.31 (দরকারে জানা ঊর্ধ্বসীমা) |
CI-ফাইলে TWINE_PASSWORD: pypi-AgE... সরাসরি লেখা | token git-এর ইতিহাসে চিরকাল ফাঁস | trusted publishing, অন্তত repository secret |
version দুই জায়গায়, requires-python নেই | অমিল version, পুরোনো Python-এ অদ্ভুত error | এক জায়গায় version, importlib.metadata; requires-python দিন |
ভেতরের package-এর জন্য --extra-index-url | dependency confusion | একক private index |
AI-কে packaging-এর কাজ দেওয়ার সময় আধুনিক নিয়মগুলো শুরুতেই বেঁধে দিন, আর সিদ্ধান্তগুলোর কারণ জানতে চান:
AI-এর দেওয়া packaging কনফিগ প্রকাশের আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো packaging-এর “কীভাবে” থেকে নকশার “কেন”-তে নিয়ে যাবে।
প্র ০১distutils ২০০০ সাল থেকে Python-এর ভেতরেই ছিল — তাহলে সেটিকে standard library থেকে সরাতে প্রায় দুই দশক লাগল কেন, আর এতে কী শেখার আছে?
distutils (Python 1.6, ২০০০): Greg Ward-এর নেতৃত্বে তৈরি এই module প্রথমবার Python-এ “package বানানো ও ইনস্টল করার” একটি মানক পথ দেয় — setup.py লিখে python setup.py install. standard library-র অংশ হওয়ায় সবার কাছে ছিল, এটিই বড় সুবিধা।
সমস্যা — standard library-র গতি: standard library-র কোড বদলায় শুধু নতুন Python release-এর সাথে, আর পুরোনো Python বহু বছর চালু থাকে। packaging-এর চাহিদা (dependency, নতুন ফরম্যাট, নতুন প্ল্যাটফর্ম) বদলাচ্ছিল অনেক দ্রুত। তাই উন্নতি হতে থাকে বাইরে: setuptools (২০০৪) distutils-এর ওপর dependency, entry point, egg যোগ করে; ২০০৮ সালে এর ফর্ক distribute আসে, যা ২০১৩ সালে আবার setuptools-এ মিশে যায়। বাস্তবে সবাই setuptools ব্যবহার করত, অথচ distutils standard library-তে বসে থাকত — পুরোনো, অর্ধেক-রক্ষণাবেক্ষণ করা, আর setuptools-কে ভেতরে ভেতরে তার ওপর patch করতে হতো।
মানক দিয়ে মুক্তি: সরানো তখনই সম্ভব হলো, যখন “কীভাবে build হবে” প্রশ্নটি কোনো একটি টুল থেকে আলাদা করে একটি চুক্তিতে পরিণত হলো। PEP 518 (২০১৬) [build-system] আনে, PEP 517 (২০১৭) frontend ও backend-এর মানক ফাংশন ঠিক করে; pip 10 (২০১৮) pyproject.toml পড়া শুরু করে, আর pip 19 (২০১৯) থেকে PEP 517 build. PEP 621 (২০২০) metadata-কে টুলনিরপেক্ষ [project] অংশে আনে। এবার আর distutils-এর দরকার নেই — যেকোনো backend চলবে।
বিদায়: PEP 632 (২০২০) distutils-কে Python 3.10-এ deprecated ঘোষণা করে, আর Python 3.12 (২০২৩) থেকে এটি সম্পূর্ণ সরানো। যাঁদের এখনো লাগে, setuptools নিজের ভেতরে distutils-এর একটি কপি রেখে দেয়। সরানোর আগে কয়েক বছরের সতর্কবার্তা ছিল, যাতে পুরোনো প্রজেক্ট সময় পায়।
এখানে একটি সাধারণ নকশা-শিক্ষা আছে: ভাষার ভেতরে যা ঢোকে, তা স্থির হয় কিন্তু ধীর হয়ে যায় — Python জগতে প্রবাদ আছে, “standard library হলো যেখানে module মরতে যায়”। দ্রুত বদলানো ক্ষেত্রে ভাষার কাজ টুল দেওয়া নয়, মানক ইন্টারফেস দেওয়া; টুলগুলো বাইরে প্রতিযোগিতা করে উন্নত হয়।
মূল উপলব্ধি: distutils থেকে PEP 517/621-এর যাত্রা হলো “একটি সরকারি টুল” থেকে “একটি সরকারি চুক্তি”-তে যাওয়া। চুক্তি স্থির থাকলে টুল বদলানো যায় — এজন্যই আজ hatchling থেকে uv_build-এ যেতে দুটি লাইন বদলালেই চলে।
প্র ০২egg তো আগে থেকেই ছিল — তাহলে wheel (PEP 427) নামে নতুন ফরম্যাট আনার দরকার পড়ল কেন? দুটিই তো zip!
setuptools-এর সাথে আসা egg (মোটামুটি ২০০৪) আর পরে আসা wheel দুটিই zip-ভিত্তিক — পার্থক্যটা উদ্দেশ্যে। egg একসাথে দুটি কাজ করতে চাইত: বিতরণের ফরম্যাট, আবার সরাসরি import করার ফরম্যাট। একটি .egg ফাইল sys.path-এ বসিয়ে দিলেই তার ভেতর থেকে import চলত। শুনতে চতুর, কিন্তু এর দাম ছিল: এক package-এর একাধিক version পাশাপাশি রাখতে .pth ফাইল আর runtime-এ sys.path বদলানোর জটিল কৌশল, zip-এর ভেতর থেকে ডেটা-ফাইল পড়ার ঝামেলা, আর ধীর import.
আরও বড় সমস্যা ছিল মানকের অভাব। egg-এর metadata (EGG-INFO) কোনো PEP-এ ঠিকভাবে লেখা ছিল না — কার্যত “setuptools যা করে, তা-ই নিয়ম”। কোন egg কোন Python বা কোন OS-এর জন্য, তা প্রকাশের কোনো সম্পূর্ণ ও নির্ভরযোগ্য ব্যবস্থা ছিল না। আর ইনস্টলের সময় কোন ফাইল কোথায় গেল, তার মানক রসিদও ছিল না, তাই uninstall অনির্ভরযোগ্য।
wheel (PEP 427, ২০১২ সালে প্রস্তাবিত, ২০১৩-তে গৃহীত, Daniel Holth): নকশার মূল সিদ্ধান্ত — wheel শুধু ইনস্টলের ফরম্যাট, import-এর নয়। ইনস্টলার এটি খুলে site-packages-এ সাধারণ ফাইল হিসেবে বসায়, তারপর Python সাধারণ ফোল্ডার থেকেই import করে। সাথে আসে স্পষ্ট মানক: ফাইলের নামে compatibility tag (PEP 425 — python, abi, platform), .dist-info-তে মানক METADATA, আর RECORD-এ প্রতিটি ফাইলের hash — যা দিয়ে uninstall আর অখণ্ডতা যাচাই দুটোই হয়। ইনস্টলের সময় কোনো কোড চলে না, তাই দ্রুত ও নিরাপদ।
পরিণতি বিশাল: NumPy-র মতো C-নির্ভর package Windows আর Mac-এ compiler ছাড়াই ইনস্টল হতে শুরু করে, আর manylinux মানক (PEP 513, ২০১৬) Linux-এও তা সম্ভব করে। Python-এর বৈজ্ঞানিক জগতের বিস্তারের পেছনে এর অবদান কম নয়। egg ধীরে ধীরে বিদায় নেয়; ২০২৩ সালে PyPI নতুন egg আপলোড নেওয়া বন্ধ করে।
মূল উপলব্ধি: একটি ফরম্যাট দুটি দায়িত্ব নিলে দুটিই খারাপ হয়। wheel জিতেছে কম কাজ করে — শুধু বিতরণ — আর সেই কাজটি নিখুঁত মানকে বেঁধে।
প্র ০৩২০১৬ সালে npm-এর ১১ লাইনের left-pad package সরিয়ে নেওয়ায় হাজারো প্রজেক্ট ভেঙে পড়ে। এমনটা কি Python-এ হতে পারে, আর একজন package-লেখক হিসেবে এ থেকে কী শিখবেন?
ঘটনা (মার্চ ২০১৬): একজন JavaScript ডেভেলপার, Azer Koçulu, একটি package-এর নাম নিয়ে কোম্পানির সাথে বিরোধের পর প্রতিবাদে npm থেকে নিজের প্রায় আড়াইশোর মতো package সরিয়ে নেন। তার একটি ছিল left-pad — একটি string-এর বাঁয়ে ফাঁকা ঘর বসানোর ১১ লাইনের ফাংশন। দেখা গেল, Babel-এর মতো বিশাল টুলসহ অসংখ্য প্রজেক্ট transitive dependency হিসেবে এর ওপর নির্ভর করত। কয়েক ঘণ্টার জন্য পৃথিবীজুড়ে build ভাঙতে থাকে। npm অস্বাভাবিক পদক্ষেপ নিয়ে package-টি ফিরিয়ে আনে, আর পরে নিয়ম বদলায়: প্রকাশের কিছু সময় পর, অন্যরা নির্ভর করলে, package সরানো কঠিন করা হয়।
Python-এ কী অবস্থা? PyPI-তে মালিক এখনো নিজের প্রজেক্ট বা release মুছতে পারেন — তাই একই ধরনের ঝুঁকি তাত্ত্বিকভাবে আছে। তবে দুটি সুরক্ষা আছে: মুছে ফেলা ফাইলের নাম আর কখনো ব্যবহার করা যায় না, ফলে কেউ মুছে একই নাম-version-এ অন্য কোড বসাতে পারে না; আর PEP 592 (২০১৯) এনেছে “yank” — মোছার বদলে নরম পথ, যাতে নতুন ইনস্টলে খারাপ version না আসে কিন্তু যাঁরা হুবহু pin করেছেন তাঁদের build না ভাঙে। Python-এর বড় standard library-ও সাহায্য করে — ছোটখাটো কাজের জন্য (যেমন str.rjust, যা left-pad-এর কাজই করে) বাইরের package লাগে না।
গভীর শিক্ষা — নির্ভরতা মানে বিশ্বাস: প্রতিটি dependency মানে কারও ভবিষ্যৎ সিদ্ধান্তের ওপর নির্ভর করা — তিনি package রাখবেন কি না, অ্যাকাউন্ট সুরক্ষিত রাখবেন কি না, ভাঙা version দেবেন কি না। প্রতিরক্ষার স্তর: lock file আর hash (যা আছে তা-ই আসবে), কোম্পানির private index-এ PyPI-র proxy বা mirror (উৎস হারালেও কপি থাকে), আর খুব ছোট dependency হলে নিজে লিখে ফেলা বা কোড প্রজেক্টের ভেতরে এনে রাখা (vendoring).
লেখক হিসেবে: আপনার package-এ যত কম dependency, আপনার ব্যবহারকারীর ঝুঁকি তত কম। আর প্রকাশিত কিছু সরাতে চাইলে মোছার বদলে yank করুন, নতুন version-এ deprecation-বার্তা দিন — অন্যের build ভাঙবেন না।
মূল উপলব্ধি: open source ইকোসিস্টেম একটি সামাজিক চুক্তি। প্রকাশ করা মানে অন্যকে আপনার ওপর নির্ভর করার আমন্ত্রণ দেওয়া — সেই দায়িত্বের সাথে আচরণ করতে হয়।
প্র ০৪PyPI-তে ক্ষতিকর package আর typosquatting-এর ঢেউ বারবার আসে। ভাণ্ডারটি প্রকাশকের দিক থেকে কোন কোন প্রতিরক্ষা গড়ে তুলেছে, আর কেন “সব package আগে পরীক্ষা করো” সমাধান নয়?
পর্ব ২৩-এ ব্যবহারকারীর দিক থেকে কয়েকটি ঘটনা দেখেছেন। প্রকাশকের দিক থেকে ছবিটা এমন: আক্রমণের দুই বড় ধরন — নতুন নামে নকল (typosquatting, পরিচিত নামের কাছাকাছি বানান) আর আসল নামের দখল (প্রকাশকের অ্যাকাউন্ট বা token চুরি)। প্রথমটি অনেক মানুষকে অল্প অল্প ধরে, দ্বিতীয়টি একবারে বিপুল ক্ষতি করে, কারণ তখন সবাই বিশ্বাস করা নামটিই ইনস্টল করেন।
কেন আগে পরীক্ষা নয়? PyPI-তে প্রতিদিন বিপুল সংখ্যক নতুন release আসে, আর ভাণ্ডারটি মূলত স্বেচ্ছাসেবক ও অল্প কয়েকজন কর্মী দিয়ে চলে। প্রতিটি release হাতে পরীক্ষা অসম্ভব, আর স্বয়ংক্রিয় স্ক্যানার চতুর আক্রমণকারীকে সবসময় ধরতে পারে না — উল্টে অনুমোদনের অপেক্ষা পুরো ইকোসিস্টেমকে ধীর করত। তাই PyPI-র কৌশল হলো আক্রমণের খরচ বাড়ানো আর ধরা পড়লে দ্রুত সরানো।
প্রতিরক্ষার স্তরগুলো:
- পরিচয় শক্ত করা: API token (২০১৯) — পাসওয়ার্ডের বদলে সীমিত-অধিকারের চাবি; ২০২৪-এর শুরু থেকে সব আপলোডকারীর জন্য 2FA বাধ্যতামূলক।
- চাবি না রাখা: trusted publishing (২০২৩) — চুরির মতো স্থায়ী token-ই নেই; সাথে attestation (PEP 740, ২০২৪) — কোন repository ও workflow ফাইলটি বানিয়েছে তার স্বাক্ষরিত প্রমাণ।
- ফাঁস ধরা: GitHub-এর secret scanning-এর সাথে যুক্ত থেকে পাবলিক কোডে পাওয়া PyPI token স্বয়ংক্রিয় বাতিল।
- দ্রুত প্রতিক্রিয়া: নিরাপত্তা গবেষক ও কোম্পানিগুলোর রিপোর্ট নেওয়ার ব্যবস্থা, ক্ষতিকর প্রজেক্ট দ্রুত সরানো বা আটকে রাখা; বড় আক্রমণের ঢেউয়ের সময় কয়েকবার সাময়িকভাবে নতুন অ্যাকাউন্ট বা প্রজেক্ট তৈরি বন্ধ রাখা হয়েছে।
- নামের নিয়ম: normalization-এর পর একই হয়ে যাওয়া নাম নেওয়া যায় না, তাই বড়-ছোট হাতের অক্ষর বা
-/_-এর খেলা দিয়ে হুবহু নকল নাম বানানো যায় না।
তবু শেষ রক্ষাকবচ মানুষ: প্রকাশক অ্যাকাউন্ট সুরক্ষিত রাখবেন, ব্যবহারকারী নাম যাচাই করবেন আর pin ও hash ব্যবহার করবেন।
মূল উপলব্ধি: খোলা ভাণ্ডারে নিখুঁত পাহারা অসম্ভব; কার্যকর নিরাপত্তা আসে স্তরে স্তরে — প্রতিটি স্তর আক্রমণকে একটু কঠিন, একটু ব্যয়বহুল আর একটু দ্রুত ধরা পড়ার মতো করে।
প্র ০৫“আমার কম্পিউটারে চলে” থেকে হুবহু পুনরুৎপাদনযোগ্য ইনস্টল — Python-এ lock file-এর ইতিহাস কেমন, আর PEP 751-এর pylock.toml কী সমাধান করতে চায়?
pyproject.toml বলে “কী লাগে”, কিন্তু একই পরিসর থেকে আজ আর ছয় মাস পরে ভিন্ন version আসে। চালানোর উপযোগী অ্যাপের দরকার হুবহু ছবি — প্রতিটি package, transitive-সহ, নির্দিষ্ট version, আর আদর্শভাবে প্রতিটি ফাইলের hash. এটিই lock file.
প্রথম যুগ — pip freeze: হুবহু version দেয়, কিন্তু hash নেই, আর এটি শুধু এই কম্পিউটারের ছবি — Windows-এ freeze করা তালিকায় Linux-এ লাগে এমন dependency না-ও থাকতে পারে। pip-tools-এর pip-compile হাতে লেখা তালিকা থেকে hash-সহ ফাইল বানায়, আর pip install --require-hashes hash না মিললে ইনস্টল থামায়।
টুল-ভিত্তিক lock: Pipenv-এর Pipfile.lock (২০১৭), Poetry-র poetry.lock (২০১৮), PDM-এর pdm.lock, আর uv-এর uv.lock (২০২৪) — যা “universal”, অর্থাৎ এক ফাইলে সব OS আর Python version-এর সমাধান রাখে। প্রতিটি ভালো, কিন্তু প্রতিটির ফরম্যাট আলাদা, আর অন্য টুল তা পড়তে পারে না। ক্লাউড প্ল্যাটফর্ম বা নিরাপত্তা-স্ক্যানারকে তাই প্রতিটি ফরম্যাট আলাদা করে বুঝতে হয়।
মানকের চেষ্টা: একটি সাধারণ lock-ফরম্যাটের প্রথম প্রস্তাব PEP 665 ২০২২ সালে প্রত্যাখ্যাত হয় — অন্যতম কারণ, এটি sdist সমর্থন করত না, যা অনেকের কাছে অগ্রহণযোগ্য ছিল। Brett Cannon-এর PEP 751 বহু আলোচনার পর ২০২৫ সালে গৃহীত হয়: pylock.toml, একটি TOML ফাইল, যাতে প্রতিটি package-এর version, উৎস আর hash থাকে, এবং যা ইনস্টল করতে resolver চালাতে হয় না — শুধু পড়ে বসিয়ে দেওয়া। টুলগুলো ধীরে ধীরে সমর্থন যোগ করছে: pip 25.1-এ পরীক্ষামূলক pip lock কমান্ড এসেছে, uv এই ফরম্যাটে export করতে পারে। তবে এখনো সব টুল নিজের ফরম্যাটই প্রধান হিসেবে রাখে; ভবিষ্যতে কতটা একীভূত হবে, তা সময় বলবে।
library-লেখকের জন্য একটি সূক্ষ্মতা: lock file আপনার নিজের CI আর ডেভেলপমেন্টকে পুনরুৎপাদনযোগ্য রাখে, কিন্তু wheel-এর সাথে ব্যবহারকারীর কাছে যায় না — ব্যবহারকারী পান শুধু METADATA-র পরিসর। তাই library-তে lock রাখা ঠিক আছে, কিন্তু সেটিকে dependency-নীতি ভাববেন না।
মূল উপলব্ধি: “কী চাই” (পরিসর) আর “ঠিক কী পেলাম” (lock) দুটি আলাদা প্রশ্ন। Python-এর ইকোসিস্টেম দুটিকে আলাদা ফাইলে মানক করতে প্রায় এক দশক লাগিয়েছে — প্রথমটি PEP 621-এ, দ্বিতীয়টি PEP 751-এ।
অনুশীলন
প্রথম তিনটি ভাবনার কাজ, শেষটি কোড লেখার। নিজের কম্পিউটার থাকলে শেষটি সত্যিই build করে দেখুন।
-
wheel-এর নাম পড়ুন: নিচের তিনটি ফাইল কোথায় কোথায় ইনস্টল হবে? (ক)
taka_format-0.1.0-py3-none-any.whl(খ)fastcalc-1.2.0-cp313-cp313-win_amd64.whl(গ)fastcalc-1.2.0-cp310-abi3-manylinux_2_28_x86_64.whl. Python 3.14, 64-বিট Windows-এfastcalcইনস্টল করতে চাইলে pip কী করবে?- (ক) যেকোনো Python 3, যেকোনো OS — বিশুদ্ধ Python.
- (খ) শুধু CPython 3.13, 64-বিট Windows.
- (গ) CPython 3.10 বা নতুন যেকোনো version (stable ABI), কিন্তু শুধু glibc 2.28 বা নতুন x86-64 Linux-এ।
Python 3.14 + Windows-এ (খ) মেলে না (ভুল Python), (গ) মেলে না (ভুল OS). তাই pip উপযুক্ত wheel না পেয়ে sdist খুঁজবে; পেলে আপনার কম্পিউটারে build করার চেষ্টা করবে — ভেতরে C কোড থাকলে compiler লাগবে, না থাকলে error. এজন্যই নতুন Python বেরোলে কিছুদিন কিছু package ইনস্টল হতে চায় না।
-
AI-এর কনফিগে ভুল ধরুন: একটি library-র জন্য AI এই pyproject.toml দিয়েছে। অন্তত পাঁচটি সমস্যা খুঁজুন:
[project]-এname = "bd_phone",versionনেই,license = {text = "MIT"},classifiers = ["License :: OSI Approved :: MIT License"],dependencies = ["phonenumbers==8.13.0"]; আর কোনো[build-system]নেই, পাশে.github/workflows/publish.yml-এpassword: pypi-AgEIcHlwaS5vcmc...[build-system]নেই — frontend জানে না কোন backend; যোগ করুন, যেমন hatchling.versionনেই (আরdynamic-এও নেই) — metadata অবৈধ, build ব্যর্থ হবে।license = {text = ...}আরLicense ::classifier — PEP 639-এর আগের রূপ; লিখুনlicense = "MIT", classifier বাদ।- library-তে
phonenumbers==8.13.0— হুবহু pin ব্যবহারকারীর জন্য জট; দিনphonenumbers>=8.13. - workflow-এ token সরাসরি লেখা — git-এর ইতিহাসে চিরকাল থাকবে। এখনই token বাতিল করুন, তারপর trusted publishing.
- বাড়তি:
requires-pythonনেই;name-এbd_phoneচলবে (normalization-এbd-phone-এর সমান), তবে প্রথা অনুযায়ীbd-phoneলেখা পরিষ্কার।
-
version সাজান:
taka-format-এর PyPI-তে আছে:1.0.0,1.0.0rc1,1.0.0a1,1.0.0.post1,1.0.0b2,1.0.0.dev3,1.1.0b1. (ক) ছোট থেকে বড় সাজান। (খ)pip install taka-formatকোনটি নেবে? (গ)pip install --pre taka-format? (ঘ)"taka-format<1.1"?(ক)
1.0.0.dev3<1.0.0a1<1.0.0b2<1.0.0rc1<1.0.0<1.0.0.post1<1.1.0b1.(খ)
1.0.0.post1— সবচেয়ে বড় স্থিতিশীল version; post-release স্থিতিশীল ধরা হয়, pre-release নয়। (গ)1.1.0b1—--preদিলে pre-release-ও বিবেচনায় আসে। (ঘ)1.0.0.post1—<1.11.1-এর pre-release (1.1.0b1)-কেও বাদ দেয়। -
একটি বাংলাদেশি utility package বানান:
bd-phoneনামে একটি package-এর জন্যis_bd_mobile(number: str) -> boolলিখুন, যা01712345678আর+8801712345678দুটিকেই গ্রহণ করে (১১ অঙ্ক, শুরুতে01, তারপর3–9-এর একটি অঙ্ক)। তারপর বলুন src layout-এ ফাইল কোথায় বসবে, আরbdphoneনামে একটি console script যোগ করতে pyproject.toml-এ কী লিখবেন।# src/bd_phone/__init__.py import re _PATTERN = re.compile(r"01[3-9]\d{8}") def is_bd_mobile(number: str) -> bool: number = number.strip().replace(" ", "").replace("-", "") if number.startswith("+880"): number = "0" + number[4:] return _PATTERN.fullmatch(number) is not None assert is_bd_mobile("01712345678") and is_bd_mobile("+8801712345678") assert not is_bd_mobile("01212345678") and not is_bd_mobile("0171234567")+880বাদ দিয়ে সামনে0বসালে দুটি রূপ এক হয়ে যায়।fullmatchপুরো string মেলায়, তাই ১০ বা ১২ অঙ্ক বাদ পড়ে (regex পর্ব ৩০-এ)। কাঠামো:bd-phone/pyproject.toml,bd-phone/src/bd_phone/__init__.py,bd-phone/src/bd_phone/cli.py(ভেতরেmain()),bd-phone/tests/. pyproject.toml-এ:[project.scripts] bdphone = "bd_phone.cli:main"তারপর
python -m build, wheel খুলে দেখা, নতুন venv-এ ইনস্টল করেbdphone 01712345678— ঠিক অংশ ৫ ও ৭-এর মতো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমনিজের Python package প্রথমবার PyPI-তে প্রকাশের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- src layout-এ কোড আর pyproject.toml লেখা
python -m buildদিয়ে sdist ও wheel বানানো- wheel খুলে দেখা আর নতুন venv-এ ইনস্টল করে চালানো
- TestPyPI-তে আপলোড করে সেখান থেকে ইনস্টল-পরীক্ষা
- আসল PyPI-তে প্রকাশ
Python-এর একটি wheel ফাইলের নামের শেষে py3-none-any থাকলে তার অর্থ কী?
- শুধু Linux-এ চলে, কোনো Python version-এর শর্ত নেই
- যেকোনো Python 3-এ, যেকোনো OS-এ চলে — ভেতরে compile করা অংশ নেই
- package-টির কোনো dependency নেই
- এটি একটি pre-release version
ইনস্টল করা একটি Python package-এর .dist-info ফোল্ডারের কোন ফাইলে package-এর প্রতিটি ফাইলের পথ ও hash লেখা থাকে, যা দেখে pip uninstall ফাইল মোছে?
METADATAWHEELentry_points.txtRECORD
অন্যরা ব্যবহার করবেন এমন একটি Python library-র pyproject.toml-এ requests-কে dependency হিসেবে লেখার সবচেয়ে উপযুক্ত রূপ কোনটি?
"requests==2.31.0""requests", কোনো শর্ত ছাড়া"requests>=2.31,<3"বা"requests>=2.31"- dependency না লিখে README-তে বলে দেওয়া “requirements.txt থেকে ইনস্টল করুন”
PEP 639 অনুযায়ী Python প্রজেক্টের pyproject.toml-এ license লেখার আধুনিক রূপ কোনটি?
license = "MIT"আরlicense-files = ["LICENSE"]license = {text = "MIT"}- শুধু
"License :: OSI Approved :: MIT License"classifier - license লেখার দরকার নেই, LICENSE ফাইল রাখলেই যথেষ্ট
GitHub Actions থেকে PyPI-তে trusted publishing ব্যবহার করলে repository-র secret-এ কী জমা রাখতে হয়?
- PyPI-র একটি project-scoped API token
- কিছুই না — workflow-এ
id-token: writeঅনুমতি আর PyPI-তে আগে থেকে publisher যোগ করাই যথেষ্ট - PyPI অ্যাকাউন্টের username আর পাসওয়ার্ড
- 2FA-র recovery code
PyPI-তে একটি package-এর version 2.0.0 আর 2.1.0rc1 আছে। কেউ শুধু pip install দিয়ে package-এর নাম লিখলে (কোনো flag বা শর্ত ছাড়া) কোনটি ইনস্টল হবে?
2.1.0rc1, কারণ সেটি সংখ্যায় বড়- pip দ্বিধায় পড়ে error দেবে
- 2.0.0
- দুটিই, পাশাপাশি
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- নিজের কোড = নিজের distribution: নাম, version আর মানক ফাইল থাকলে একই কোড সব প্রজেক্টে, দলে বা PyPI-তে — কপি-পেস্ট নয়।
- src layout আর একটি pyproject.toml:
[build-system]+[project]; license একটি SPDX expression,requires-pythonদিন, কমান্ড[project.scripts]-এ। python -m build→ sdist + wheel: wheel একটি zip — কোড আর.dist-info(METADATA, RECORD, entry_points). প্রতিবার খুলে দেখুন, নতুন venv-এ পরীক্ষা করুন।- Version প্রতিশ্রুতি: PEP 440, pre-release নিজে থেকে আসে না, একবার আপলোড মানে চিরকাল; version এক জায়গায়; library-তে পরিসর, অ্যাপে lock.
- নিরাপদ প্রকাশ: TestPyPI আগে, 2FA, token কখনো git-এ নয় — সম্ভব হলে trusted publishing; কোম্পানিতে একক private index.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৫ · exception — ভুল সামলানোপরবর্তী পর্বভুল হলে প্রোগ্রাম ভেঙে না পড়ে কীভাবে সামলাবে — try, except, finally আর নিজের exception.
- পর্ব ২৩ · pip, virtual environment আর pyprojectআগের পর্বব্যবহারকারীর দিক — venv, pip, version-শর্ত, lock file আর package-নামের নিরাপত্তা।
- Python প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Java প্রোগ্রামিং · Maven ও Gradleতুলনা করুনJava-তে build, dependency আর Maven Central — একই সমস্যার আরেক সমাধান।
- Cloud ও DevOps · build-test-deploy pipelineপাশের ট্র্যাকCI/CD pipeline-এর নকশা — package প্রকাশের workflow যার একটি অংশ।