dev-dna 0.1.0__py3-none-any.whl

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.
dev_dna/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """dev-dna — 23andMe for developers.
2
+
3
+ Analyze any GitHub user's developer DNA: dominant languages, evolution
4
+ over time, commit personality archetype, specialization score, star power,
5
+ and a shareable summary.
6
+ """
7
+
8
+ from dev_dna.analyzer import analyze_user, fetch_repos
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = ["analyze_user", "fetch_repos"]
dev_dna/analyzer.py ADDED
@@ -0,0 +1,597 @@
1
+ """Core analysis logic for dev-dna.
2
+
3
+ Fetches GitHub public API data and computes developer DNA metrics.
4
+ All HTTP calls use urllib.request — no third-party HTTP libraries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ import urllib.error
13
+ import urllib.request
14
+ from collections import Counter
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+ GITHUB_API = "https://api.github.com"
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # GitHub API helpers
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ def _build_headers(token: Optional[str] = None) -> Dict[str, str]:
26
+ headers: Dict[str, str] = {
27
+ "Accept": "application/vnd.github+json",
28
+ "X-GitHub-Api-Version": "2022-11-28",
29
+ "User-Agent": "dev-dna-cli/0.1.0",
30
+ }
31
+ resolved_token = token or os.environ.get("GITHUB_TOKEN")
32
+ if resolved_token:
33
+ headers["Authorization"] = f"Bearer {resolved_token}"
34
+ return headers
35
+
36
+
37
+ def _get(url: str, token: Optional[str] = None) -> Any:
38
+ """Perform a GET request and return parsed JSON, or raise on error."""
39
+ req = urllib.request.Request(url, headers=_build_headers(token))
40
+ try:
41
+ with urllib.request.urlopen(req, timeout=15) as resp:
42
+ return json.loads(resp.read().decode("utf-8"))
43
+ except urllib.error.HTTPError as exc:
44
+ if exc.code == 403:
45
+ body = exc.read().decode("utf-8", errors="replace")
46
+ if "rate limit" in body.lower():
47
+ raise RateLimitError(
48
+ "GitHub API rate limit exceeded. "
49
+ "Set GITHUB_TOKEN or pass --token for higher limits."
50
+ ) from exc
51
+ raise GitHubError(f"HTTP 403 from GitHub API: {body}") from exc
52
+ if exc.code == 404:
53
+ raise NotFoundError(f"GitHub user or resource not found: {url}") from exc
54
+ raise GitHubError(f"GitHub API error {exc.code}: {exc.reason}") from exc
55
+ except urllib.error.URLError as exc:
56
+ raise GitHubError(f"Network error: {exc.reason}") from exc
57
+
58
+
59
+ class RateLimitError(RuntimeError):
60
+ pass
61
+
62
+
63
+ class NotFoundError(RuntimeError):
64
+ pass
65
+
66
+
67
+ class GitHubError(RuntimeError):
68
+ pass
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Data fetching
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def fetch_user(username: str, token: Optional[str] = None) -> Dict[str, Any]:
77
+ """Fetch the public profile for *username*."""
78
+ return _get(f"{GITHUB_API}/users/{username}", token=token)
79
+
80
+
81
+ def fetch_repos(username: str, token: Optional[str] = None) -> List[Dict[str, Any]]:
82
+ """Fetch all public repos for *username* (up to 100, sorted by update date)."""
83
+ url = (
84
+ f"{GITHUB_API}/users/{username}/repos"
85
+ "?per_page=100&sort=updated&type=public"
86
+ )
87
+ repos: List[Dict[str, Any]] = _get(url, token=token)
88
+ if not isinstance(repos, list):
89
+ return []
90
+ # Filter out forks to focus on original work (keep forks if they have stars)
91
+ return [r for r in repos if not r.get("fork") or (r.get("stargazers_count", 0) > 0)]
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Metric computation
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def _parse_date(date_str: Optional[str]) -> Optional[datetime]:
100
+ if not date_str:
101
+ return None
102
+ try:
103
+ # GitHub returns ISO 8601 with Z suffix
104
+ return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
105
+ except ValueError:
106
+ return None
107
+
108
+
109
+ def compute_language_dna(repos: List[Dict[str, Any]]) -> List[Tuple[str, int]]:
110
+ """Return [(language, repo_count), ...] sorted descending, top 8."""
111
+ counter: Counter = Counter()
112
+ for repo in repos:
113
+ lang = repo.get("language")
114
+ if lang:
115
+ counter[lang] += 1
116
+ return counter.most_common(8)
117
+
118
+
119
+ def compute_star_power(repos: List[Dict[str, Any]]) -> Dict[str, Any]:
120
+ total = sum(r.get("stargazers_count", 0) for r in repos)
121
+ if not repos:
122
+ return {"total": 0, "top_repo": None, "top_stars": 0}
123
+ top = max(repos, key=lambda r: r.get("stargazers_count", 0))
124
+ return {
125
+ "total": total,
126
+ "top_repo": top.get("name", ""),
127
+ "top_stars": top.get("stargazers_count", 0),
128
+ }
129
+
130
+
131
+ def compute_velocity(
132
+ repos: List[Dict[str, Any]], user: Dict[str, Any]
133
+ ) -> Dict[str, Any]:
134
+ """Repos per month since account creation."""
135
+ created_at = _parse_date(user.get("created_at"))
136
+ if not created_at:
137
+ return {"repos_per_month": 0.0, "total_repos": len(repos), "months": 0}
138
+
139
+ now = datetime.now(timezone.utc)
140
+ months = max(
141
+ 1,
142
+ (now.year - created_at.year) * 12 + (now.month - created_at.month),
143
+ )
144
+ rpm = round(len(repos) / months, 1)
145
+ return {
146
+ "repos_per_month": rpm,
147
+ "total_repos": len(repos),
148
+ "months": months,
149
+ "account_created": created_at,
150
+ }
151
+
152
+
153
+ def compute_evolution(
154
+ repos: List[Dict[str, Any]],
155
+ ) -> Dict[str, Any]:
156
+ """Compare primary language in oldest 20% vs newest 20% of repos."""
157
+ dated = []
158
+ for r in repos:
159
+ dt = _parse_date(r.get("created_at"))
160
+ if dt:
161
+ dated.append((dt, r))
162
+ if not dated:
163
+ return {"early": "N/A", "recent": "N/A", "earliest_year": None}
164
+
165
+ dated.sort(key=lambda x: x[0])
166
+ n = max(1, len(dated) // 5) # 20%
167
+
168
+ def top_lang(subset: List[Tuple[datetime, Dict[str, Any]]]) -> str:
169
+ c: Counter = Counter()
170
+ for _, r in subset:
171
+ lang = r.get("language")
172
+ if lang:
173
+ c[lang] += 1
174
+ return c.most_common(1)[0][0] if c else "N/A"
175
+
176
+ earliest_year = dated[0][0].year
177
+ early_lang = top_lang(dated[:n])
178
+ recent_langs: Counter = Counter()
179
+ for _, r in dated[-n:]:
180
+ lang = r.get("language")
181
+ if lang:
182
+ recent_langs[lang] += 1
183
+
184
+ # Trajectory label
185
+ top_recent = [l for l, _ in recent_langs.most_common(3)]
186
+ if len(top_recent) >= 2:
187
+ recent_str = " + ".join(top_recent[:2])
188
+ trajectory = "Diversifying"
189
+ elif top_recent:
190
+ recent_str = top_recent[0]
191
+ trajectory = "Deepening specialization" if recent_str == early_lang else "Language shift"
192
+ else:
193
+ recent_str = "N/A"
194
+ trajectory = "Not enough data"
195
+
196
+ return {
197
+ "early": early_lang,
198
+ "recent": recent_str,
199
+ "trajectory": trajectory,
200
+ "earliest_year": earliest_year,
201
+ }
202
+
203
+
204
+ def compute_open_source_signal(repos: List[Dict[str, Any]]) -> int:
205
+ """Percentage of repos that have a license."""
206
+ if not repos:
207
+ return 0
208
+ licensed = sum(1 for r in repos if r.get("license"))
209
+ return round(licensed / len(repos) * 100)
210
+
211
+
212
+ def compute_documentation_score(repos: List[Dict[str, Any]]) -> int:
213
+ """Percentage of repos with both a description and (assumed) README.
214
+
215
+ The repos endpoint doesn't expose README presence directly, so we
216
+ use description as a proxy — it's a reasonable signal for how much
217
+ care the developer puts into documentation.
218
+ """
219
+ if not repos:
220
+ return 0
221
+ with_desc = sum(
222
+ 1
223
+ for r in repos
224
+ if r.get("description") and r["description"].strip()
225
+ )
226
+ return round(with_desc / len(repos) * 100)
227
+
228
+
229
+ def compute_specialization_score(
230
+ repos: List[Dict[str, Any]], lang_dna: List[Tuple[str, int]]
231
+ ) -> int:
232
+ """0-100. High = deep in one domain, Low = generalist.
233
+
234
+ Formula: weighted by language concentration (Herfindahl-style) + topic
235
+ coherence signal.
236
+ """
237
+ if not repos or not lang_dna:
238
+ return 0
239
+ total_with_lang = sum(c for _, c in lang_dna)
240
+ if total_with_lang == 0:
241
+ return 0
242
+ # Herfindahl index: sum of squares of shares
243
+ hhi = sum((c / total_with_lang) ** 2 for _, c in lang_dna)
244
+ # Map 0..1 to 0..100 (pure mono-language = 1.0 → 100, equal split → near 0)
245
+ # We scale so that HHI of 0.5 → ~50
246
+ score = round(min(100, hhi * 100 * 1.5))
247
+ return score
248
+
249
+
250
+ def _score_velocity(rpm: float) -> int:
251
+ """Map repos-per-month to a 0-100 score."""
252
+ if rpm >= 5:
253
+ return 99
254
+ if rpm >= 3:
255
+ return 90
256
+ if rpm >= 2:
257
+ return 80
258
+ if rpm >= 1:
259
+ return 65
260
+ if rpm >= 0.5:
261
+ return 45
262
+ if rpm >= 0.2:
263
+ return 25
264
+ return 10
265
+
266
+
267
+ def _velocity_percentile_label(rpm: float) -> str:
268
+ if rpm >= 5:
269
+ return "faster than 95% of GitHub developers"
270
+ if rpm >= 3:
271
+ return "faster than 90% of GitHub developers"
272
+ if rpm >= 2:
273
+ return "faster than 80% of GitHub developers"
274
+ if rpm >= 1:
275
+ return "faster than 65% of GitHub developers"
276
+ if rpm >= 0.5:
277
+ return "around the median GitHub developer"
278
+ return "a deliberate, quality-focused pace"
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Archetype detection
283
+ # ---------------------------------------------------------------------------
284
+
285
+ _ARCHETYPE_KEYWORDS: Dict[str, List[str]] = {
286
+ "toolsmith": [
287
+ "cli", "tool", "utility", "script", "automation", "helper",
288
+ "generator", "formatter", "linter", "plugin", "extension",
289
+ "command", "terminal", "shell", "devtool",
290
+ ],
291
+ "researcher": [
292
+ "ml", "ai", "deep-learning", "neural", "nlp", "llm", "gpt",
293
+ "bert", "transformer", "dataset", "research", "paper",
294
+ "experiment", "model", "training", "embedding", "rag",
295
+ "diffusion", "vision", "reinforcement",
296
+ ],
297
+ "fullstack": [
298
+ "frontend", "backend", "fullstack", "full-stack", "api",
299
+ "webapp", "web-app", "dashboard", "react", "vue", "angular",
300
+ "nextjs", "django", "flask", "fastapi", "express", "node",
301
+ "rest", "graphql", "tailwind", "css",
302
+ ],
303
+ "data_engineer": [
304
+ "etl", "pipeline", "data", "analytics", "warehouse",
305
+ "spark", "kafka", "airflow", "dbt", "postgres", "mysql",
306
+ "bigquery", "snowflake", "pandas", "numpy", "visualization",
307
+ "chart", "dashboard", "metrics",
308
+ ],
309
+ "hacker": [], # fallback for wide variety, many repos
310
+ "craftsperson": [], # fallback for few, polished repos
311
+ }
312
+
313
+ _ARCHETYPE_META: Dict[str, Dict[str, str]] = {
314
+ "toolsmith": {
315
+ "label": "The Toolsmith",
316
+ "blurb": (
317
+ "Ships fast. Builds tools that make other developers' lives "
318
+ "easier. Prefers CLI over GUI. Has opinions about your commit "
319
+ "messages."
320
+ ),
321
+ },
322
+ "researcher": {
323
+ "label": "The Researcher",
324
+ "blurb": (
325
+ "Lives at the intersection of academia and engineering. "
326
+ "Reads papers on weekends. Turns ideas into prototypes before "
327
+ "most people have finished the abstract."
328
+ ),
329
+ },
330
+ "fullstack": {
331
+ "label": "The Full-Stack Builder",
332
+ "blurb": (
333
+ "Comfortable everywhere from the database to the browser. "
334
+ "Thinks in user journeys. Can wire up an API and style a "
335
+ "landing page before lunch."
336
+ ),
337
+ },
338
+ "data_engineer": {
339
+ "label": "The Data Engineer",
340
+ "blurb": (
341
+ "Speaks fluent SQL and pipeline DSL. Believes clean data is "
342
+ "a prerequisite for everything. Has a strong opinion on "
343
+ "column naming conventions."
344
+ ),
345
+ },
346
+ "hacker": {
347
+ "label": "The Hacker",
348
+ "blurb": (
349
+ "Wide variety, fast shipping, zero apologies. Tries "
350
+ "everything. Learns by building. The repo count alone is a "
351
+ "flex."
352
+ ),
353
+ },
354
+ "craftsperson": {
355
+ "label": "The Craftsperson",
356
+ "blurb": (
357
+ "Fewer repos, but each one is polished. Sweats the details. "
358
+ "Writes the README before the code. Quality over quantity, "
359
+ "always."
360
+ ),
361
+ },
362
+ }
363
+
364
+
365
+ def detect_archetype(
366
+ repos: List[Dict[str, Any]], lang_dna: List[Tuple[str, int]]
367
+ ) -> str:
368
+ """Return an archetype key based on repo metadata signals."""
369
+ scores: Counter = Counter()
370
+
371
+ for repo in repos:
372
+ text = " ".join(
373
+ filter(
374
+ None,
375
+ [
376
+ repo.get("name", ""),
377
+ repo.get("description", "") or "",
378
+ repo.get("language", "") or "",
379
+ " ".join(repo.get("topics", []) or []),
380
+ ],
381
+ )
382
+ ).lower()
383
+
384
+ for archetype, keywords in _ARCHETYPE_KEYWORDS.items():
385
+ if not keywords:
386
+ continue
387
+ hits = sum(1 for kw in keywords if kw in text)
388
+ if hits:
389
+ scores[archetype] += hits
390
+
391
+ if not scores:
392
+ # No keyword matches — use structural heuristics
393
+ if len(repos) >= 30:
394
+ return "hacker"
395
+ return "craftsperson"
396
+
397
+ top_archetype, top_score = scores.most_common(1)[0]
398
+
399
+ # If top score is very low and we have many repos, prefer hacker
400
+ if top_score <= 2 and len(repos) >= 30:
401
+ return "hacker"
402
+
403
+ return top_archetype
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Traits / insights
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ def compute_traits(
412
+ repos: List[Dict[str, Any]],
413
+ lang_dna: List[Tuple[str, int]],
414
+ velocity: Dict[str, Any],
415
+ star_power: Dict[str, Any],
416
+ doc_score: int,
417
+ oss_score: int,
418
+ ) -> List[Dict[str, str]]:
419
+ """Return a list of {sign: '+'/'-', text: '...'} trait items."""
420
+ traits = []
421
+ total = len(repos)
422
+ rpm = velocity.get("repos_per_month", 0)
423
+
424
+ # Prolific builder
425
+ if total >= 50:
426
+ traits.append({"sign": "+", "text": f"Prolific builder — top 5% by repo count ({total} repos)"})
427
+ elif total >= 20:
428
+ traits.append({"sign": "+", "text": f"Active builder — {total} public repos"})
429
+
430
+ # AI native
431
+ ai_keywords = {"ml", "ai", "llm", "gpt", "nlp", "neural", "rag", "diffusion"}
432
+ ai_repos = sum(
433
+ 1
434
+ for r in repos
435
+ if any(
436
+ kw in " ".join([
437
+ r.get("name", ""),
438
+ r.get("description", "") or "",
439
+ " ".join(r.get("topics", []) or []),
440
+ r.get("language", "") or "",
441
+ ]).lower()
442
+ for kw in ai_keywords
443
+ )
444
+ )
445
+ if total > 0 and ai_repos / total >= 0.4:
446
+ traits.append({"sign": "+", "text": f"AI-native — {round(ai_repos/total*100)}%+ of repos involve ML/LLMs"})
447
+
448
+ # Velocity
449
+ if rpm >= 3:
450
+ traits.append({"sign": "+", "text": f"Rapid shipper — {rpm} repos/month average"})
451
+
452
+ # Stars
453
+ if star_power["total"] >= 100:
454
+ traits.append({"sign": "+", "text": f"Community recognition — {star_power['total']} total stars"})
455
+ elif star_power["total"] == 0 and total >= 10:
456
+ traits.append({"sign": "-", "text": "Discovery gap — great code, low visibility"})
457
+
458
+ # Documentation
459
+ if doc_score >= 70:
460
+ traits.append({"sign": "+", "text": f"Well-documented — {doc_score}% of repos have descriptions"})
461
+ elif doc_score < 40:
462
+ traits.append({"sign": "-", "text": f"Documentation debt — many repos lack descriptions ({doc_score}%)"})
463
+
464
+ # Open source
465
+ if oss_score >= 60:
466
+ traits.append({"sign": "+", "text": f"Open source contributor — {oss_score}% repos have licenses"})
467
+ elif oss_score < 30:
468
+ traits.append({"sign": "-", "text": f"Few repos have open-source licenses ({oss_score}%)"})
469
+
470
+ return traits
471
+
472
+
473
+ def compute_insight(
474
+ repos: List[Dict[str, Any]],
475
+ archetype: str,
476
+ velocity: Dict[str, Any],
477
+ star_power: Dict[str, Any],
478
+ doc_score: int,
479
+ oss_score: int,
480
+ spec_score: int,
481
+ ) -> str:
482
+ rpm = velocity.get("repos_per_month", 0)
483
+ total = len(repos)
484
+
485
+ if archetype == "researcher":
486
+ if star_power["total"] < 10:
487
+ return (
488
+ "Your research depth is rare. Consider publishing one "
489
+ "polished project with a demo, a paper summary, and a "
490
+ "clear README — that's what turns followers into stars."
491
+ )
492
+ return (
493
+ "Strong research portfolio. Packaging your best experiments "
494
+ "as installable libraries could multiply your impact."
495
+ )
496
+
497
+ if archetype == "toolsmith":
498
+ if rpm >= 3 and star_power["total"] < 20:
499
+ return (
500
+ "You build at a rare velocity. The bottleneck isn't output — "
501
+ "it's surface area. Adding descriptions, PyPI packages, and "
502
+ "one viral project would unlock the stars your code deserves."
503
+ )
504
+ return (
505
+ "Quality tooling with good visibility. "
506
+ "Consider writing about your tools on Dev.to or Hacker News "
507
+ "to compound your reach."
508
+ )
509
+
510
+ if archetype == "fullstack":
511
+ if doc_score < 50:
512
+ return (
513
+ "Full-stack range is a genuine superpower. "
514
+ "A few well-documented showcase projects would make this "
515
+ "profile stand out to any hiring team."
516
+ )
517
+ return (
518
+ "Well-rounded full-stack presence. "
519
+ "Pinning your three best projects on GitHub would create a "
520
+ "strong first impression."
521
+ )
522
+
523
+ if archetype == "data_engineer":
524
+ return (
525
+ "Data pipelines are the invisible backbone of every great "
526
+ "product. Sharing one end-to-end example with a dataset "
527
+ "and dashboard would make your skills tangible to outsiders."
528
+ )
529
+
530
+ if archetype == "hacker":
531
+ return (
532
+ f"You've shipped {total} repos — that's creative energy most "
533
+ "developers never tap. Picking your top 3 and investing in "
534
+ "their README, docs, and packaging would unlock outsized returns."
535
+ )
536
+
537
+ # craftsperson or fallback
538
+ return (
539
+ "Quality-over-quantity is a defensible strategy. "
540
+ "Your next step is distribution — make sure each polished "
541
+ "project has a demo link, license, and 3-line install guide."
542
+ )
543
+
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # Main analysis entry point
547
+ # ---------------------------------------------------------------------------
548
+
549
+
550
+ def analyze_user(
551
+ username: str, token: Optional[str] = None
552
+ ) -> Dict[str, Any]:
553
+ """Fetch GitHub data and return a structured DNA report dict."""
554
+ user = fetch_user(username, token=token)
555
+ repos = fetch_repos(username, token=token)
556
+
557
+ lang_dna = compute_language_dna(repos)
558
+ star_power = compute_star_power(repos)
559
+ velocity = compute_velocity(repos, user)
560
+ evolution = compute_evolution(repos)
561
+ oss_score = compute_open_source_signal(repos)
562
+ doc_score = compute_documentation_score(repos)
563
+ spec_score = compute_specialization_score(repos, lang_dna)
564
+ archetype_key = detect_archetype(repos, lang_dna)
565
+ archetype_meta = _ARCHETYPE_META[archetype_key]
566
+ traits = compute_traits(
567
+ repos, lang_dna, velocity, star_power, doc_score, oss_score
568
+ )
569
+ velocity_score = _score_velocity(velocity["repos_per_month"])
570
+ velocity_label = _velocity_percentile_label(velocity["repos_per_month"])
571
+ insight = compute_insight(
572
+ repos, archetype_key, velocity, star_power, doc_score, oss_score, spec_score
573
+ )
574
+
575
+ return {
576
+ "username": username,
577
+ "name": user.get("name") or username,
578
+ "bio": user.get("bio") or "",
579
+ "followers": user.get("followers", 0),
580
+ "following": user.get("following", 0),
581
+ "public_repos_api": user.get("public_repos", len(repos)),
582
+ "repos_analyzed": len(repos),
583
+ "lang_dna": lang_dna,
584
+ "star_power": star_power,
585
+ "velocity": velocity,
586
+ "velocity_score": velocity_score,
587
+ "velocity_label": velocity_label,
588
+ "evolution": evolution,
589
+ "oss_score": oss_score,
590
+ "doc_score": doc_score,
591
+ "spec_score": spec_score,
592
+ "archetype_key": archetype_key,
593
+ "archetype_label": archetype_meta["label"],
594
+ "archetype_blurb": archetype_meta["blurb"],
595
+ "traits": traits,
596
+ "insight": insight,
597
+ }
dev_dna/cli.py ADDED
@@ -0,0 +1,509 @@
1
+ """CLI entry point for dev-dna.
2
+
3
+ Commands
4
+ --------
5
+ dev-dna <username>
6
+ dev-dna <username> --json
7
+ dev-dna <username> --token <ghp_...>
8
+ dev-dna <username> --compare <other_username>
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ import textwrap
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+
19
+ import click
20
+ from rich import box
21
+ from rich.columns import Columns
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+ from rich.text import Text
26
+
27
+ from dev_dna.analyzer import (
28
+ GitHubError,
29
+ NotFoundError,
30
+ RateLimitError,
31
+ analyze_user,
32
+ )
33
+
34
+ console = Console()
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Rendering helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def _bar(fraction: float, width: int = 20, fill: str = "█", empty: str = "░") -> str:
42
+ """Return a Unicode block bar of given width."""
43
+ filled = round(fraction * width)
44
+ filled = max(0, min(width, filled))
45
+ return fill * filled + empty * (width - filled)
46
+
47
+
48
+ def _terminal_width() -> int:
49
+ try:
50
+ return os.get_terminal_size().columns
51
+ except OSError:
52
+ return 80
53
+
54
+
55
+ def _wrap(text: str, indent: int = 2) -> str:
56
+ width = max(40, _terminal_width() - indent - 4)
57
+ lines = textwrap.wrap(text, width=width)
58
+ prefix = " " * indent
59
+ return ("\n" + prefix).join(lines)
60
+
61
+
62
+ def _lang_bar_width() -> int:
63
+ """Dynamic bar chart width based on terminal size."""
64
+ cols = _terminal_width()
65
+ # Reserve: 2 indent + 12 name + 1 space + bar + 1 space + 4 pct = ~20 fixed
66
+ bar_width = max(10, min(40, cols - 30))
67
+ return bar_width
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Rich panel renderers
72
+ # ---------------------------------------------------------------------------
73
+
74
+ _LANG_COLORS = [
75
+ "bright_cyan",
76
+ "bright_green",
77
+ "bright_yellow",
78
+ "bright_magenta",
79
+ "bright_blue",
80
+ "bright_red",
81
+ "cyan",
82
+ "green",
83
+ ]
84
+
85
+
86
+ def render_header(data: Dict[str, Any]) -> Panel:
87
+ username = data["username"]
88
+ name = data["name"]
89
+ bio = data["bio"]
90
+
91
+ lines = Text()
92
+ lines.append(f" github.com/{username}\n", style="bold bright_white")
93
+ if name and name != username:
94
+ lines.append(f" {name}\n", style="dim white")
95
+ if bio:
96
+ lines.append(f" {bio}\n", style="italic dim white")
97
+
98
+ return Panel(
99
+ lines,
100
+ title="[bold bright_green]DEV DNA ANALYSIS[/bold bright_green]",
101
+ title_align="center",
102
+ border_style="bright_green",
103
+ padding=(0, 1),
104
+ )
105
+
106
+
107
+ def render_archetype(data: Dict[str, Any]) -> Panel:
108
+ label = data["archetype_label"]
109
+ blurb = data["archetype_blurb"]
110
+ wrapped = _wrap(blurb, indent=0)
111
+ content = Text()
112
+ content.append(f"{label}\n\n", style="bold bright_yellow")
113
+ content.append(f'"{wrapped}"', style="italic dim white")
114
+ return Panel(
115
+ content,
116
+ title="[bold]ARCHETYPE[/bold]",
117
+ border_style="yellow",
118
+ padding=(0, 2),
119
+ )
120
+
121
+
122
+ def render_language_dna(data: Dict[str, Any]) -> Panel:
123
+ lang_dna: List[Tuple[str, int]] = data["lang_dna"]
124
+ bar_width = _lang_bar_width()
125
+
126
+ if not lang_dna:
127
+ return Panel(
128
+ Text("No language data available.", style="dim"),
129
+ title="[bold]LANGUAGE DNA[/bold]",
130
+ border_style="cyan",
131
+ )
132
+
133
+ total = sum(c for _, c in lang_dna)
134
+ content = Text()
135
+ for idx, (lang, count) in enumerate(lang_dna[:6]):
136
+ frac = count / total if total else 0
137
+ pct = round(frac * 100)
138
+ bar = _bar(frac, width=bar_width)
139
+ color = _LANG_COLORS[idx % len(_LANG_COLORS)]
140
+ lang_str = (lang[:13] + "…" if len(lang) > 14 else lang).ljust(14)
141
+ content.append(f" {lang_str}", style="bold white")
142
+ content.append(bar, style=color)
143
+ content.append(f" {pct:>3}%\n", style="dim white")
144
+
145
+ return Panel(
146
+ content,
147
+ title="[bold cyan]LANGUAGE DNA[/bold cyan]",
148
+ border_style="cyan",
149
+ padding=(0, 1),
150
+ )
151
+
152
+
153
+ def render_evolution(data: Dict[str, Any]) -> Panel:
154
+ evo = data["evolution"]
155
+ content = Text()
156
+ year = evo.get("earliest_year")
157
+ year_str = f" ({year})" if year else ""
158
+
159
+ content.append(" Started with: ", style="dim white")
160
+ content.append(f"{evo['early']}{year_str}\n", style="bold white")
161
+ content.append(" Now building: ", style="dim white")
162
+ content.append(f"{evo['recent']}\n", style="bold bright_cyan")
163
+ content.append(" Trajectory: ", style="dim white")
164
+ content.append(evo.get("trajectory", ""), style="italic bright_yellow")
165
+
166
+ return Panel(
167
+ content,
168
+ title="[bold green]EVOLUTION[/bold green]",
169
+ border_style="green",
170
+ padding=(0, 1),
171
+ )
172
+
173
+
174
+ def render_velocity(data: Dict[str, Any]) -> Panel:
175
+ vel = data["velocity"]
176
+ rpm = vel.get("repos_per_month", 0)
177
+ total = vel.get("total_repos", 0)
178
+ months = vel.get("months", 0)
179
+ label = data["velocity_label"]
180
+
181
+ content = Text()
182
+ content.append(f" {total} repos", style="bold bright_white")
183
+ content.append(" over ", style="dim white")
184
+ content.append(f"~{months} months", style="bold bright_white")
185
+ content.append(f" = {rpm} repos/month\n", style="bright_yellow")
186
+ content.append(f" That's {label}\n", style="italic dim white")
187
+
188
+ return Panel(
189
+ content,
190
+ title="[bold yellow]VELOCITY[/bold yellow]",
191
+ border_style="yellow",
192
+ padding=(0, 1),
193
+ )
194
+
195
+
196
+ def render_star_power(data: Dict[str, Any]) -> Panel:
197
+ sp = data["star_power"]
198
+ total = sp["total"]
199
+ top_repo = sp.get("top_repo") or "—"
200
+ top_stars = sp.get("top_stars", 0)
201
+ repos_analyzed = data["repos_analyzed"]
202
+
203
+ content = Text()
204
+ content.append(f" Total stars: ", style="dim white")
205
+ content.append(f"{total}\n", style="bold bright_yellow")
206
+ content.append(f" Best performer: ", style="dim white")
207
+ content.append(f"{top_repo}", style="bold white")
208
+ content.append(f" ({top_stars} {'star' if top_stars == 1 else 'stars'})\n", style="dim white")
209
+
210
+ if total == 0 and repos_analyzed >= 5:
211
+ content.append(
212
+ " Untapped potential: high output, early-stage discovery\n",
213
+ style="italic dim yellow",
214
+ )
215
+ elif total >= 100:
216
+ content.append(
217
+ " Community recognition: established open-source presence\n",
218
+ style="italic bright_green",
219
+ )
220
+
221
+ return Panel(
222
+ content,
223
+ title="[bold yellow]STAR POWER[/bold yellow]",
224
+ border_style="yellow",
225
+ padding=(0, 1),
226
+ )
227
+
228
+
229
+ def render_scores(data: Dict[str, Any]) -> Panel:
230
+ # Dynamically choose bar width so the full line fits in the terminal
231
+ # Layout: 2 indent + 16 label + bar + 2 + 7 score + 2 + note(~20) + 2 border = ~51 + bar
232
+ avail = max(10, _terminal_width() - 53)
233
+ bar_w = min(20, avail)
234
+ scores = [
235
+ ("Specialization", data["spec_score"], "(focused)"),
236
+ ("Documentation", data["doc_score"], "(has description)"),
237
+ ("Open Source", data["oss_score"], "(has license)"),
238
+ ("Velocity", data["velocity_score"], "(ships fast)"),
239
+ ]
240
+
241
+ content = Text()
242
+ for label, score, note in scores:
243
+ frac = score / 100
244
+ bar = _bar(frac, width=bar_w)
245
+ color = "bright_green" if score >= 70 else ("bright_yellow" if score >= 40 else "bright_red")
246
+ label_str = label.ljust(16)
247
+ content.append(f" {label_str}", style="dim white")
248
+ content.append(bar, style=color)
249
+ content.append(f" {score:>3}/100 ", style="bold white")
250
+ content.append(f"{note}\n", style="dim white")
251
+
252
+ return Panel(
253
+ content,
254
+ title="[bold magenta]SCORES[/bold magenta]",
255
+ border_style="magenta",
256
+ padding=(0, 1),
257
+ )
258
+
259
+
260
+ def render_traits(data: Dict[str, Any]) -> Panel:
261
+ traits: List[Dict[str, str]] = data["traits"]
262
+ if not traits:
263
+ return Panel(
264
+ Text("Not enough data to compute traits.", style="dim"),
265
+ title="[bold]DEVELOPER TRAITS[/bold]",
266
+ border_style="bright_white",
267
+ )
268
+
269
+ content = Text()
270
+ for t in traits:
271
+ sign = t["sign"]
272
+ if sign == "+":
273
+ content.append(" + ", style="bold bright_green")
274
+ else:
275
+ content.append(" - ", style="bold bright_red")
276
+ content.append(t["text"] + "\n", style="white")
277
+
278
+ return Panel(
279
+ content,
280
+ title="[bold]DEVELOPER TRAITS[/bold]",
281
+ border_style="bright_white",
282
+ padding=(0, 1),
283
+ )
284
+
285
+
286
+ def render_insight(data: Dict[str, Any]) -> Panel:
287
+ insight = data["insight"]
288
+ # Use panel padding for left indent; let Rich handle wrapping
289
+ return Panel(
290
+ Text(insight, style="italic bright_white"),
291
+ title="[bold bright_green]DNA INSIGHT[/bold bright_green]",
292
+ border_style="bright_green",
293
+ padding=(0, 2),
294
+ )
295
+
296
+
297
+ def render_footer(data: Dict[str, Any]) -> Text:
298
+ t = Text(justify="center")
299
+ t.append("\n Generated by ", style="dim")
300
+ t.append("dev-dna", style="bold bright_green")
301
+ t.append(" — pip install dev-dna\n", style="dim")
302
+ return t
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Full report rendering
307
+ # ---------------------------------------------------------------------------
308
+
309
+
310
+ def print_report(data: Dict[str, Any]) -> None:
311
+ console.print()
312
+ console.print(render_header(data))
313
+ console.print()
314
+ console.print(render_archetype(data))
315
+ console.print()
316
+ console.print(render_language_dna(data))
317
+ console.print()
318
+ console.print(render_evolution(data))
319
+ console.print()
320
+ console.print(render_velocity(data))
321
+ console.print()
322
+ console.print(render_star_power(data))
323
+ console.print()
324
+ console.print(render_scores(data))
325
+ console.print()
326
+ console.print(render_traits(data))
327
+ console.print()
328
+ console.print(render_insight(data))
329
+ console.print(render_footer(data))
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # Comparison mode
334
+ # ---------------------------------------------------------------------------
335
+
336
+
337
+ def _compact_lang_bar(lang_dna: List[Tuple[str, int]], bar_width: int = 16) -> Text:
338
+ total = sum(c for _, c in lang_dna) or 1
339
+ t = Text()
340
+ for idx, (lang, count) in enumerate(lang_dna[:5]):
341
+ frac = count / total
342
+ pct = round(frac * 100)
343
+ bar = _bar(frac, width=bar_width)
344
+ color = _LANG_COLORS[idx % len(_LANG_COLORS)]
345
+ t.append(f" {lang:<12}", style="bold white")
346
+ t.append(bar, style=color)
347
+ t.append(f" {pct:>3}%\n", style="dim white")
348
+ return t
349
+
350
+
351
+ def print_comparison(data_a: Dict[str, Any], data_b: Dict[str, Any]) -> None:
352
+ console.print()
353
+ console.rule("[bold bright_green]DEV DNA — HEAD TO HEAD[/bold bright_green]")
354
+ console.print()
355
+
356
+ def _side_panel(data: Dict[str, Any]) -> Panel:
357
+ content = Text()
358
+ content.append(f"github.com/{data['username']}\n", style="bold bright_white")
359
+ if data["name"] != data["username"]:
360
+ content.append(f"{data['name']}\n", style="dim white")
361
+ content.append(f"\nARCHETYPE: {data['archetype_label']}\n\n", style="bold bright_yellow")
362
+
363
+ # Language DNA
364
+ content.append("LANGUAGE DNA\n", style="bold cyan")
365
+ total = sum(c for _, c in data["lang_dna"]) or 1
366
+ for idx, (lang, count) in enumerate(data["lang_dna"][:5]):
367
+ frac = count / total
368
+ pct = round(frac * 100)
369
+ bar = _bar(frac, width=14)
370
+ color = _LANG_COLORS[idx % len(_LANG_COLORS)]
371
+ lang_short = (lang[:11] + "…" if len(lang) > 12 else lang).ljust(12)
372
+ content.append(f" {lang_short}", style="bold white")
373
+ content.append(bar, style=color)
374
+ content.append(f" {pct:>3}%\n", style="dim white")
375
+
376
+ # Scores
377
+ content.append("\nSCORES\n", style="bold magenta")
378
+ for label, score in [
379
+ ("Specialization", data["spec_score"]),
380
+ ("Documentation", data["doc_score"]),
381
+ ("Open Source", data["oss_score"]),
382
+ ("Velocity", data["velocity_score"]),
383
+ ]:
384
+ bar = _bar(score / 100, width=14)
385
+ color = "bright_green" if score >= 70 else ("bright_yellow" if score >= 40 else "bright_red")
386
+ content.append(f" {label:<16}", style="dim white")
387
+ content.append(bar, style=color)
388
+ content.append(f" {score:>3}\n", style="bold white")
389
+
390
+ # Star power
391
+ sp = data["star_power"]
392
+ content.append(f"\nSTARS: {sp['total']}", style="bold bright_yellow")
393
+ content.append(f" (best: {sp.get('top_repo','—')} {sp['top_stars']})\n", style="dim white")
394
+
395
+ # Velocity
396
+ vel = data["velocity"]
397
+ content.append(
398
+ f"VELOCITY: {vel['repos_per_month']} repos/month "
399
+ f"({vel['total_repos']} repos)\n",
400
+ style="bright_white",
401
+ )
402
+
403
+ # Traits
404
+ content.append("\nTRAITS\n", style="bold white")
405
+ for t in data["traits"][:4]:
406
+ sign_color = "bright_green" if t["sign"] == "+" else "bright_red"
407
+ content.append(f" {t['sign']} ", style=f"bold {sign_color}")
408
+ content.append(t["text"] + "\n", style="white")
409
+
410
+ return Panel(
411
+ content,
412
+ title=f"[bold]{data['username']}[/bold]",
413
+ border_style="bright_cyan" if data == data_a else "bright_magenta",
414
+ padding=(0, 1),
415
+ )
416
+
417
+ cols = Columns([_side_panel(data_a), _side_panel(data_b)], equal=True, expand=True)
418
+ console.print(cols)
419
+ console.print()
420
+
421
+ # Verdict
422
+ a_total = data_a["spec_score"] + data_a["doc_score"] + data_a["oss_score"] + data_a["velocity_score"]
423
+ b_total = data_b["spec_score"] + data_b["doc_score"] + data_b["oss_score"] + data_b["velocity_score"]
424
+ if a_total > b_total:
425
+ verdict = f"[bold bright_cyan]{data_a['username']}[/bold bright_cyan] edges ahead on aggregate scores."
426
+ elif b_total > a_total:
427
+ verdict = f"[bold bright_magenta]{data_b['username']}[/bold bright_magenta] edges ahead on aggregate scores."
428
+ else:
429
+ verdict = "These developers are evenly matched — different strengths, same drive."
430
+ console.print(Panel(verdict, title="[bold]VERDICT[/bold]", border_style="bright_white"))
431
+ console.print()
432
+
433
+
434
+ # ---------------------------------------------------------------------------
435
+ # CLI definition
436
+ # ---------------------------------------------------------------------------
437
+
438
+
439
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
440
+ @click.argument("username")
441
+ @click.option(
442
+ "--token", "-t",
443
+ default=None,
444
+ envvar="GITHUB_TOKEN",
445
+ metavar="TOKEN",
446
+ help="GitHub personal access token (or set GITHUB_TOKEN env var). Increases rate limits.",
447
+ )
448
+ @click.option(
449
+ "--json", "output_json",
450
+ is_flag=True,
451
+ default=False,
452
+ help="Output raw JSON instead of the formatted report.",
453
+ )
454
+ @click.option(
455
+ "--compare", "-c",
456
+ default=None,
457
+ metavar="USERNAME",
458
+ help="Compare with another GitHub user side-by-side.",
459
+ )
460
+ def main(username: str, token: Optional[str], output_json: bool, compare: Optional[str]) -> None:
461
+ """Analyze a GitHub developer's DNA.
462
+
463
+ \b
464
+ Examples:
465
+ dev-dna torvalds
466
+ dev-dna torvalds --json
467
+ dev-dna gvanrossum --compare torvalds
468
+ dev-dna myuser --token ghp_xxxx
469
+ """
470
+ try:
471
+ with console.status(
472
+ f"[bright_green]Fetching GitHub data for [bold]{username}[/bold]...[/bright_green]",
473
+ spinner="dots",
474
+ ):
475
+ data = analyze_user(username, token=token)
476
+
477
+ if compare:
478
+ with console.status(
479
+ f"[bright_magenta]Fetching GitHub data for [bold]{compare}[/bold]...[/bright_magenta]",
480
+ spinner="dots",
481
+ ):
482
+ data_b = analyze_user(compare, token=token)
483
+
484
+ if output_json:
485
+ click.echo(json.dumps({"a": data, "b": data_b}, indent=2, default=str))
486
+ else:
487
+ print_comparison(data, data_b)
488
+ else:
489
+ if output_json:
490
+ click.echo(json.dumps(data, indent=2, default=str))
491
+ else:
492
+ print_report(data)
493
+
494
+ except NotFoundError as exc:
495
+ console.print(f"\n[bold red]Not found:[/bold red] {exc}\n")
496
+ sys.exit(1)
497
+ except RateLimitError as exc:
498
+ console.print(f"\n[bold yellow]Rate limited:[/bold yellow] {exc}\n")
499
+ sys.exit(1)
500
+ except GitHubError as exc:
501
+ console.print(f"\n[bold red]GitHub API error:[/bold red] {exc}\n")
502
+ sys.exit(1)
503
+ except KeyboardInterrupt:
504
+ console.print("\n[dim]Interrupted.[/dim]\n")
505
+ sys.exit(0)
506
+
507
+
508
+ if __name__ == "__main__":
509
+ main()
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: dev-dna
3
+ Version: 0.1.0
4
+ Summary: 23andMe for developers — analyze any GitHub user's developer DNA
5
+ Author: dev-dna contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/lakshmisravyavedantham/dev-dna
8
+ Project-URL: Repository, https://github.com/lakshmisravyavedantham/dev-dna
9
+ Project-URL: Issues, https://github.com/lakshmisravyavedantham/dev-dna/issues
10
+ Keywords: github,developer,analytics,cli,profile
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: click>=8.0
27
+ Requires-Dist: rich>=13.0
28
+ Dynamic: license-file
29
+
30
+ # dev-dna
31
+
32
+ **23andMe for developers.** Point it at any GitHub username and get a rich terminal report of their Developer DNA — dominant languages, evolution over time, commit personality archetype, specialization vs. generalist score, star power, and a shareable summary.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install dev-dna
38
+ ```
39
+
40
+ Or install from source:
41
+
42
+ ```bash
43
+ git clone https://github.com/lakshmisravyavedantham/dev-dna
44
+ cd dev-dna
45
+ pip install -e .
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```bash
51
+ # Analyze any public GitHub profile
52
+ dev-dna torvalds
53
+
54
+ # Output raw JSON
55
+ dev-dna torvalds --json
56
+
57
+ # Pass a GitHub token for higher rate limits (60 → 5000 req/hr)
58
+ dev-dna torvalds --token ghp_xxx
59
+ # or set the env var:
60
+ export GITHUB_TOKEN=ghp_xxx
61
+ dev-dna torvalds
62
+
63
+ # Compare two developers side-by-side
64
+ dev-dna gvanrossum --compare torvalds
65
+ ```
66
+
67
+ ## Sample Output
68
+
69
+ ```
70
+ ╔══════════════════════════════════════════════════════════╗
71
+ ║ DEV DNA ANALYSIS ║
72
+ ║ github.com/lakshmisravyavedantham ║
73
+ ╚══════════════════════════════════════════════════════════╝
74
+
75
+ ARCHETYPE: The Toolsmith
76
+ "Ships fast. Builds tools that make other developers' lives
77
+ easier. Prefers CLI over GUI. Has opinions about your commit
78
+ messages."
79
+
80
+ LANGUAGE DNA
81
+ Python ████████████████████ 67%
82
+ TypeScript ██████░░░░░░░░░░░░░░ 18%
83
+ JavaScript ████░░░░░░░░░░░░░░░░ 10%
84
+ HTML ██░░░░░░░░░░░░░░░░░░ 5%
85
+
86
+ EVOLUTION
87
+ Started with: Python (2022)
88
+ Now building: Python + TypeScript
89
+ Trajectory: Expanding into full-stack
90
+
91
+ VELOCITY
92
+ 108 repos over ~24 months = 4.5 repos/month
93
+ That's faster than 94% of GitHub developers
94
+
95
+ STAR POWER
96
+ Total stars: 4
97
+ Best performer: Coherence (1 star)
98
+ Untapped potential: high output, early-stage discovery
99
+
100
+ SCORES
101
+ Specialization ████████████░░░░░░░░ 72/100 (focused depth)
102
+ Documentation ████████░░░░░░░░░░░░ 41/100 (descriptions + readability)
103
+ Open Source ███████░░░░░░░░░░░░░ 38/100 (repos with license)
104
+ Velocity ████████████████████ 96/100 (shipping speed)
105
+
106
+ DEVELOPER TRAITS
107
+ + Prolific builder — top 5% by repo count
108
+ + AI-native — 60%+ of recent work involves LLMs
109
+ - Discovery gap — great code, low visibility
110
+ - Documentation debt — many repos lack descriptions
111
+
112
+ DNA INSIGHT
113
+ You build at a rare velocity. The bottleneck isn't output —
114
+ it's surface area. Adding descriptions, PyPI packages, and
115
+ one viral project would unlock the stars your code deserves.
116
+ ```
117
+
118
+ ## What it analyzes
119
+
120
+ | Signal | Description |
121
+ |---|---|
122
+ | **Language DNA** | Top 6 languages by repo count, shown as a terminal bar chart |
123
+ | **Archetype** | Personality classification: Toolsmith, Researcher, Full-Stack Builder, Data Engineer, Hacker, or Craftsperson |
124
+ | **Specialization Score** | 0–100. High = deep in one domain. Low = polyglot generalist |
125
+ | **Velocity** | Repos created per month; scored against a GitHub-wide distribution |
126
+ | **Star Power** | Total stars + top repo; flags discovery gap for prolific but under-starred devs |
127
+ | **Evolution** | Primary language in oldest 20% of repos vs newest 20% — shows trajectory |
128
+ | **Open Source Signal** | % of repos with a license |
129
+ | **Documentation Score** | % of repos with a description |
130
+
131
+ ## Architecture
132
+
133
+ ```
134
+ src/dev_dna/
135
+ ├── __init__.py — public API
136
+ ├── cli.py — Click CLI + Rich rendering
137
+ └── analyzer.py — GitHub API calls + metric computation
138
+ ```
139
+
140
+ - Uses only `urllib.request` for HTTP (no `requests` library)
141
+ - Zero external dependencies beyond `click` and `rich`
142
+ - Supports `GITHUB_TOKEN` env var or `--token` flag for 5000 req/hr rate limit
143
+ - Gracefully handles: rate limits, private-only accounts, 0-star accounts, missing data
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,9 @@
1
+ dev_dna/__init__.py,sha256=Snip_hK6IY02DME-vHKoBscQxuzdp5ZEka-AJcF00V8,336
2
+ dev_dna/analyzer.py,sha256=cGiHnRLITPlgC3SPhRHQuWfKWQdR0Xkicbxt-8eYzHk,20262
3
+ dev_dna/cli.py,sha256=zC_8qcbF-ahFY-Njd2u0F8XQnzrmIBfoIaDi_eiBoms,16872
4
+ dev_dna-0.1.0.dist-info/licenses/LICENSE,sha256=a1JqlBXUwW3JVspjcrTggLsSzoVyM80GVlA6J9debzU,1077
5
+ dev_dna-0.1.0.dist-info/METADATA,sha256=EI3LCVF4nmZ2az4U7vHtmqlQqkUXMfxHjekaUzD_jjI,5517
6
+ dev_dna-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
7
+ dev_dna-0.1.0.dist-info/entry_points.txt,sha256=ZpBFNo2kUBUTBi6oNULecy20dx9ZmnfjPoyP3KByRfs,45
8
+ dev_dna-0.1.0.dist-info/top_level.txt,sha256=M6gdaYdfGaJ8LEs-Gv1Xgynbf51lPNH3gyJbg313eAE,8
9
+ dev_dna-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dev-dna = dev_dna.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dev-dna contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dev_dna