Skip to content

API Reference

Reference for the modules most useful when working against Raaz directly - e.g. writing a third-party provider plugin (see Providers) or scripting around the local context.

Request flow

Every command that touches secrets follows the same path, regardless of which backend it ends up talking to:

flowchart LR
    CLI["CLI"] --> CTX["Context"] --> CLIENT["RaazClient"] --> PROVIDER(["Provider"]) --> BACKEND["Backend"]

RaazClient (the client facade below) is the only thing CLI commands talk to directly - it never imports a concrete provider itself, only the Provider protocol, looked up by name via get_provider(). That's the extension point: a third-party provider (see Providers) just needs to implement Provider and register itself under the raaz.providers entry point group - RaazClient, and every command built on it, works with it identically to the five built-in ones. local is the one built-in provider whose "backend" is actually EnvManager's on-disk store under ~/.raaz rather than a real cloud SDK - see Local env store below.

Provider protocol

Provider

Bases: Protocol

Source code in raaz\providers\protocol.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Provider(Protocol):
    name: str
    # Every built-in provider sets both in __init__ (see CLAUDE.md's "Every provider has
    # an env_dir instance attribute" section) - declared here so the code that reads
    # them directly off `self.provider`/`client.provider` (cli.py, dotfiles_commands.py,
    # context_commands.py, context_overrides.py) type-checks. This isn't statically
    # enforced against third-party providers loaded via entry points (mypy can't see
    # through that dynamic import) - the one call site that genuinely needs to tolerate
    # a third-party provider omitting them (RaazClient's pre-pull backup) already reads
    # them via getattr(..., default) instead of direct attribute access.
    env_dir: Path
    env_file_pattern: str

    def __init__(self, config: Optional[ProviderConfig] = None) -> None:
        ...

    def pull(self, app: str, env: str) -> None:
        ...

    def push(self, app: str, env: str) -> None:
        ...

    def list(self, app: str, env: str, prefix: Optional[str] = None) -> List[str]:
        ...

    def fetch_secrets(self, app: str, env: str) -> Dict[str, str]:
        """{filename: raw content} for app/env, without writing to disk."""

    @staticmethod
    def get_provider() -> Callable[[], Provider]:
        ...

fetch_secrets(app, env)

{filename: raw content} for app/env, without writing to disk.

Source code in raaz\providers\protocol.py
32
33
def fetch_secrets(self, app: str, env: str) -> Dict[str, str]:
    """{filename: raw content} for app/env, without writing to disk."""

ProviderConfig

Bases: Protocol

Source code in raaz\providers\protocol.py
40
41
42
43
44
class ProviderConfig(Protocol):
    def get_config(self) -> Dict[str, Any]:
        """Current config values as a plain dict for the provider's SDK client -
        distinct from config_fields' static schema (no values), and not required to
        include every config_fields entry."""

get_config()

Current config values as a plain dict for the provider's SDK client - distinct from config_fields' static schema (no values), and not required to include every config_fields entry.

Source code in raaz\providers\protocol.py
41
42
43
44
def get_config(self) -> Dict[str, Any]:
    """Current config values as a plain dict for the provider's SDK client -
    distinct from config_fields' static schema (no values), and not required to
    include every config_fields entry."""

Context

