Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 33 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,97 @@
A command-line tool for managing Credential Engine platform resources.

## Requirements

- Python 3.10 or later
- `pip`

## Installation

> **Recommended:** install inside a virtual environment. This avoids file-permission errors, PATH conflicts, and "invalid distribution" warnings from previous installs.

### Option 1: Install from source with a virtual environment (recommended)

**macOS / Linux:**

```bash
git clone https://github.com/CredentialEngine/ce-cli.git
cd ce-cli

python3 -m venv .venv
source .venv/bin/activate

pip install -e .
```

**Windows (PowerShell):**

```powershell
git clone https://github.com/CredentialEngine/ce-cli.git
cd ce-cli

python -m venv .venv
.venv\Scripts\activate

pip install -e .
```

When the venv is active you'll see `(.venv)` in your prompt, and `ce` is automatically on `PATH`. To leave the venv, run `deactivate`. To re-enter it next time, run the activate command above from the project folder.

### Option 2: Install from a release

1. Download the `.whl` from the [Releases page](https://github.com/CredentialEngine/ce-cli/releases).
2. Install it (inside a venv is still recommended):
```bash
pip install ce_cli-<version>-py3-none-any.whl
```

### Upgrading
### Upgrading

From a release `.whl`:

```bash
pip install --upgrade ce_cli-<version>-py3-none-any.whl
```

From source: `git pull` and re-run `pip install -e .` inside your venv.

### Option 3: Install from source without a venv

```bash
git clone https://github.com/CredentialEngine/ce-cli.git
cd ce-cli
pip install -e .
```

This puts the `ce` command on your `$PATH`. On Windows, see the [Windows PATH note](#windows-path-note) below if `ce` isn't found.

### Verify the installation

```bash
ce --help
```

You should see the top-level command list.

### Windows PATH note

When you install on Windows without a venv, pip often drops ce.exe into your user Scripts folder, which Windows doesn't put on PATH by default:

```
C:\Users\<you>\AppData\Roaming\Python\Python310\Scripts
```

If `ce --help` fails after install, either use a venv (recommended) or add that folder to your User PATH:

```powershell
[Environment]::SetEnvironmentVariable(
"Path",
$env:Path + ";C:\Users\$env:USERNAME\AppData\Roaming\Python\Python310\Scripts",
"User"
)
```

Then **close and reopen PowerShell** existing windows won't pick up the new PATH.


Then **close and reopen PowerShell** existing windows won't pick up the new PATH.

## Quick start

Expand Down Expand Up @@ -210,8 +209,8 @@ Verifies the JWT signatures and publishes each issuer to the IIR. Pass `--yes`/`
| `VerificationMethod` | Yes | Full verification method ID (e.g., `did:key:z6Mk...#z6Mk...`) |
| `Algorithm` | For did:key | `Ed25519`, `secp256k1`, `P-256`, or `X25519` |
| `PrivateKey` | Yes | Multibase-encoded private key for signing |
| `ValidFrom` | No | Date when the issuer becomes valid (MM/DD/YYYY) |
| `ValidUntil` | No | Date when the issuer expires (MM/DD/YYYY) |
| `ValidFrom` | No | Date when the issuer becomes valid (YYYY-MM-DD) |
| `ValidUntil` | No | Date when the issuer expires (YYYY-MM-DD) |

Example:

Expand Down
2 changes: 1 addition & 1 deletion ce/commands/iir.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def bulk_sign(csv_path: str, output_path: str | None, errors_path: str | None) -

\b
Input CSV columns (optional):
ValidFrom, ValidUntil (format: MM/DD/YYYY)
ValidFrom, ValidUntil (format: YYYY-MM-DD)

\b
Produces two files:
Expand Down
38 changes: 38 additions & 0 deletions ce/iir/api/iir_client/models/pub_issuer_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class PubIssuerDTO:
logo_uri (str | Unset):
valid_from (datetime.datetime | Unset):
valid_until (datetime.datetime | Unset):
date_valid_from (datetime.datetime | Unset):
date_valid_until (datetime.datetime | Unset):
"""

ctid: str | Unset = UNSET
Expand All @@ -48,6 +50,8 @@ class PubIssuerDTO:
logo_uri: str | Unset = UNSET
valid_from: datetime.datetime | Unset = UNSET
valid_until: datetime.datetime | Unset = UNSET
date_valid_from: datetime.datetime | Unset = UNSET
date_valid_until: datetime.datetime | Unset = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)


Expand Down Expand Up @@ -79,6 +83,14 @@ def to_dict(self) -> dict[str, Any]:
if not isinstance(self.valid_until, Unset):
valid_until = self.valid_until.isoformat()

date_valid_from: str | Unset = UNSET
if not isinstance(self.date_valid_from, Unset):
date_valid_from = self.date_valid_from.isoformat()

date_valid_until: str | Unset = UNSET
if not isinstance(self.date_valid_until, Unset):
date_valid_until = self.date_valid_until.isoformat()


field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
Expand All @@ -104,6 +116,10 @@ def to_dict(self) -> dict[str, Any]:
field_dict["ValidFrom"] = valid_from
if valid_until is not UNSET:
field_dict["ValidUntil"] = valid_until
if date_valid_from is not UNSET:
field_dict["DateValidFrom"] = date_valid_from
if date_valid_until is not UNSET:
field_dict["DateValidUntil"] = date_valid_until

return field_dict

Expand Down Expand Up @@ -148,6 +164,26 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:



_date_valid_from = d.pop("DateValidFrom", UNSET)
date_valid_from: datetime.datetime | Unset
if isinstance(_date_valid_from, Unset):
date_valid_from = UNSET
else:
date_valid_from = isoparse(_date_valid_from)




_date_valid_until = d.pop("DateValidUntil", UNSET)
date_valid_until: datetime.datetime | Unset
if isinstance(_date_valid_until, Unset):
date_valid_until = UNSET
else:
date_valid_until = isoparse(_date_valid_until)




pub_issuer_dto = cls(
ctid=ctid,
did=did,
Expand All @@ -159,6 +195,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
logo_uri=logo_uri,
valid_from=valid_from,
valid_until=valid_until,
date_valid_from=date_valid_from,
date_valid_until=date_valid_until,
)


Expand Down
28 changes: 18 additions & 10 deletions ce/iir/did_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import base64
from datetime import timezone
import json
import re
from functools import lru_cache
Expand Down Expand Up @@ -88,12 +89,12 @@ def validate_date(value: str, field_name: str) -> tuple[bool, str, str]:
return True, "", ""
from datetime import datetime, timezone
try:
parsed = datetime.strptime(value, "%m/%d/%Y")
utc_dt = parsed.replace(tzinfo=timezone.utc)
parsed = datetime.strptime(value, "%Y-%m-%d")
utc_dt = parsed.replace(hour=0, minute=0, second=0, tzinfo=timezone.utc)
iso_value = utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
return True, "", iso_value
except ValueError:
return False, f"{field_name} must be in MM/DD/YYYY format, got '{value}'", ""
return False, f"{field_name} must be in YYYY-MM-DD format, got '{value}'", ""


def validate_date_range(valid_from: str, valid_until: str) -> tuple[bool, str]:
Expand Down Expand Up @@ -433,16 +434,18 @@ def call_submit_to_iir(
) -> tuple[bool, str]:
from datetime import datetime

def _to_dt_or_unset(value: str):
def _to_iso_z(value: str) -> str:
if not value:
return UNSET
return ""
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
try:
return datetime.strptime(value, "%Y-%m-%d")
dt = datetime.strptime(value, "%Y-%m-%d")
except ValueError:
return UNSET
return ""
local_midnight = datetime(dt.year, dt.month, dt.day).astimezone()
return local_midnight.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

try:
client = _client(token, publisher_base, ssl_verify)
Expand All @@ -456,10 +459,15 @@ def _to_dt_or_unset(value: str):
subject_web_page=subject_web_page,
logo_base_64=logo_base64 if logo_base64 else UNSET,
logo_uri=logo_uri if logo_uri else UNSET,
valid_from=_to_dt_or_unset(valid_from),
valid_until=_to_dt_or_unset(valid_until),
)

df = _to_iso_z(valid_from)
du = _to_iso_z(valid_until)
if df:
body.additional_properties["DateValidFrom"] = df
if du:
body.additional_properties["DateValidUntil"] = du

response = submit_to_iir_sync(client=client, body=body)

if response.status_code == 409:
Expand Down
8 changes: 8 additions & 0 deletions ce/iir/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,14 @@
"ValidUntil": {
"format": "date-time",
"type": "string"
},
"DateValidFrom": {
"format": "date-time",
"type": "string"
},
"DateValidUntil": {
"format": "date-time",
"type": "string"
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions ce/iir/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,14 @@
"ValidUntil": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between openapi.json and swagger.json?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swagger generated is swagger2.0 but openapi-python-client supports openapi 3.0

so the current scripts converts from swagger to openapi

"format": "date-time",
"type": "string"
},
"DateValidFrom": {
"format": "date-time",
"type": "string"
},
"DateValidUntil": {
"format": "date-time",
"type": "string"
}
}
}
Expand Down
Loading
Loading