Metadata-Version: 2.4
Name: sd-metrics-lib
Version: 3.0
Summary: Library to calculate various metrics of software development process
Author-email: Igor Zarvanskyi <iclutcher@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/clutcher/sd-metrics-lib
Project-URL: Bug Tracker, https://github.com/clutcher/sd-metrics-lib/issues
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-dateutil>=2.8
Provides-Extra: jira
Requires-Dist: atlassian-python-api>=3.0; extra == "jira"
Provides-Extra: azure
Requires-Dist: azure-devops>=7.1.0b4; extra == "azure"
Requires-Dist: msrest>=0.7; extra == "azure"
Dynamic: license-file

# sd-metrics-lib

Python library for calculating various metrics related to the software development process. Provides developer and team velocity
calculations based on data from Jira and Azure DevOps. Metrics calculation classes use interfaces, so the library can be easily extended
with other data providers (e.g., Trello, Asana) from application code.

## Architecture and API reference

### Overview

This library separates metric calculation from data sourcing. Calculators operate on abstract provider interfaces so you can plug in Jira, Azure DevOps, or your own sources. Below is a structured overview by package with links to the key classes you will use.

### Calculators

- Module: `calculators.metrics_calculator`
    - `MetricCalculator` (abstract): Base interface for all metric calculators (`calculate()`).
- Module: `calculators.velocity_calculator`
    - `AbstractMetricCalculator` (abstract): Adds lazy extraction and shared `calculate()` workflow.
    - `UserVelocityCalculator`: Per-user velocity (story points per time unit). Requires `TaskProvider`, `StoryPointExtractor`, `WorklogExtractor`.
    - `GeneralizedTeamVelocityCalculator`: Team velocity (total story points per time unit). Requires `TaskProvider`, `StoryPointExtractor`, `TaskTotalSpentTimeExtractor`.

### Data providers

- Module: `data_providers.task_provider`
    - `TaskProvider` (abstract): Fetches a list of tasks/work items.
    - `ProxyTaskProvider`: Wraps a pre-fetched list of tasks (useful for tests/custom sources).
    - `CachingTaskProvider`: Caches results of any `TaskProvider`. Cache key is built from `provider.query` and `provider.additional_fields`; works with any dict-like cache (e.g., `cachetools.TTLCache`).
- Module: `data_providers.story_point_extractor`
    - `StoryPointExtractor` (abstract)
    - `ConstantStoryPointExtractor`: Returns a constant number of story points.
- Module: `data_providers.worklog_extractor`
    - `WorklogExtractor` (abstract): Returns mapping `user -> seconds` for a task.
    - `TaskTotalSpentTimeExtractor` (abstract): Returns total time-in-seconds spent on a task.
    - `ChainedWorklogExtractor`: Tries extractors in order and returns the first non-empty result.
- Module: `data_providers.worktime_extractor`
    - `WorkTimeExtractor` (abstract)
    - `SimpleWorkTimeExtractor`: Heuristic working-time calculator (caps to a day; ignores < 15 min intervals).
    - `BoundarySimpleWorkTimeExtractor`: Limits calculation to a boundary window, then delegates to `SimpleWorkTimeExtractor`.
- Module: `data_providers.abstract_worklog_extractor`
    - `AbstractStatusChangeWorklogExtractor` (abstract): Derives work time from assignment/status change history; attributes time to assignee and respects optional user filters and `WorkTimeExtractor`.

#### Jira

- Module: `data_providers.jira.task_provider`
    - `JiraTaskProvider`: Fetch tasks by `JQL` via `atlassian-python-api`; supports paging and optional `ThreadPoolExecutor`.
- Module: `data_providers.jira.query_builder`
    - `JiraSearchQueryBuilder`: Builder for `JQL` (project, status, date range, type, team, custom raw filters, order by)
- Module: `data_providers.jira.story_point_extractor`
    - `JiraCustomFieldStoryPointExtractor`: Reads a numeric custom field; supports default value.
    - `JiraTShirtStoryPointExtractor`: Maps T-shirt sizes (e.g., `S`/`M`/`L`) to numbers from a custom field.
- Module: `data_providers.jira.worklog_extractor`
    - `JiraWorklogExtractor`: Aggregates time from native Jira worklogs (optionally includes subtasks); optional user filter.
    - `JiraStatusChangeWorklogExtractor`: Derives time from changelog (status/assignee changes); supports username vs `accountId` and status names vs codes; uses a `WorkTimeExtractor`.
    - `JiraResolutionTimeTaskTotalSpentTimeExtractor`: Total time from `created` to `resolutiondate`.

#### Azure DevOps

- Module: `data_providers.azure.task_provider`
    - `AzureTaskProvider`: Executes `WIQL`; fetches work items in pages (sync or `ThreadPoolExecutor`); can expand updates for status-change-based calculations.
- Module: `data_providers.azure.query_builder`
    - `AzureSearchQueryBuilder`: Builder for WIQL (project, status, date range, type, area path/team, custom raw filters, order by)