Source code in raaz\provider_context.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class RaazContext:
    def __init__(
        self,
        app: str,
        env: str | None = None,
        provider: str | None = None,
        dir: str | None = None,
    ) -> None:
        self.app = app
        self.env = env
        self.provider = provider
        self.dir = dir  # subfolder under .raaz/ holding this project's .env files, if any

    def print_current(self):
        table = Table(title=None, show_header=True, header_style="bold")
        table.add_column("App", style="green")
        table.add_column("Env", style="green")
        table.add_column("Provider", style="green")
        table.add_column(".env Dir", style="green")
        dir_display = f".raaz/{self.dir}" if self.dir else "project root"
        table.add_row(self.app, self.env, self.provider, dir_display)
        Print.print(table)

    def save(self):
        os.makedirs(os.path.dirname(RAAZ_CONTEXT_STORE), exist_ok=True)
        with open(RAAZ_CONTEXT_STORE, "w") as f:
            json.dump(self.__dict__, f)

    @classmethod
    def load(cls) -> Optional[RaazContext]:
        if os.path.exists(RAAZ_CONTEXT_STORE):
            with open(RAAZ_CONTEXT_STORE) as f:
                data = json.load(f)
            return cls(**data)
        return None

    @classmethod
    def clear(cls) -> None:
        if os.path.exists(RAAZ_CONTEXT_STORE):
            os.remove(RAAZ_CONTEXT_STORE)
            Print.info("Context cleared.")
        else:
            Print.error("No context file to clear.")

    @classmethod
    def resolve_context(cls,
                        app: Optional[str] = None,
                        env: Optional[str] = None,
                        provider: Optional[str] = None,
                        dir: Optional[str] = None,
                        ) -> RaazContext:
        """CLI options override the saved context; raises if neither yields a full set."""
        logging.info("Resolving context...")
        loaded = RaazContext.load()
        if not loaded and not (app and env and provider):
            Print.error("No context set or insufficient CLI overrides provided")
            raise ValueError("No context set or insufficient CLI overrides provided")

        if loaded is None:
            # Guaranteed by the check above: no saved context means app/env/provider
            # must all have been given.
            assert app and env and provider
            resolved = RaazContext(app=app, env=env, provider=provider, dir=dir)
        else:
            resolved = RaazContext(
                app=app or loaded.app,
                env=env or loaded.env,
                provider=provider or loaded.provider,
                dir=dir or loaded.dir,
            )
        logging.info(f"Resolved context {resolved.app}/{resolved.env}/{resolved.provider}")
        return resolved

    def __repr__(self) -> str:
        return f"Context(app={self.app}, env={self.env}, provider={self.provider}, dir={self.dir})"

resolve_context(app=None, env=None, provider=None, dir=None) classmethod

CLI options override the saved context; raises if neither yields a full set.

Source code in raaz\provider_context.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@classmethod
def resolve_context(cls,
                    app: Optional[str] = None,
                    env: Optional[str] = None,
                    provider: Optional[str] = None,
                    dir: Optional[str] = None,
                    ) -> RaazContext:
    """CLI options override the saved context; raises if neither yields a full set."""
    logging.info("Resolving context...")
    loaded = RaazContext.load()
    if not loaded and not (app and env and provider):
        Print.error("No context set or insufficient CLI overrides provided")
        raise ValueError("No context set or insufficient CLI overrides provided")

    if loaded is None:
        # Guaranteed by the check above: no saved context means app/env/provider
        # must all have been given.
        assert app and env and provider
        resolved = RaazContext(app=app, env=env, provider=provider, dir=dir)
    else:
        resolved = RaazContext(
            app=app or loaded.app,
            env=env or loaded.env,
            provider=provider or loaded.provider,
            dir=dir or loaded.dir,
        )
    logging.info(f"Resolved context {resolved.app}/{resolved.env}/{resolved.provider}")
    return resolved

Local env store

Source code in raaz\env_manager.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class EnvManager:
    def __init__(self, ctx: RaazContext | None = None) -> None:
        self.ctx = ctx
        self.base = RAAZ_HOME / "providers"

    @staticmethod
    def init(ctx: RaazContext):
        config_file = RAAZ_CONTEXT_STORE
        config_file.parent.mkdir(parents=True, exist_ok=True)
        config_file.write_text(json.dumps(ctx.__dict__, indent=2))
        Print.info("Raaz context initialized...")

    def get_env_dir(self, ctx: RaazContext) -> Path:
        """Return the local env dir for a context, shaped like every provider's
        ~/.raaz/providers/<provider>/... path (same root provider.env lives under)."""
        assert ctx.provider is not None and ctx.env is not None, "ctx must be fully resolved"
        return self.base / ctx.provider / ctx.app / ctx.env

get_env_dir(ctx)

Return the local env dir for a context, shaped like every provider's ~/.raaz/providers//... path (same root provider.env lives under).

Source code in raaz\env_manager.py
21
22
23
24
25
def get_env_dir(self, ctx: RaazContext) -> Path:
    """Return the local env dir for a context, shaped like every provider's
    ~/.raaz/providers/<provider>/... path (same root provider.env lives under)."""
    assert ctx.provider is not None and ctx.env is not None, "ctx must be fully resolved"
    return self.base / ctx.provider / ctx.app / ctx.env

Client facade

Source code in raaz\raaz_client.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class RaazClient:
    def __init__(self, provider: str | None = None, ctx: RaazContext | None = None) -> None:
        self.ctx: Optional[RaazContext] = None
        if ctx is not None:
            self.ctx = ctx
            provider = ctx.provider
            if provider is None:
                raise ValueError("ctx.provider is not set")
        elif provider is not None:
            pass
        else:
            raise ValueError("Must provide provider name or context")

        try:
            factory: Optional[Callable[[], Provider]] = get_provider(provider)
        except Exception as e:
            # e's message is already user-friendly (from the provider's own SDK guard).
            logging.error(e)
            raise ValueError(str(e)) from None

        if factory is None:
            raise ValueError(f"Provider '{provider}' not found. Installed: {', '.join(get_providers())}")

        try:
            self.provider: Provider = factory()
        except Exception as e:
            logging.error(e)
            raise ValueError(f"Failed to initialize provider '{provider}': {e}") from None

        if self.ctx is not None and self.ctx.dir:
            self.provider.env_dir = project_dir() / self.ctx.dir
            self.provider.env_dir.mkdir(parents=True, exist_ok=True)

    def _require_ctx(self) -> RaazContext:
        """Every real operation below (pull/push/list/fetch_secrets/diff/run) needs a
        full RaazContext - the provider-only construction mode has no legitimate use
        past __init__ today (nothing in this codebase constructs RaazClient without
        ctx=). Narrows self.ctx from Optional for the type checker and gives a clear
        error instead of an AttributeError if that ever changes."""
        if self.ctx is None:
            raise ValueError("This operation requires a RaazContext - construct RaazClient with ctx=...")
        return self.ctx

    @audited("pull")
    def pull(self, filtered: bool = False, filter_desc: Optional[str] = None) -> None:
        """`filtered=True` (a --pattern/filename was given) fetches everything and
        writes only matching entries, instead of delegating to provider.pull(),
        which always writes everything under app/env regardless of pattern."""
        ctx = self._require_ctx()
        assert ctx.env is not None, "ctx.env is not set"
        logging.debug(f"Pulling env for {ctx.app}/{ctx.env}/{ctx.provider}")
        Print.warn(f"Pulling env for {ctx.app}/{ctx.env}/{ctx.provider}...")

        pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
        env_dir = getattr(self.provider, "env_dir", Path.cwd())
        snapshot = backup_manager.create_snapshot(pattern, env_dir)
        if snapshot is not None:
            Print.note(f"Backed up existing local files to {snapshot} - restore with `raaz rollback`", True)

        if not filtered:
            self.provider.pull(app=ctx.app, env=ctx.env)
            return

        regex = re.compile(pattern)
        matched = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
        if not matched:
            desc = filter_desc or f"pattern '{pattern}'"
            Print.warn(f"Nothing to pull - no secret matched {desc} for {ctx.app}/{ctx.env}.")
            return

        env_dir.mkdir(parents=True, exist_ok=True)
        for filename, content in matched.items():
            (env_dir / filename).write_text(content)
            Print.status(f"Pulling {filename}...")
        Print.info(f"Pulled {len(matched)} file(s) for {ctx.app}/{ctx.env}/{ctx.provider}.", True)

    @audited("push")
    def push(self, no_examples: bool = False) -> None:
        ctx = self._require_ctx()
        assert ctx.env is not None, "ctx.env is not set"
        logging.debug(f"Pushing env for {ctx.app}/{ctx.env}/{ctx.provider}")
        Print.warn(f"Pushing env for {ctx.app}/{ctx.env}/{ctx.provider}...")
        self.provider.push(app=ctx.app, env=ctx.env)

        if no_examples:
            return
        self._refresh_examples()

    def _refresh_examples(self) -> None:
        """A <file>.example next to a real file is what marks it as "keep this
        example up to date" - no separate config to track, and no per-provider
        code needed since this only ever touches local files in env_dir."""
        pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
        env_dir = getattr(self.provider, "env_dir", Path.cwd())
        refreshed = []
        for file in find_env_files(env_dir, pattern):
            example_path = env_dir / f"{file.name}.example"
            if example_path.exists():
                example_path.write_text(generate_example_content(file.read_text()))
                refreshed.append(example_path.name)
        if refreshed:
            Print.note(f"Refreshed example file(s): {', '.join(refreshed)}", True)

    def list(self, prefix: Optional[str] = None) -> List[str]:
        ctx = self._require_ctx()
        assert ctx.env is not None, "ctx.env is not set"
        logging.debug(f"Listing secrets for {ctx.app}/{ctx.env}/{ctx.provider}")
        secrets = self.provider.list(app=ctx.app, env=ctx.env, prefix=prefix)
        return secrets

    def fetch_secrets(self) -> Dict[str, str]:
        """{filename: raw content} for the current context - nothing written to disk."""
        ctx = self._require_ctx()
        assert ctx.env is not None, "ctx.env is not set"
        logging.debug(f"Fetching secrets for {ctx.app}/{ctx.env}/{ctx.provider}")
        return self.provider.fetch_secrets(app=ctx.app, env=ctx.env)

    def unpushed_changes(self) -> List[str]:
        """Local .env-style files that differ from, or were never pushed to, this
        context - used to warn before `raaz ctx use` switches away."""
        never_pushed, edited = self.unpushed_changes_detail()
        return never_pushed + edited

    def unpushed_changes_detail(self) -> Tuple[List[str], List[str]]:
        """Same as unpushed_changes(), split into (never_pushed, edited_since_push) -
        the two read as ambiguous when merged into one warning message."""
        remote = self.fetch_secrets()
        pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
        env_dir = getattr(self.provider, "env_dir", Path.cwd())
        never_pushed: List[str] = []
        edited: List[str] = []
        for file in find_env_files(env_dir, pattern):
            remote_content = remote.get(file.name)
            local_content = file.read_text()
            if remote_content == local_content:
                continue
            (never_pushed if remote_content is None else edited).append(file.name)
        return never_pushed, edited

    def diff(self) -> DiffResult:
        """Full local-vs-remote comparison (local-only/remote-only/changed/unchanged),
        without pulling or pushing. Both sides filtered by env_file_pattern, since
        cloud providers' fetch_secrets() otherwise returns everything under app/env
        regardless of pattern."""
        pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
        env_dir = getattr(self.provider, "env_dir", Path.cwd())
        regex = re.compile(pattern)
        remote = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
        local = {file.name: file.read_text() for file in find_env_files(env_dir, pattern)}

        local_only, remote_only, changed, unchanged = _bucket(local, remote)
        return DiffResult(local, remote, local_only, remote_only, changed, unchanged)

    def diff_envs(self, against_env: str) -> DiffResult:
        """Remote-vs-remote: the current context's env vs. `against_env`, same
        app/provider - never touches local files at all. Reuses DiffResult's shape
        (see its docstring) purely to avoid a near-duplicate type for one caller."""
        ctx = self._require_ctx()
        pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
        regex = re.compile(pattern)
        current = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
        other_raw = self.provider.fetch_secrets(app=ctx.app, env=against_env)
        other = {name: content for name, content in other_raw.items() if regex.search(name)}

        current_only, other_only, changed, unchanged = _bucket(current, other)
        return DiffResult(current, other, current_only, other_only, changed, unchanged)

    def run(self, command: List[str], verbose: bool = False) -> int:
        """Run `command` with every secret injected as env vars - nothing written to
        disk. `verbose=True` prints a table of what's being injected first."""
        ctx = self._require_ctx()
        if not command:
            raise ValueError("No command given")

        env_vars: Dict[str, str] = {}
        table = Table(title=None, show_header=True, header_style="bold") if verbose else None
        if table is not None:
            table.add_column("File", style="cyan")
            table.add_column("Key", style="green")
            table.add_column("Value")

        for filename, content in self.fetch_secrets().items():
            parsed = dotenv_values(stream=StringIO(content))
            for key, value in parsed.items():
                if value is None:
                    continue
                env_vars[key] = value
                if table is not None:
                    table.add_row(filename, key, value)

        if table is not None:
            Print.print(table)

        Print.warn(
            f"Running with env for {ctx.app}/{ctx.env}/{ctx.provider}: {command}"
        )
        result = subprocess.run(command, env={**os.environ, **env_vars})
        return result.returncode