- Module: `data_providers.azure.story_point_extractor`
    - `AzureStoryPointExtractor`: Reads story points from a field (default `Microsoft.VSTS.Scheduling.StoryPoints`); robust parsing with default.
- Module: `data_providers.azure.worklog_extractor`
    - `AzureStatusChangeWorklogExtractor`: Derives per-user time from work item updates (assignment/state changes); supports status filters; uses `WorkTimeExtractor`.
    - `AzureTaskTotalSpentTimeExtractor`: Total time from `System.CreatedDate` to `Microsoft.VSTS.Common.ClosedDate`.

### Utilities

- Module: `data_providers.utils.enums`
    - `VelocityTimeUnit` (Enum): values `HOUR`, `DAY`, `WEEK`, `MONTH`
    - `HealthStatus` (Enum): values `GREEN`, `YELLOW`, `RED`
    - `SeniorityLevel` (Enum): values `JUNIOR`, `MIDDLE`, `SENIOR`
- Module: `data_providers.utils.query_utils`
    - `TimeRangeGenerator`: Iterator producing date ranges for the requested `VelocityTimeUnit`
- Module: `data_providers.utils.story_point_utils`
    - `TShirtMapping`: Helper to convert between T-shirt sizes (`XS`/`S`/`M`/`L`/`XL`) and story points using default mapping `xs=1`, `s=5`, `m=8`, `l=13`, `xl=21`.
      Methods: `convert_into_points(size: str) -> int`, `convert_into_size(story_point: int) -> str`.
- Module: `calculators.utils.time_utils`
    - `convert_time(spent_time_in_seconds, VelocityTimeUnit) -> float`
    - `get_seconds_in_day() -> int`
    - Constants: `SECONDS_IN_HOUR`, `WORKING_HOURS_PER_DAY`, `WORKING_DAYS_PER_WEEK`, `WORKING_WEEKS_IN_MONTH`, `WEEKDAY_FRIDAY`

### Public API exports (import shortcuts)

- `calculators.__init__`: exports `UserVelocityCalculator`, `GeneralizedTeamVelocityCalculator`
- `data_providers.__init__`: exports `TaskProvider`, `ProxyTaskProvider`, `CachingTaskProvider`, `StoryPointExtractor`, `WorklogExtractor`, `ChainedWorklogExtractor`, `TaskTotalSpentTimeExtractor`, `SimpleWorkTimeExtractor`, `BoundarySimpleWorkTimeExtractor`
- `data_providers.jira.__init__`: exports `JiraSearchQueryBuilder`, `JiraTaskProvider`, `JiraCustomFieldStoryPointExtractor`, `JiraTShirtStoryPointExtractor`, `JiraWorklogExtractor`, `JiraStatusChangeWorklogExtractor`, `JiraResolutionTimeTaskTotalSpentTimeExtractor`
- `data_providers.azure.__init__`: exports `AzureSearchQueryBuilder`, `AzureTaskProvider`, `AzureStoryPointExtractor`, `AzureStatusChangeWorklogExtractor`, `AzureTaskTotalSpentTimeExtractor`
- `data_providers.utils.__init__`: exports `VelocityTimeUnit`, `HealthStatus`, `SeniorityLevel`, `TimeRangeGenerator`, `TShirtMapping`

## Installation

Install core library:

```bash
pip install sd-metrics-lib
```

Optional extras for providers:

```bash
pip install sd-metrics-lib[jira]
pip install sd-metrics-lib[azure]
```

## Code examples

### Calculate amount of tickets developer resolves per day based on Jira ticket status change history.

This code should work on any project and give at least some data for analysis.

```python
from atlassian import Jira

from calculators import UserVelocityCalculator
from data_providers.utils import VelocityTimeUnit
from data_providers.jira.task_provider import JiraTaskProvider
from data_providers.jira.worklog_extractor import JiraStatusChangeWorklogExtractor
from data_providers.story_point_extractor import ConstantStoryPointExtractor

JIRA_SERVER = 'server_url'
JIRA_LOGIN = 'login'
JIRA_PASS = 'password'
jira_client = Jira(JIRA_SERVER, JIRA_LOGIN, JIRA_PASS, cloud=True)

jql = " project in ('TBC') AND resolutiondate >= 2022-08-01 "
task_provider = JiraTaskProvider(jira_client, jql, additional_fields=['changelog'])

story_point_extractor = ConstantStoryPointExtractor()
jira_worklog_extractor = JiraStatusChangeWorklogExtractor(['In Progress', 'In Development'])

velocity_calculator = UserVelocityCalculator(task_provider=task_provider,
                                             story_point_extractor=story_point_extractor,
                                             worklog_extractor=jira_worklog_extractor)
velocity = velocity_calculator.calculate(velocity_time_unit=VelocityTimeUnit.DAY)

print(velocity)
```

### Calculate amount of story points developer resolves per day based on Azure DevOps work items.

This example uses Azure DevOps WIQL to fetch closed items and derives time spent per user from status/assignment changes.
It also demonstrates enabling concurrency with a thread pool and caching results with a TTL cache.