diff()

Full local-vs-remote comparison (local-only/remote-only/changed/unchanged), without pulling or pushing. Both sides filtered by env_file_pattern, since cloud providers' fetch_secrets() otherwise returns everything under app/env regardless of pattern.

Source code in raaz\raaz_client.py
192
193
194
195
196
197
198
199
200
201
202
203
204
def diff(self) -> DiffResult:
    """Full local-vs-remote comparison (local-only/remote-only/changed/unchanged),
    without pulling or pushing. Both sides filtered by env_file_pattern, since
    cloud providers' fetch_secrets() otherwise returns everything under app/env
    regardless of pattern."""
    pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
    env_dir = getattr(self.provider, "env_dir", Path.cwd())
    regex = re.compile(pattern)
    remote = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
    local = {file.name: file.read_text() for file in find_env_files(env_dir, pattern)}

    local_only, remote_only, changed, unchanged = _bucket(local, remote)
    return DiffResult(local, remote, local_only, remote_only, changed, unchanged)

diff_envs(against_env)

Remote-vs-remote: the current context's env vs. against_env, same app/provider - never touches local files at all. Reuses DiffResult's shape (see its docstring) purely to avoid a near-duplicate type for one caller.

Source code in raaz\raaz_client.py
206
207
208
209
210
211
212
213
214
215
216
217
218
def diff_envs(self, against_env: str) -> DiffResult:
    """Remote-vs-remote: the current context's env vs. `against_env`, same
    app/provider - never touches local files at all. Reuses DiffResult's shape
    (see its docstring) purely to avoid a near-duplicate type for one caller."""
    ctx = self._require_ctx()
    pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
    regex = re.compile(pattern)
    current = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
    other_raw = self.provider.fetch_secrets(app=ctx.app, env=against_env)
    other = {name: content for name, content in other_raw.items() if regex.search(name)}

    current_only, other_only, changed, unchanged = _bucket(current, other)
    return DiffResult(current, other, current_only, other_only, changed, unchanged)

fetch_secrets()

{filename: raw content} for the current context - nothing written to disk.

Source code in raaz\raaz_client.py
163
164
165
166
167
168
def fetch_secrets(self) -> Dict[str, str]:
    """{filename: raw content} for the current context - nothing written to disk."""
    ctx = self._require_ctx()
    assert ctx.env is not None, "ctx.env is not set"
    logging.debug(f"Fetching secrets for {ctx.app}/{ctx.env}/{ctx.provider}")
    return self.provider.fetch_secrets(app=ctx.app, env=ctx.env)

pull(filtered=False, filter_desc=None)

filtered=True (a --pattern/filename was given) fetches everything and writes only matching entries, instead of delegating to provider.pull(), which always writes everything under app/env regardless of pattern.

Source code in raaz\raaz_client.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@audited("pull")
def pull(self, filtered: bool = False, filter_desc: Optional[str] = None) -> None:
    """`filtered=True` (a --pattern/filename was given) fetches everything and
    writes only matching entries, instead of delegating to provider.pull(),
    which always writes everything under app/env regardless of pattern."""
    ctx = self._require_ctx()
    assert ctx.env is not None, "ctx.env is not set"
    logging.debug(f"Pulling env for {ctx.app}/{ctx.env}/{ctx.provider}")
    Print.warn(f"Pulling env for {ctx.app}/{ctx.env}/{ctx.provider}...")

    pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
    env_dir = getattr(self.provider, "env_dir", Path.cwd())
    snapshot = backup_manager.create_snapshot(pattern, env_dir)
    if snapshot is not None:
        Print.note(f"Backed up existing local files to {snapshot} - restore with `raaz rollback`", True)

    if not filtered:
        self.provider.pull(app=ctx.app, env=ctx.env)
        return

    regex = re.compile(pattern)
    matched = {name: content for name, content in self.fetch_secrets().items() if regex.search(name)}
    if not matched:
        desc = filter_desc or f"pattern '{pattern}'"
        Print.warn(f"Nothing to pull - no secret matched {desc} for {ctx.app}/{ctx.env}.")
        return

    env_dir.mkdir(parents=True, exist_ok=True)
    for filename, content in matched.items():
        (env_dir / filename).write_text(content)
        Print.status(f"Pulling {filename}...")
    Print.info(f"Pulled {len(matched)} file(s) for {ctx.app}/{ctx.env}/{ctx.provider}.", True)