```python
from cachetools import TTLCache
from concurrent.futures import ThreadPoolExecutor

from azure.devops.connection import Connection
from msrest.authentication import BasicAuthentication

from calculators import UserVelocityCalculator
from data_providers.utils import VelocityTimeUnit
from data_providers.azure.task_provider import AzureTaskProvider
from data_providers.azure.story_point_extractor import AzureStoryPointExtractor
from data_providers.azure.worklog_extractor import AzureStatusChangeWorklogExtractor
from data_providers.task_provider import CachingTaskProvider

# Caches and thread pools
JQL_CACHE = TTLCache(maxsize=100, ttl=600)
jira_fetch_executor = ThreadPoolExecutor(max_workers=100, thread_name_prefix="jira-fetch")

ORGANIZATION_URL = 'https://dev.azure.com/your_org'
PERSONAL_ACCESS_TOKEN = 'your_pat'

credentials = BasicAuthentication('', PERSONAL_ACCESS_TOKEN)
connection = Connection(base_url=ORGANIZATION_URL, creds=credentials)
wit_client = connection.clients.get_work_item_tracking_client()

wiql = """
       SELECT [System.Id]
       FROM workitems
       WHERE
           [System.TeamProject] = 'YourProject'
         AND [System.State] IN ('Closed', 'Done', 'Resolved')
         AND [System.WorkItemType] IN ('User Story', 'Bug')
         AND [Microsoft.VSTS.Common.ClosedDate] >= '2025-08-01'
       ORDER BY [System.ChangedDate] DESC \
       """

# Use thread pool and cache
task_provider = AzureTaskProvider(wit_client, query=wiql, thread_pool_executor=jira_fetch_executor)
task_provider = CachingTaskProvider(task_provider, cache=JQL_CACHE)

story_point_extractor = AzureStoryPointExtractor(default_story_points_value=1)
worklog_extractor = AzureStatusChangeWorklogExtractor(transition_statuses=['In Progress'])

velocity_calculator = UserVelocityCalculator(task_provider=task_provider,
                                             story_point_extractor=story_point_extractor,
                                             worklog_extractor=worklog_extractor)
velocity = velocity_calculator.calculate(velocity_time_unit=VelocityTimeUnit.DAY)

print(velocity)
```

## Version history

### 3.0

+ (Breaking) Rename all Issue* terms to Task* across API (IssueProvider -> TaskProvider, IssueTotalSpentTimeExtractor -> TaskTotalSpentTimeExtractor, etc.). Removed backward-compatibility aliases.
+ (Breaking) Change package and method names in JiraSearchQueryBuilder

+ (Feature) Introduce AzureSearchQueryBuilder
+ (Feature) AzureTaskProvider: make changelog history optional via additional fields
+ (Feature) Extend JiraSearchQueryBuilder: custom raw filters; filter by Team; open-ended resolution date
+ (Feature) Rewrite CachingTaskProvider to support Django caches
+ (Feature) Introduce AzureSearchQueryBuilder
+ (Bug Fix) Azure: fetch all tasks beyond 20k limit using stable pagination
+ (Bug Fix) Jira: do not fail on empty search results

### 2.0

+ (Feature) Add integration with Azure DevOps
+ (Breaking) Add a generic CachingIssueProvider to wrap any IssueProvider and remove CachingJiraIssueProvider

### 1.2.1

+ **(Improvement)** Add possibility to adjust init time
+ **(Bug Fix)** Fix bug with wrong cache data fetching
+ **(Bug Fix)** Fix bug in week time period end date resolving

### 1.2

+ **(Feature)** Added BoundarySimpleWorkTimeExtractor
+ **(Improvement)** Filter unneeded changelog items for better performance
+ **(Improvement)** Add T-Shirt to story points mapping util class
+ **(Improvement)** Add helper enums
+ **(Bug Fix)** Fix bug with story points returned instead of spent time
+ **(Bug Fix)** Fix bug with missing time for active status
+ **(Bug Fix)** Fix bug with passing class instances in extractor

### 1.1.4

+ **(Improvement)** Add multithreading support for JiraIssueProvider.

### 1.1.3

+ **(Feature)** Add CachingJiraIssueProvider.

### 1.1.2

+ **(Improvement)** Add story points getter for GeneralizedTeamVelocityCalculator.

### 1.1.1

+ **(Improvement)** Execute data fetching in Jira velocity calculators only once.
+ **(Improvement)** Add story points getter for Jira velocity calculators.

### 1.1

+ **(Feature)** Add team velocity calculator.
+ **(Improvement)** Add JQL filter for last modified data.
+ **(Bug Fix)** Fix wrong user resolving in JiraStatusChangeWorklogExtractor.
+ **(Bug Fix)** Fix resolving more time than spent period of time.
+ **(Bug Fix)** Fix Jira filter query joins without AND.

### 1.0.3

+ **(Improvement)** Add JiraIssueSearchQueryBuilder util class.
+ **(Improvement)** Add TimeRangeGenerator util class.
+ **(Bug Fix)** Fix filtering by status when no status list passed.

### 1.0.2

+ **(Bug Fix)** Fix package import exception after installing from pypi.

### 1.0

+ **(Feature)** Add user velocity calculator.