run(command, verbose=False)

Run command with every secret injected as env vars - nothing written to disk. verbose=True prints a table of what's being injected first.

Source code in raaz\raaz_client.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def run(self, command: List[str], verbose: bool = False) -> int:
    """Run `command` with every secret injected as env vars - nothing written to
    disk. `verbose=True` prints a table of what's being injected first."""
    ctx = self._require_ctx()
    if not command:
        raise ValueError("No command given")

    env_vars: Dict[str, str] = {}
    table = Table(title=None, show_header=True, header_style="bold") if verbose else None
    if table is not None:
        table.add_column("File", style="cyan")
        table.add_column("Key", style="green")
        table.add_column("Value")

    for filename, content in self.fetch_secrets().items():
        parsed = dotenv_values(stream=StringIO(content))
        for key, value in parsed.items():
            if value is None:
                continue
            env_vars[key] = value
            if table is not None:
                table.add_row(filename, key, value)

    if table is not None:
        Print.print(table)

    Print.warn(
        f"Running with env for {ctx.app}/{ctx.env}/{ctx.provider}: {command}"
    )
    result = subprocess.run(command, env={**os.environ, **env_vars})
    return result.returncode

unpushed_changes()

Local .env-style files that differ from, or were never pushed to, this context - used to warn before raaz ctx use switches away.

Source code in raaz\raaz_client.py
170
171
172
173
174
def unpushed_changes(self) -> List[str]:
    """Local .env-style files that differ from, or were never pushed to, this
    context - used to warn before `raaz ctx use` switches away."""
    never_pushed, edited = self.unpushed_changes_detail()
    return never_pushed + edited

unpushed_changes_detail()

Same as unpushed_changes(), split into (never_pushed, edited_since_push) - the two read as ambiguous when merged into one warning message.

Source code in raaz\raaz_client.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def unpushed_changes_detail(self) -> Tuple[List[str], List[str]]:
    """Same as unpushed_changes(), split into (never_pushed, edited_since_push) -
    the two read as ambiguous when merged into one warning message."""
    remote = self.fetch_secrets()
    pattern = getattr(self.provider, "env_file_pattern", ENV_FILENAME_PATTERN)
    env_dir = getattr(self.provider, "env_dir", Path.cwd())
    never_pushed: List[str] = []
    edited: List[str] = []
    for file in find_env_files(env_dir, pattern):
        remote_content = remote.get(file.name)
        local_content = file.read_text()
        if remote_content == local_content:
            continue
        (never_pushed if remote_content is None else edited).append(file.name)
    return never_pushed, edited